agent: builder model: opus max_turns: 60 budget_usd: 12 —

Brief: yaml load, the tree a program reads: aliases expanded within the limit, a duplicate key an error (#928 step 4, second half)

Score: #928 step 4, the next step of the package after y21-events.md (which runs just before this brief, in the same checkout; its commit is on your develop). This change touches only packages/yaml and needs no version raise and no CHANGELOG.md entry (ADR-yaml-1 D1).

Read docs/builder-facts.md, ADR-yaml-1 D4, D6, D8 (docs/specification/adr/adr-yaml-1.md), and in packages/yaml/src/main.nika the tree types at the top, Limits, Error, place_of, documents, parse, as_i64, as_f64.

Pre-flight

rg -n "pub fn events|pub fn event_line" packages/yaml/src/main.nika shows both (y21 run 4 built). If not, build nothing here: Result: blocked, Found: y21 not built. Rebuild target/fast/nikaia first (cargo build --profile fast -p nikaia): it must hold #1070-#1072 (y24-y26) and #1085 (y28; a value of views handed back through an if/match arm, which load’s resolving will likely write too). If a view-checker refusal stops you, narrow it in two probes (the shape as return x and as let r = x; return r) and report it as Found:; do not rewrite the code around it.

To build (architect’s decisions, 2026-10-31; names yours where not given)

  1. pub fn load(input: ref Bytes; limits: Limits = default_limits()) -> Vec[Node] sync throws: documents (one parse), then each root resolved, one Node per document. The strings stay views of the input (D4, D6): a copied node is its structure copied (.clone() of a Node whose Strings are views is a copy of the views, not of text). The result has no Value::Alias left; anchors and tags stay on the nodes as written.
    • Aliases: an alias is replaced by the resolved node of the most recent anchor of that name before it in the same document (anchors do not cross documents). An alias to an anchor not seen before it is an Error (unknown anchor *name, position of the alias). An alias inside the node its own anchor names (&a [ *a ]) is an Error (alias *a inside its own anchor), as libyaml-based loaders refuse a recursive alias; an earlier, finished &a does not make it legal.
    • Alias limit (D8): per document, the nodes the expansions produce (each alias counts the node count of the resolved node it copies, its own expansions included) may not exceed 10 * <the document's own nodes> + limits.alias_nodes. Keep each anchor’s resolved node together with its node count, and check before copying, so a billion laughs stops at the first alias over the limit without building it. The error names the limit and its value and the alias’s position (alias expansion exceeds <n> nodes (alias_nodes limit <alias_nodes>), wording yours).
    • Duplicate keys (D6): a key given twice in one mapping (after alias expansion) is an Error at the second key’s position, whose message also names the first’s line and column (duplicate key at L:C, first at L:C). Keys compare by kind and value: Null = Null; Bool by its bool; Str by text() (so a and "a" are one key); Int by as_i64 (so 16 and 0x10 are one key; an Int that does not fit compares by its text); Float by as_f64 (a NaN equals no key); Seq/Map structurally, element by element and entry by entry in order. Different kinds never match (1 and "1" are two keys).
    • << is an ordinary key: nothing is merged (D6). A merge function on request is the next brief, not this one.
    • parse and documents stay as they are (the text as written). Commit.
  2. Tests: packages/yaml/tests/load.nika + .stdout: an anchor used twice (both copies printed); an alias inside a nested collection; anchors are per document (two documents, the second’s *a unknown); unknown anchor; recursive alias; a billion laughs of 9 levels with 9 aliases each stops with the limit error, and the same with a raised Limits { alias_nodes: .. } small enough to build passes; a Limits lowered so an ordinary anchor fails; duplicate keys: a/"a", 16/0x10, [1, 2] as a key twice, and 1/"1" accepted; <<: *base kept as a key << with the copied mapping as its value. Write the test before the first package run and fill .stdout with --bless in that run (one run is 3-4 minutes); check every line by hand.

If a form the package needs is missing or refused in the compiler, the lowering or std (no ADR refusing it), stop: Result: blocked, Found: the gap with a probe of a few lines, Needs: architect decision. Do not write around it (a hand-written deep copy instead of .clone(), a map from anchor to index because a Map[String, Node] of views is refused, are both ways around: report them).

Run 2 (architect, 2026-10-31): take the draft, after y29

Run 1 built both steps on local branch wip/y27-load (eef3a2e7: main.nika load and helpers, tests/load.nika, tests/load.stdout blessed and checked line by line in a scratch copy) and stopped on #1089: let a = match m.get(k) { null => throw .. x => x } then match a.node { .. } in Loader::expansion lowers without & (rustc E0507, shown as NK1127 on every limits = default_limits()). y29-1089-match-on-a-get-bound-local.md runs just before this brief and fixes it.

Pre-flight instead of the one above: git log --oneline -5 shows y29’s commit (Fixes #1089); rebuild target/fast/nikaia. Then git checkout eef3a2e7 -- packages/yaml/src/main.nika packages/yaml/tests/load.nika packages/yaml/tests/load.stdout (only these three; main.nika of eef3a2e7 is y21’s file plus load, check with git diff HEAD -- packages/yaml/src/main.nika that nothing else differs), target/fast/nikaia check packages/yaml/src/main.nika clean, then the package gate once without --bless: load must pass with the blessed .stdout as it is. Do not rewrite expansion’s match a.node (run 1’s scratch spelling a.node?.clone() is the way around #1089). If the build still fails there, Result: blocked with rustc’s error. Commit the three files (and nikaia.contracts/nikaia.derived if they changed), Part: #928. No --bless unless check is clean (docs/builder-facts.md, Traps already met).

Rules (CLAUDE.md; this block is pasted into every brief, also a fix-forward)

Report (at most 15 lines, to exactly /home/user/Nikaia/docs/briefs/yaml/y27-load.md.report.md)

Fields of docs/architect.md, Briefs and reports: Result: done|noop|partial|blocked (the bare word alone on that line: Result: done, never a summary after it, which cuts the batch, b869a 2026-10-27; Owner: is a question for the owner or none, never a role), Files, Commit, Body (optional, one paragraph for the commit message), Fixes, Pushed, Tests, Gate (green|red), Deviation, Found, Needs, Changelog, Owner, Disk freed, and for a move Rs2nika: (the kept N of M lines of rs2nika --score, then the three costliest hand edits; none for a task that moves no Rust, none (redesign) for a brief with Shape: redesign and no Convert: line). none where nothing, and then the bare word: Deviation: none (a note) cuts the batch (batch.nika compares exactly); a note goes into the commit message. Found: is only for a defect outside your change (it cuts the batch); a process event such as another session resetting or rewriting your files is no Found:, it goes under Needs: (m3a 2026-10-28); a converter draft that came out wrong is no Found:, it is one of the costliest edits under Rs2nika:, which the converter lane reads (e12-4b 2026-10-27); how you solved your own step goes into the commit message. Deviation: is only a step of the brief built otherwise than written or not built (a choice the brief left to you, such as keep what X needs and say so, is no deviation: it goes into the commit message; nor is building two steps in one commit when every step is built, e12-3a 2026-10-27, nor a step another commit on develop already built, shown by your test failing with that commit’s change reverted, b889 2026-10-27: name the commit in Body:, nor a test’s expected .stdout/.stderr taken from what the program really prints where the brief’s sketch of it was short or wrong, b934 2026-10-29: say so in Body:, nor a lowering that follows the ADR governing it where the brief’s expected lowering contradicts that ADR, b933 2026-10-29: name the ADR in Body:, nor a lowering other than the brief’s where rustc cannot take the brief’s, b1029 2026-10-31: say why in Body:, nor re-keying an allow-list entry (KNOWN) whose hash changed for the same program and the same reason, b-fixforward-sweep2 2026-10-28: say so in Body:, nor an existing test’s expected .stdout/.stderr updated because your change rightly changes the message it prints, b1012b 2026-10-31: name the test in Body:); how you verified (e.g. a test not run against the unfixed compiler) goes under Tests:. A noop says why under Tests: (what was probed), not under Deviation:. Final answer, one line: done <sha>, report <path> (or BLOCKED: ... / PUSH REJECTED ...).

Attribution: Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com Claude-Session: https://claude.ai/code/session_01UVxJDveRM5qKZKyCqq6Y24

This site is open source. Improve this page.