release: Chatter.Rest.Hal 2.0.0 - #122
Conversation
…CURIE expansion (#115) * fix(builders): resolve the owning resource in FindParent walks FindParent<T>() matched the calling builder itself before walking to Parent. That made ResourceCollectionBuilder — which covariantly satisfies IBuildHalPart<Resource> but throws from that BuildPart() — resolve itself, and left the fallback branch in ResourceCollectionBuilder.AddEmbedded as dead code that would have recursed forever if reached. FindParent now starts at Parent and walks up iteratively. Builders that satisfy IBuildHalPart<T> for a part they cannot build declare it via the new internal IDeclareUnbuildableHalParts marker and are skipped, so a lookup never resolves to a builder whose BuildPart() throws. LinkObjectBuilder.AddEmbedded and ResourceCollectionBuilder.AddEmbedded now resolve the nearest Resource ancestor before falling back to an EmbeddedResourceCollection ancestor. Previously a link object on an embedded resource skipped past its own resource to the root resource's embedded collection, silently attaching the new embed to the wrong node. Refs #103 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(builders): stop AddLink/AddSelf/AddCuries throwing after AddResources ResourceCollectionBuilder's explicit IAddLinkToEmbeddedStage, IAddSelfLinkToEmbeddedStage and IAddCuriesLinkToEmbeddedStage implementations looked up FindParent<LinkCollection>() and dereferenced the result with the null-forgiving operator. A LinkCollectionBuilder is only ever a child of a resource builder, never an ancestor of a ResourceCollectionBuilder, so the lookup returned null every time and plain fluent chaining such as ResourceBuilder.New().AddEmbedded("items").AddResources(items).AddLink("next") threw NullReferenceException. These stages now resolve the resource that owns the "_embedded" entry being filled and add the link to that resource's link collection. Per-resource links remain configurable through the AddResources builder callback. The remaining null-forgiving parent dereferences in ResourceCollectionBuilder, ResourceCollectionResourceBuilder and LinkObjectBuilder are replaced with explicit InvalidOperationException guards that name the missing ancestor. Refs #102 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(builders): merge repeated link relations into a single link AddLink/AddSelf/AddCuries appended a new LinkBuilder on every call, so a repeated relation produced two Link instances and the converter wrote the relation twice: ResourceBuilder.New().AddLink("a").AddLinkObject("/1") .AddLink("a").AddLinkObject("/2").Build(); // {"_links":{"a":{"href":"/1"},"a":{"href":"/2"}}} Duplicate member names are ambiguous per RFC 8259 and most parsers keep only the last, silently dropping the first link. HAL's "_links" is a JSON object keyed by relation, so the duplicate could never be spec-valid. LinkCollectionBuilder now indexes its link builders by relation (ordinal, so relations stay case-sensitive) and returns the existing builder when a relation repeats. Link objects merge into that single link and the array form follows naturally from the object count, so a second AddCuries() extends the one "curies" array. First-insertion order is preserved. Refs #104 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(extensions): correct link lookup, CURIE expansion and builder validation Four defects in the link extensions and their builder call sites: - The single-argument GetLinkObjectOrDefault is documented as returning the first link object but used SingleOrDefault, throwing InvalidOperationException for any relation legitimately carrying more than one link object. It now uses FirstOrDefault. - ExpandCurieRelation resolved "curies" through GetLinkOrDefault, which throws when a collection carries two "curies" links, breaking its documented promise to return the original relation for every edge case. It now searches the link objects of every "curies" link directly. - ExpandCurieRelation substituted the CURIE reference verbatim, so "a:x y#z" injected a raw space and fragment into the href; it "expanded" an empty reference such as "a:"; and it ignored the definition's templated flag. The reference is now percent-encoded per RFC 6570 simple string expansion, an empty reference is returned unchanged, and a definition that is not marked templated is not expanded. - Null or whitespace rel/href/name arguments were accepted at the call site and only threw from inside Build(), far from the faulting call. AddLink and AddLinkObject now validate eagerly with ArgumentException. Two existing assertions encoded the old expansion behaviour and are updated: "foo:bar:baz" now expands with the colon percent-encoded, and "acme:" is returned unchanged. Refs #105 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ON (#116) * fix(converters): honor caller case sensitivity, keep HAL reserved names literal Every converter Read hardcoded JsonNodeOptions.PropertyNameCaseInsensitive = true, ignoring the caller's JsonSerializerOptions. That violated the HAL specification (draft-kelly-json-hal section 4.1 defines _links and _embedded as literal, case-sensitive reserved names) and caused two further defects: - state properties differing only by case ({"Name":"a","name":"b"} is legal JSON) collided in the case-insensitive node tree and broke the resource - ResourceConverter looked up the reserved names case-insensitively but stripped them from state case-sensitively, so "_LINKS" both populated Links and stayed in state, serializing the link data twice All eight converters now derive their JsonNodeOptions from the caller's JsonSerializerOptions.PropertyNameCaseInsensitive, so ordinary property names follow the caller's configuration. The reserved _links/_embedded lookups are matched ordinally regardless of that setting, keeping the lookup and the state stripping consistent in both directions. Refs #93 * fix(converters): surface malformed HAL JSON as JsonException Malformed input leaked InvalidOperationException/ArgumentException out of the converters, so callers that catch JsonException crashed. Every case verified in the issue now throws System.Text.Json.JsonException: - LinkObjectConverter: a non-string "href" (123, {}, []) and a Link Object that is not a JSON object (reachable via {"_links":{"self":[[]]}}) replace GetValue<string>()/indexer InvalidOperationException with JsonException. - LinkCollectionConverter: a numeric or boolean rel value ({"_links":{"self":123}}) is rejected instead of throwing from GetValue<string?>(); the string href shorthand still works. - LinkCollectionConverter and EmbeddedResourceCollectionConverter: a reserved array containing non-objects ({"_links":[1]}, {"_embedded":[1]}) is rejected instead of throwing from AsObject(), as is a reserved value that is neither an object nor an array. - LinkObjectCollectionConverter: a non-string primitive element is rejected rather than silently swallowed by a catch-all. Duplicate property names are normalized last-wins by a new ConverterHelpers.ParseNode, which materializes the node tree through JsonDocument instead of JsonNode.Parse. JsonNode otherwise defers a duplicate-key ArgumentException to whenever the object is first materialized, which for the lazy resource creators is property-access time. Last-wins matches what most JSON parsers do with a construct RFC 8259 leaves undefined, and it keeps the read path compatible with the decision in #86 that the link and embedded collections reject duplicate keys. The link and embedded collection converters additionally de-duplicate rel/name across array elements, keeping the first-seen position. Refs #94 * fix(converters): reject non-object resources at parse time ResourceConverter's lazy linkCollectionCreator/embeddedCollectionCreator closures indexed the parsed node without checking it was a JsonObject, so a primitive or array resource deserialized "successfully" and then threw InvalidOperationException at property-access time, far from the deserialization call — Resource.Parse("123")!.Links and Resource.Parse("{\"_embedded\":{\"orders\":[123]}}")! .Embedded[0].Resources[0].Links both reproduced it. Per the exception contract in #86 the converter now rejects a non-object resource with JsonException at parse time, and materializes _links and _embedded eagerly during Read so a malformed member fails at the deserialization call rather than on a later property read. The creator delegates are retained, returning the already-materialized collections, so Resource's internal constructor and the lazy Links/Embedded getters are unchanged. ResourceConvertersTests.Resource_As_Typed_For_Primitive_Should_Return_Value asserted the old tolerance — that "123" deserializes into a usable Resource — which is exactly the defect this change removes and which contradicts the existing HalDeserializationRobustnessTests root-type expectations. It is updated in place to assert the new contract. Refs #95 * fix(converters): build node tree ordinally and stop cloning per scalar Addresses two review findings on the parse path introduced by this PR. Deriving the node tree's JsonNodeOptions from the caller's PropertyNameCaseInsensitive merged distinct JSON members before any lookup could run. A document carrying both "_LINKS" and "_links" collapsed to one entry, so depending on member order either the literal _links was lost or the case variant was treated as reserved, and state properties or relation names differing only by case were discarded the same way — the very defect #93 exists to remove, reintroduced for callers that opt into case-insensitivity. The tree is now always built with an ordinal comparer. Case-insensitive matching is applied at lookup time, by ConverterHelpers.GetProperty, and only to ordinary Link Object attribute names; the reserved names stay literal, and an exact match still wins over a case variant. Cloning each scalar JsonElement out of the JsonDocument allocated a separate backing document per value, so a large state array multiplied allocations by an order of magnitude. Well-formed payloads no longer go through JsonDocument at all: duplicate property names are detected with an allocation-free pre-scan over a copy of the reader (Utf8JsonReader is a struct), and only a payload that actually contains duplicates is rebuilt — cloning the root element once rather than once per scalar. The pre-scan is deliberately conservative, taking the rebuild path on a hash collision, an escaped property name, or a name split across buffer segments, all of which yield the same tree. Measured on Resource.Parse, before -> after: 100k-scalar state array 24.69 MB -> 2.27 MB 20k embedded HAL resources 201.68 MB -> 93.83 MB Refs #93, #94 * fix(converters): hash-based duplicate scan overflow, option-aware href prechecks The duplicate-name pre-scan's overflow list scanned linearly past the 16-entry inline buffer, making wide untrusted objects O(n^2); a HashSet keeps it linear. LinkConverter's href prechecks used ordinal indexing after the DOM went ordinal, rejecting case-variant hrefs that LinkObjectConverter would accept under PropertyNameCaseInsensitive; both prechecks now go through ConverterHelpers.GetProperty. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY * fix(converters): walk existing node tree for nested resources, scan and rebuild iteratively Nested resources were materialized with JsonNode.Deserialize, which re-serializes the subtree to UTF-8 and re-parses it — once per ancestor, turning a deeply nested _embedded chain into O(depth x size) work even when the caller never touches Embedded. Internal ReadFromNode paths on the resource, link-collection, embedded-collection, and resource-collection converters now walk the already-parsed tree, so the whole document is materialized in O(size). The duplicate-name pre-scan and rebuild also recursed once per nesting level (the scan with a stackalloc per object frame), so a caller raising MaxDepth could hit an uncatchable StackOverflowException on permitted input. Both are now iterative with heap-tracked depth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY * fix(converters): dispatch nested parts through options-registered custom converters The node-walking fast path bypassed custom JsonConverter registrations for LinkCollection, EmbeddedResourceCollection, Resource, and ResourceCollection, though options-registered converters are documented to take precedence over attribute-wired ones. Each dispatch point now checks options.GetConverter and falls back to JsonNode.Deserialize when the selected converter is not the built-in, keeping the fast path for the default configuration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY * fix(converters): last case-insensitive variant wins in GetProperty fallback The fallback returned the first case-insensitive match, while the old case-insensitive JsonObject resolved a later variant over an earlier one and duplicate keys normalize last-wins everywhere else. Exact-name preference is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY * fix(converters): custom Resource converters see every collection item shape The object-shape guard ran before the custom-converter dispatch, so an options-registered Resource converter that deliberately accepts scalar items threw instead of running. The custom path now dispatches first for any node shape; the built-in path keeps the guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
) * fix(converters): stop dropping state fields named Links/Embedded ResourceConverter.Write skipped state properties whose names matched the naming-policy-converted CLR names "Links"/"Embedded". Real _links/_embedded never reach the state object — ReadFromNode's jsonObjectCreator strips them — so the filter could only ever delete legitimate user state, and under a camelCase policy it deleted a state field literally named "links". Compare against the literal reserved names instead, and skip a state property carrying one only when the resource's own collection is written under the same name, so the output never contains a duplicate JSON member. Closes #96 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(converters): preserve embedded array shape on round trip HAL clients read array-vs-object shape as the signal that a relation is a collection. Links preserved it via Link.IsArray; embedded resources did not. - Reading an _embedded relation whose value is a JSON array now sets ForceWriteAsCollection, so a single-element array no longer collapses to an object on round trip. Applied in both EmbeddedResourceCollectionConverter and the standalone EmbeddedResourceConverter. - EmbeddedResourceConverter.Write now honors ForceWriteAsCollection instead of delegating shape selection to ResourceCollectionConverter, so an instance serializes identically standalone and inside an _embedded collection. - Pin the empty-relation shape: a relation carrying no link objects writes as [], the only spec-conformant rendering, documented on LinkCollectionConverter.Write and covered by tests. Closes #97 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(converters): primitive state, registration guard, multi-rel, node reuse - ResourceConverter.Write rejects state that serializes to something other than a JSON object with a JsonException instead of letting EnumerateObject leak an InvalidOperationException. A HAL Resource Object has no place for a primitive or array state; state serializing to JSON null contributes no members. - AddHalConverters guards per converter type. The old guard checked only LinkCollectionConverter, so a consumer who had registered just that one got a silent no-op and the remaining seven converters were never registered (and halOptions was ignored). A consumer's own converter stays ahead of ours in the list, so JsonSerializer keeps selecting it. - EmbeddedResourceConverter.Read throws on a multi-relation object instead of keeping the first relation and dropping the rest; a single EmbeddedResource holds exactly one name. EmbeddedResourceCollection remains the multi-relation reader. - The remaining node-to-UTF-8-to-node hops are gone: EmbeddedResourceConverter, LinkCollectionConverter, LinkConverter, and LinkObjectCollectionConverter now materialize nested parts from the retained JsonNode subtree through internal ReadFromNode fast paths, with options-registered custom converters still dispatched ahead of them. Closes #98 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(converters): keep custom ResourceCollection converters on the write path Inlining the embedded shape logic in EmbeddedResourceConverter.Write bypassed a consumer's options-registered JsonConverter<ResourceCollection>, which the previous delegation to JsonSerializer.Serialize had dispatched through. Both write paths now route through ConverterHelpers.WriteEmbeddedResources, which hands the whole collection to a custom converter when one is registered and applies the built-in count/ForceWriteAsCollection rule otherwise. Sharing the routine also keeps the standalone and in-collection shapes identical by construction, including in the custom-converter case — EmbeddedResourceCollection Converter.Write previously bypassed such a converter too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e caches (#114) * fix(domain): reject duplicate rels and keep collection indexes consistent LinkCollection.Add overwrote _index[item.Rel] on a duplicate relation and Remove dropped the index entry keyed by rel without checking that the indexed value was the removed item, so the backing list and the relation index desynchronized. EmbeddedResourceCollection had the identical defect keyed by name. Per the duplicate-rel decision recorded in #86, Add now throws ArgumentException on a duplicate relation (or embedded resource name), completing the dictionary semantics the existing index already implied. HAL models _links and _embedded as JSON objects keyed by relation, so duplicate entries could never serialize into spec-valid output. With uniqueness enforced at Add, Remove can drop the index entry unconditionally and stay consistent. Add and Remove also reject null instead of throwing NullReferenceException. Three existing extension tests asserted the removed duplicate-tolerant behavior and could no longer compile-and-pass as written; they now assert that duplicates are rejected at Add time. Refs #99 * fix(domain): give HAL domain types real structural equality The synthesized record equality on Resource included the mutable private fields that back its lazy caches (_linksImpl, _embeddedImpl, _stateObject, _resourceNode). Reading Links, Embedded, State<T> or As<T> populated those fields, so a pure read changed the resource's hash code and evicted it from any hash-based collection it had been placed in. The synthesized equality on the collection records was also only nominally structural: each holds a Collection<T> (and, for the keyed collections, a Dictionary<,>) that the compiler compares by reference, so two collections holding identical content were never equal and Remove/Contains could not find a structurally equal item. Per the semver decision in #86 the types stay records, preserving source compatibility and with-expressions; Equals/GetHashCode are overridden explicitly instead: - Resource compares its links, its embedded resources and a stable JSON key for its state. The caches are excluded, so the hash code no longer changes when a getter runs. For a parsed resource the state key is derived from the original JSON rather than from the cached state object, which State<T> replaces. - LinkCollection and EmbeddedResourceCollection compare by relation and by name respectively, since a HAL "_links"/"_embedded" value is a JSON object and JSON object members are unordered. The derived index takes no part in equality. - LinkObjectCollection and ResourceCollection compare positionally, since both serialize as JSON arrays, where order is significant. Link, LinkObject and EmbeddedResource keep their synthesized equality, which is now genuinely structural because the collection members they hold are. Refs #100 * fix(domain): correct the State<T> and As<T> caches on Resource State<T> cached the first successful deserialization in _stateObject and then returned it through an unguarded (T?) cast. A later State<U> for a different type threw InvalidCastException, which the blanket catch swallowed, so the call returned null even though the JSON deserialized as U perfectly well. The cache is now only reused when it already holds an instance of T; otherwise the state is deserialized again from the underlying JSON. The cache is populated only when it was empty, so projecting a resource onto a second state type cannot replace the state the resource serializes from. As<T> cached the serialized JsonNode on its first call and never invalidated it, so any link, embedded resource or state change made afterwards was invisible to every later call. A resource constructed in memory is mutable and is now re-serialized on every call. A parsed resource keeps converting from the document it was parsed from, which preserves the original shape and is the behavior the node was there to provide. Refs #101 * fix(domain): canonicalize the Resource state equality key Two defects in the state half of Resource equality, both raised in review of #114. The key came from JsonObject.ToJsonString(), which preserves insertion order, so two resources parsed from the same state properties written in a different order compared unequal and hashed differently. That contradicted the rest of the equality design, where LinkCollection and EmbeddedResourceCollection already compare order-insensitively because JSON object members are unordered. The key is now canonical: object properties are ordered by name and array element order is preserved, since JSON array order is significant. Canonicalization runs over a JsonNode, so a parsed resource and an in-memory resource holding the same state produce the same key. Serialization failures were also mapped onto the same null key used for an absent state, so a resource whose state cannot be represented as JSON — a cyclic graph, a delegate — compared equal to a stateless resource, and two resources holding different unserializable states compared equal to each other. The key helper now reports whether the state could be represented at all; when either side could not, equality falls back to reference identity of the state object, and every such state contributes one fixed hash-code component so equal resources still agree on their hash code. Refs #100 * fix(domain): empty state object normalizes to the absent-state equality key Resource.Parse("{}") and new Resource() serialize to the identical HAL document, so they must compare equal under HAL-content equality. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY * fix(domain): preserve constructor-supplied JsonElement for later State projections The first State<T>() call replaced a JsonElement state with the projected object, so a later State<U>() reached the always-null creator of public-constructor instances and returned null. The original element is preserved as the rematerialization source, giving constructed resources the same multi-type projection contract as parsed ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY * fix(domain): equality ignores shape flags without serialization effect; key from source element Link.IsArray and EmbeddedResource.ForceWriteAsCollection only change the serialized document when the count is exactly one, so explicit equality normalizes the flag to its effective value. The state equality key of a constructor-supplied JsonElement resource derives from the preserved element rather than a projected DTO, so reading state cannot change the hash code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY * fix(domain): State<T> projections are detached from the serialization source A projection cached into _stateObject let a mutated DTO change what Write serializes while equality kept reading the original JSON — the two could permanently disagree. State<T> now returns a detached snapshot materialized from the state of record (parsed JSON or the constructor-supplied JsonElement) on every call; only a state supplied directly as T is returned by reference. This also removes the _sourceStateElement field added in the previous round — the element simply stays the state of record. Three tests asserting the old same-instance caching contract are updated to the detached contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY * fix(domain): LinkObject equality normalizes converter-omitted optional values LinkObjectConverter.Write omits optional string properties that are null or whitespace-only alike, so both forms serialize to identical HAL and must compare equal with matching hash codes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY * docs(domain): describe detached State<T> projections; drop obsolete caching claims Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY * docs(domain): As<T> no longer caches for in-memory resources; describe both paths Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY * fix(domain): equality key mirrors WhenWritingNull omission of null state properties With DefaultIgnoreCondition = WhenWritingNull in a resource's own options, ResourceConverter.Write omits top-level null-valued state properties, so the equality key omits them identically. The options are immutable per-instance state, so the key stays deterministic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY * docs(domain): note the by-reference constructor-state exception in the api summary Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY * test: reconcile curie tests with the duplicate-rel reject policy after integration Merging release/2.0 (Tracks 1, 2, 4) surfaced two Track 4 tests that hand-build a second 'curies' link, a state LinkCollection.Add now rejects per #86 decision 1. The multi-definition scenario lives as multiple curie link objects within the single curies link; the two-links hazard is asserted as rejected at Add. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY * fix(domain): equality key mirrors reserved-name suppression and null-serializing state Two more writer/key mirror gaps against the integrated WriteStateMembers: a state property literally named _links/_embedded is excluded from the key exactly when the writer suppresses it (nonempty own collection written under that name), and a state that serializes to JSON null keys as absent, matching the empty document the writer produces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY * refactor(domain): derive the state equality key from the writer itself The key builder had become a hand-maintained mirror of ResourceConverter's write rules (null omission, reserved-name suppression, null/empty state), and every writer change could silently desynchronize it — the source of a long review-comment stream. The writer's state-member emission is now factored into SerializeStateMembersToUtf8 (shared CreateStateDocument + WriteStateMembers code paths), and equality canonicalizes those exact bytes. All mirrored rules are deleted; equality is state-equal exactly when the writer emits the same members, by construction, for any future writer behavior. All prior equality tests pass unchanged against the derived implementation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY * fix(domain): netstandard2.0 build — JsonSerializerOptions.Default unavailable Use a shared default-configured instance for equality-key serialization; .Default is net7+ only and the package multi-targets netstandard2.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
) * fix(builders): curies serializes as an array by default per HAL spec section 8.3 The spec establishes CURIEs via an array of Link Objects and common HAL clients index _links.curies as an array, so a single definition must still serialize as a one-element array. AsArray() becomes a no-op. Found by the full spec-conformance audit; the single-definition default was the one medium-severity deviation. Closes #119 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY * fix(builders): curies array default applies to every builder path Set in the LinkBuilder constructor keyed on the rel, so AddCuries(), AddLink("curies"), and merges into an existing builder all get the array form; the factory-only initializer missed the generic path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Atomic version bump per CLAUDE.md: csproj Version, CLAUDE.md package table, and CHANGELOG.md release section with breaking-changes and migration notes for the coordinated 2.0.0 fix wave. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY
# Conflicts: # CLAUDE.md
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ba9133720f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…eption Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a9523c70a6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Merges the release/2.0 integration branch: the coordinated fix wave from the full-repository review (Tracks 1–4: converters input hardening, write-side correctness, domain types, builders/extensions — 22 issues), the HAL §8.3 curies conformance fix (#119), and the atomic 2.0.0 version bump (csproj + CLAUDE.md table + CHANGELOG with breaking-changes/migration section).
Validation: 446 tests green on the integrated branch; clause-by-clause HAL spec-conformance audit passed (28/30 conform, both deviations resolved — #119 fixed, #120 documented and deferred); CodeQL green.
On merge, hal-cicd deploys
Chatter.Rest.Hal 2.0.0to NuGet (deploy environment approval required) and create-version-tag cutshal/v2.0.0.🤖 Generated with Claude Code
https://claude.ai/code/session_01TaPH7EX9mPnwPDegPBophY