Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueComment |
Reviewer's GuideThis planning-only PR introduces a revision-2 ExecPlan for roadmap item 4.4.1, specifying how the command interpolation contract will be documented, tested through executable README examples and negative controls, propagated across documentation and translations, and recorded in ADR-021; no implementation changes are included. Flow diagram for planned command placeholder handlingflowchart TD
Recipe{Recipe kind}
Recipe -->|command| Command[Rewrite ins and outs]
Recipe -->|script| Script[Rewrite ins outs and legacy dollar aliases]
Command --> Guard[POSIX or Bash validation gates]
Script --> ScriptResult[Generate script text]
Guard -->|accepted| CommandResult[Generate command text]
Guard -->|rejected| Invalid[Invalid command interpolation]
CommandResult --> Quoted[Own path substitutions are shell-quoted]
ScriptResult --> Quoted
Flow diagram for planned documentation and verification milestonesflowchart LR
Decisions[ADR-021 decisions D1 D2 D3] --> Corrections[Correct internal documentation]
Corrections --> README[README contract and examples]
README --> Tests[Executable examples and negative controls]
README --> Translations[Six translated READMEs]
Tests --> Gates[Validation gates]
Translations --> Parity[README structural parity]
Gates --> Closure[Close roadmap item 4.4.1]
Parity --> Closure
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
@coderabbitai please create a new GitHub issue for the following:
|
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
repo="leynos/netsuke"
pr_number="699"
needle='$in and $out should not be emitted as ninja input and output markers'
comment_url="$(
{
gh api "repos/${repo}/issues/${pr_number}/comments" --paginate
gh api "repos/${repo}/pulls/${pr_number}/comments" --paginate
} | jq -r --arg needle "$needle" '
select((.body // "") | contains($needle)) | .html_url
' | head -n1
)"
if [ -z "$comment_url" ]; then
echo "Could not find the originating comment URL." >&2
exit 1
fi
body_file="$(mktemp)"
trap 'rm -f "$body_file"' EXIT
cat >"$body_file" <<EOF
## Summary
Prevent \`$in\` and \`$out\` from becoming Ninja input and output markers.
When these forms occur in \`script:\` recipes, escape them as shell variables. Preserve the distinction between Netsuke-owned markers, internal tokens, and shell variables.
## Rationale
The current documentation conflates three distinct concepts:
- **Marker**: a Netsuke-owned placeholder that Netsuke rewrites: \`{{ ins }}\`, \`{{ outs }}\`, and, in scripts only, \`$in\` and \`$out\`.
- **Internal token**: the \`INS_TOKEN\` or \`OUTS_TOKEN\` sentinel used only between interpolation stages.
- **Shell variable**: text such as \`$PATH\` or \`$ins\` that Netsuke intentionally leaves unchanged.
Do not emit shell-variable forms as Ninja markers.
## Affected areas
- Command interpolation for \`script:\` recipes.
- Ninja marker generation and escaping.
- Documentation that describes markers, internal tokens, and shell variables.
- Regression tests for script interpolation and generated Ninja output.
## Acceptance criteria
- \`$in\` and \`$out\` do not appear as Ninja input or output markers in generated output.
- In \`script:\` recipes, \`$in\` and \`$out\` are escaped as shell variables.
- \`{{ ins }}\` and \`{{ outs }}\` retain their documented Netsuke marker behaviour.
- Tests distinguish markers, internal tokens, and shell variables.
- Documentation uses the defined terms consistently.
Requested by `@leynos`.
Backlinks: [PR `#699`](https://github.com/leynos/netsuke/pull/699) · [originating comment](${comment_url})
EOF
gh issue create \
--repo "$repo" \
--title 'Escape `$in` and `$out` as shell variables in scripts' \
--body-file "$body_file" \
--assignee leynosLength of output: 2717 🏁 Script executed: #!/bin/bash
set -euo pipefail
repo="leynos/netsuke"
pr_number="699"
needle='$in and $out should not be emitted as ninja input and output markers'
comment_url="$(
{
gh api "repos/${repo}/issues/${pr_number}/comments" --paginate
gh api "repos/${repo}/pulls/${pr_number}/comments" --paginate
} | jq -r --arg needle "$needle" '
.[] | select((.body // "") | contains($needle)) | .html_url
' | head -n1
)"
if [ -z "$comment_url" ]; then
echo "Could not find the originating comment URL." >&2
exit 1
fi
body_file="$(mktemp)"
trap 'rm -f "$body_file"' EXIT
cat >"$body_file" <<'EOF'
## Summary
Prevent `$in` and `$out` from becoming Ninja input and output markers.
When these forms occur in `script:` recipes, escape them as shell variables. Preserve the distinction between Netsuke-owned markers, internal tokens, and shell variables.
## Rationale
The current documentation conflates three distinct concepts:
- **Marker**: a Netsuke-owned placeholder that Netsuke rewrites: `{{ ins }}`, `{{ outs }}`, and, in scripts only, `$in` and `$out`.
- **Internal token**: the `INS_TOKEN` or `OUTS_TOKEN` sentinel used only between interpolation stages.
- **Shell variable**: text such as `$PATH` or `$ins` that Netsuke intentionally leaves unchanged.
Do not emit shell-variable forms as Ninja markers.
## Affected areas
- Command interpolation for `script:` recipes.
- Ninja marker generation and escaping.
- Documentation that describes markers, internal tokens, and shell variables.
- Regression tests for script interpolation and generated Ninja output.
## Acceptance criteria
- `$in` and `$out` do not appear as Ninja input or output markers in generated output.
- In `script:` recipes, `$in` and `$out` are escaped as shell variables.
- `{{ ins }}` and `{{ outs }}` retain their documented Netsuke marker behaviour.
- Tests distinguish markers, internal tokens, and shell variables.
- Documentation uses the defined terms consistently.
Requested by `@leynos`.
Backlinks: [PR `#699`](https://github.com/leynos/netsuke/pull/699) · [originating comment](COMMENT_URL)
EOF
sed -i "s|COMMENT_URL|${comment_url}|g" "$body_file"
gh issue create \
--repo "$repo" \
--title 'Escape `$in` and `$out` as shell variables in scripts' \
--body-file "$body_file" \
--assignee leynosLength of output: 2654 🏁 Script executed: #!/bin/bash
set -euo pipefail
repo="leynos/netsuke"
pr_number="699"
echo "Issue comments:"
gh api "repos/${repo}/issues/${pr_number}/comments" --paginate \
--jq '.[] | [.html_url, .user.login, (.body | gsub("[\r\n]+"; " ") | .[0:180])] | `@tsv`' || true
echo "Review comments:"
gh api "repos/${repo}/pulls/${pr_number}/comments" --paginate \
--jq '.[] | [.html_url, .user.login, (.body | gsub("[\r\n]+"; " ") | .[0:180])] | `@tsv`' || true
echo "Reviews:"
gh api "repos/${repo}/pulls/${pr_number}/reviews" --paginate \
--jq '.[] | [.html_url, .user.login, (.body | gsub("[\r\n]+"; " ") | .[0:180])] | `@tsv`' || trueLength of output: 1946 🏁 Script executed: #!/bin/bash
set -euo pipefail
body_file="$(mktemp)"
trap 'rm -f "$body_file"' EXIT
cat >"$body_file" <<'EOF'
## Summary
Prevent `$in` and `$out` from becoming Ninja input and output markers.
When `$in` or `$out` occurs in a `script:` recipe, escape it as a shell variable. Keep the three concepts below distinct in implementation, tests, and documentation.
## Definitions
- **Marker**: A Netsuke-owned placeholder that Netsuke rewrites: `{{ ins }}`, `{{ outs }}`, and, in scripts only, `$in` and `$out`.
- **Internal token**: The `INS_TOKEN` or `OUTS_TOKEN` sentinel that exists only between interpolation stages 1 and 2.
- **Shell variable**: Text such as `$PATH` or `$ins` that Netsuke deliberately leaves unchanged.
## Rationale
The existing documentation conflates markers, internal tokens, and shell variables. This change prevents `$in` and `$out` from being emitted as Ninja markers while preserving the documented placeholder contract.
## Affected areas
- Command interpolation for `script:` recipes.
- Ninja marker generation and escaping.
- Documentation that defines markers, internal tokens, and shell variables.
- Regression tests for script interpolation and generated Ninja output.
## Acceptance criteria
- `$in` and `$out` do not appear as Ninja input or output markers in generated output.
- In `script:` recipes, `$in` and `$out` are escaped as shell variables.
- `{{ ins }}` and `{{ outs }}` retain their documented Netsuke marker behaviour.
- Tests distinguish markers, internal tokens, and shell variables.
- Documentation uses the defined terms consistently.
Requested by `@leynos`.
Backlinks: [PR `#699`](https://github.com/leynos/netsuke/pull/699) · [originating comment](https://github.com/leynos/netsuke/pull/699#issuecomment-5737451901)
EOF
gh issue create \
--repo leynos/netsuke \
--title 'Escape `$in` and `$out` as shell variables in scripts' \
--body-file "$body_file" \
--assignee leynosLength of output: 2125
✏️ Learnings added
You are interacting with an AI system. |
|
@coderabbitai please create a new GitHub issue for the following: roadmap 4.2.3 is marked complete but its execplan header says Assess the completion status of the execplan and update it to COMPLETE if all acceptance criteria are met. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '== Roadmap item 4.2.3 =='
sed -n '480,510p' docs/roadmap.md
echo '== ExecPlan outline =='
ast-grep outline docs/execplans/4-2-3-kani-harnesses-for-command-interpolation.md --items all || true
echo '== ExecPlan content =='
cat -n docs/execplans/4-2-3-kani-harnesses-for-command-interpolation.md
echo '== Matching Kani and command interpolation artefacts =='
fd -HI . . | rg '(^|/)(kani|cmd_interpolate|command_interpolation)' | head -n 200
echo '== Originating comment =='
gh api 'repos/leynos/netsuke/issues/699/comments?per_page=100' --paginate \
--jq '.[] | select(.user.login == "leynos" and (.body | contains("roadmap 4.2.3 is marked complete"))) | .html_url' | head -n 1Length of output: 128196 🏁 Script executed: #!/bin/bash
set -euo pipefail
repo="leynos/netsuke"
query='repo:leynos/netsuke is:issue is:open ("4.2.3" OR "command interpolation") ("IN PROGRESS" OR "Status")'
gh issue list \
--repo "$repo" \
--state open \
--search "$query" \
--json number,title,url,body \
--jq '.[] | {number, title, url, body}'Length of output: 453 🏁 Script executed: #!/bin/bash
set -euo pipefail
body_file="$(mktemp)"
trap 'rm -f "$body_file"' EXIT
cat >"$body_file" <<'EOF'
## Summary
Reconcile the completion state of roadmap item 4.2.3 with its ExecPlan status.
`docs/roadmap.md` marks item 4.2.3 and its sub-items complete. `docs/execplans/4-2-3-kani-harnesses-for-command-interpolation.md` currently states `Status: IN PROGRESS`.
## Assessment
Do not change the ExecPlan status to `COMPLETE` from the currently recorded evidence.
The ExecPlan acceptance criteria require a successful capped full `make kani-ir` run, passing deterministic and documentation gates, resolved trace links, and final reconciliation before the status changes to `COMPLETE`.
Revision 2.24 records that the documented `LD_LIBRARY_PATH`-capped full Kani run exited with code 124 before the full suite completed. Revision 2.26 records successful focused YAML, format, and lint validation, but retains `Status: IN PROGRESS`.
## Required work
1. Reassess each acceptance criterion in `docs/execplans/4-2-3-kani-harnesses-for-command-interpolation.md`.
2. Run or obtain valid current evidence for the outstanding full Kani-suite requirement.
3. Confirm the remaining required deterministic, documentation, mutation, trace-link, and review evidence.
4. If every acceptance criterion passes, change the ExecPlan header to `Status: COMPLETE`.
5. If any criterion remains unmet, retain `Status: IN PROGRESS` and correct `docs/roadmap.md` so that it does not claim completion without the required evidence.
6. Record the assessment and evidence in the ExecPlan.
## Affected files
- `docs/roadmap.md`
- `docs/execplans/4-2-3-kani-harnesses-for-command-interpolation.md`
- Kani validation and related verification artefacts, as required by the ExecPlan
## Acceptance criteria
- The issue documents the status of every ExecPlan acceptance criterion.
- The full Kani-suite requirement has current, reproducible evidence.
- The roadmap and ExecPlan report the same completion state.
- Set `Status: COMPLETE` only after all ExecPlan acceptance criteria pass.
Requested by `@leynos`.
Backlinks: [PR `#699`](https://github.com/leynos/netsuke/pull/699) · [originating comment](https://github.com/leynos/netsuke/pull/699#issuecomment-5737476040)
EOF
gh issue create \
--repo leynos/netsuke \
--title 'Reconcile roadmap 4.2.3 with ExecPlan completion evidence' \
--body-file "$body_file" \
--assignee leynosLength of output: 2609
The issue records that the ExecPlan must remain You are interacting with an AI system. |
Add the ExecPlan for roadmap item 4.4.1, which adds a "Security and command interpolation" section to the README and settles the three contract questions that docs/formal-verification-methods-in-netsuke.md leaves open. Ground truth was established by reading src/ir/cmd_interpolate/ rather than by trusting existing prose, and three findings shaped the plan: - `$in` and `$out` ARE substituted in `script:` recipes though not in `command:` recipes, contradicting docs/developers-guide.md and docs/formal-verification-methods-in-netsuke.md; - the backtick guard is a whole-string odd-parity count, not a quoting-aware model, and does not defend against author-written command substitution; - `shlex::split` gates POSIX and Bash `command:` recipes only, never `script:` recipes and never PowerShell. The plan therefore spans five milestones: a new ADR recording the three decisions, the README section with two executable examples, correction of the two inaccurate internal documents, the six translated READMEs, and the roadmap closure. Because tests/documentation_examples/ requires a `tested-example` marker on every README fence and compares identifiers for exact set equality, adding the section is a test change as well as a prose change. That gives the item a genuine red-green cycle, which the verification plan uses to make each documented claim falsifiable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fold a six-lens design review into revision 2 of the ExecPlan. The
review found four factual errors in revision 1 and two obligations
that should have existed and did not.
Blocking corrections:
- EP-M2 (document corrections) now precedes the README milestone and
covers docs/users-guide.md:1697,1713, which also contradict the
script-recipe placeholder behaviour. Revision 1 would have shipped a
README that contradicted the very page it links to.
- The negative controls now run after the README is committed and
restore by file copy. Revision 1 ran them first and restored with
`git checkout -- README.md`, which would have deleted the section it
had just written.
- OBL-PLACEHOLDERS' negative control was non-discriminating:
UndefinedBehavior::Strict makes an undefined `{{ inputs }}` fail
during rendering, before marker recognition is reached.
- Two obligations added. OBL-RECIPE-KIND covers the plan's own headline
finding, and OBL-QUOTING covers the section's only positive security
promise. Neither had any test in revision 1.
Factual corrections: assert_shell_command is in src/ninja_gen/mod.rs,
not src/ninja_gen_recipe_shell.rs; `shlex = "2.0.1"` is a caret
requirement, not a pin; the registry test is
every_documented_fence_has_a_known_unique_identifier; and
docs/netsuke-design.md already states the contract correctly and must
not be touched.
Rewording: D2 now promises the marker invariant and the backtick parity
check at different strengths, so a future quoting-aware fix is not a
breaking change. D3 names both shlex drift directions and states that
passing the gate does not make a command safe. D1 frames the
script-only $in/$out forms as retained legacy with a shadowing warning
and raises D1-LEGACY for the owner.
The README section order was inverted to put the live hazards before
the placeholder table, and the templated-value injection vector was
promoted out of a bullet list into its own labelled part. Scope
tolerance was raised to 18 files / 1200 lines, because the review
showed the original would fire on the plan's own expected path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The `spelling` gate rejects "hand-written". The plan already used "handwritten" in the other of its two occurrences, so this also makes the document internally consistent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Re-verify facts A, B, and C against the code after rebasing the branch onto origin/main at 0ba6672, and record the refreshed line citations in Progress so a resuming implementer does not open the wrong lines. The rebase moved the ADR ceiling from 020 to 026, so the plan's ADR becomes 027 and every reference is renumbered. PR 621 holds an unmerged and conflicted adr-021 filename, which would collide on merge. The plan header moves from DRAFT to IN PROGRESS now that implementation has begun.
Netsuke's command interpolation splits recipe text into a part Netsuke
rewrites and a part the shell owns. That split is a security boundary,
and the prose across the developers' guide, users' guide, and the
formal-verification document disagrees with the implementation on which
dollar-prefixed forms are placeholders.
ADR-027 settles three questions the formal-verification document records
as open, and states the answers at the strength the code supports:
- The supported placeholder set is `{{ ins }}`/`{{ outs }}` in both
recipe kinds, plus `$in`/`$out` in `script:` recipes only, with the
`script:`-only forms recorded as retained legacy behaviour.
- Backtick handling is two mechanisms at two strengths: a promised
marker-invariant, and a conservative whole-string parity check that is
explicitly not promised and may widen without a breaking change.
- The `shlex` guard is part of the observable acceptance contract (the
rejection is stable and localized) but is not a stability commitment
about the precise accepted set, because `Cargo.toml` carries a caret
requirement rather than a pin.
The record cites the implementing file and line for each decision, and
distinguishes what is proved (the marker recognizer, by Kani) from what
is property-tested (the backtick state machine and the guard).
Index the ADR from docs/contents.md and cross-reference it from the
netsuke-design.md section that describes command interpolation.
Co-Authored-By: Claude Code <noreply@anthropic.com>
2e5e9e2 to
3594b56
Compare
Note the milestone commit, the two gate results, and the confirmation that the typos-config-builder refresh inside `make markdownlint` left the working tree clean. Record why the force-with-lease push was safe: `git patch-id --stable` reports the same three patch-ids before and after the rebase, so the rewrite changed lineage only, not content. Note the out-of-band PR title and Lody session rename. Co-Authored-By: Claude Code <noreply@anthropic.com>
The milestone review returned twelve findings over the whole branch delta. Six were accepted and fixed; six were rejected with reasons. No behaviour changed. Accepted: - Two stale `locales/en-GB/messages.ftl:179` citations in the plan became `:188`, the current line. - The ADR-ceiling step in `Concrete steps` now records the historical `20`, the observed `26` after the rebase, and that step 4 is already complete, so a re-run cannot mint a second record. - `docs/netsuke-design.md` no longer states a guard as applying to both recipe kinds when `is_valid_command_for_shell` applies it to `command:` recipes on the POSIX and Bash routes only. - ADR-027 no longer describes the README section as already published. At EP-M1 the README has no such section; `Status`, `Rationale`, `Consequences`, and `Implementation references` now present it as established by this change set. - The artefact checklist cited off-by-one step numbers, and the acceptance inventory named five test cases and three obligations where the plan defines seven and five. Both now match the plan. - The second-person constructions at two locations, which `docs/documentation-style-guide.md:39` bars outside `README.md`. Rejected: the ADR date change, because both the system clock and GitHub's HTTP `Date` header report 2026-09-19, which is what the ADR says. Also record the CodeRabbit triage as `D-CODERABBIT-EP-M1`, and note in `Surprises & discoveries` that the module doc comment links to an item that does not exist, latent because `lint-clippy` runs `cargo doc` without `--document-private-items`. Co-Authored-By: Claude Code <noreply@anthropic.com>
Fact A — `{{ ins }}` and `{{ outs }}` work in both recipe kinds, bare
`$in` and `$out` are rewritten in `script:` recipes only, and `$ins`,
`$outs`, `$input`, `$output` are never rewritten — was already pinned by
the tests in `src/ir/cmd_interpolate/`, but three documents stated it as
if the placeholder set were the same in every recipe kind.
Correct `docs/developers-guide.md`, `docs/users-guide.md`, and
`docs/formal-verification-methods-in-netsuke.md` to state the set per
recipe kind, and record in the formal-verification document that the
three questions its contract section raised are now settled by ADR-027.
Correct the `[^8]` footnote path to `src/ir/cmd_interpolate/mod.rs`.
Correct the module doc comment on `src/ir/cmd_interpolate/mod.rs`, which
made the same claim. No code changes.
This milestone deliberately precedes the README section so the repository
is never in a state where two normative documents disagree.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Two findings were real and are fixed here; three were rejected. Real, and introduced by `efb5ea17`: the **Settled** paragraph in `docs/formal-verification-methods-in-netsuke.md` stated in the present tense that the contract is documented in the README, but the README section does not exist until EP-M3. The ADR `Status` line carried the same ambiguity and is reworded too. This commit exists to remove a document-claims-something-untrue window, so that it opened one is worth recording. Real, and pre-existing: three paragraphs read as though the unmatched-backtick parity check and the `shlex` guard applied to both recipe kinds. They apply to `command:` only — `is_valid_command_for_shell` is reached from `interpolate_command_with_bindings` alone, and `interpolate_script_with_bindings` routes to `substitute_script`, which never calls it. The marker-in-backticks invariant *is* shared, so the fix separates that invariant from the two follow-on checks. Rejected: the plan's literal README section label keeps its second person, which `docs/documentation-style-guide.md:39` permits in `README.md`; and the ADR date is 2026-09-19, re-verified. Co-Authored-By: Claude Code <noreply@anthropic.com>
The re-review of `24197456` found that ADR-027 said "PowerShell is outside both mechanisms", conflating the two halves of the promised invariant. PowerShell is outside the backtick half — its backtick is an escape, not a command-substitution delimiter — but it is inside the `$( … )` half: `power_shell_marker_protection` treats an active command substitution as protected and rejects the marker, the same outcome as POSIX by a different rule, pinned by `power_shell_rejects_markers_without_a_context_safe_encoder`. This is a second axis from the EP-M2 finding about recipe kinds: that one was `command:` versus `script:`, this one is POSIX versus PowerShell. The EP-M1 pass reviewed the ADR and missed it, because the pass was read against the recipe-kind axis the plan had been discussing. The same conflation appears twice in the plan's own `D2` narrative, so both were corrected with the ADR. The `D2` route-scope bullet now instructs EP-M3 that the README prose must not say PowerShell is exempt from the marker invariant outright. Co-Authored-By: Claude Code <noreply@anthropic.com>
Two fixes from the first review round on the new ADR. CodeRabbit asked for a bare `Accepted.` status and a date-only `Date`. That was right, and it caught a real error rather than a style preference: the style guide defines `Date` as the date the ADR was *created*, and this one was created today, not on 2026-09-17 when the decision was taken. Every other ADR in the repository uses the bare form, with the decision summary in its own section, which is where the "deferred, not closed" sentence has moved. The measurement dates stay in the context section, where they are read as evidence rather than as the record's identity. The second fix is a collision. Three open branches claimed ADR 027 -- PR #739's `adr-027-windows-reparse-point-same-handle-open.md`, PR #699's `adr-027-command-placeholder-contract.md`, and this one. Checking only `origin/main` for the ceiling was not enough, and the note that triggered the check recorded that #739 had already taken 027. Per the convention that the unmerged branch renumbers, this one moves to 028, which no branch or merged tree holds. The file moves with `git mv`; the heading, the `[adr-028-trim]` link definition, and all seven inbound references across the guide, `contents.md`, `nextest.toml` and the test doc comment move with it. `make check-fmt` and `make markdownlint` pass, and `.config/nextest.toml` remains structurally identical to `origin/main` under `tomllib`.
Two fixes from the first review round on the new ADR. CodeRabbit asked for a bare `Accepted.` status and a date-only `Date`. That was right, and it caught a real error rather than a style preference: the style guide defines `Date` as the date the ADR was *created*, and this one was created today, not on 2026-09-17 when the decision was taken. Every other ADR in the repository uses the bare form, with the decision summary in its own section, which is where the "deferred, not closed" sentence has moved. The measurement dates stay in the context section, where they are read as evidence rather than as the record's identity. The second fix is a collision. Three open branches claimed ADR 027 -- PR #739's `adr-027-windows-reparse-point-same-handle-open.md`, PR #699's `adr-027-command-placeholder-contract.md`, and this one. Checking only `origin/main` for the ceiling was not enough, and the note that triggered the check recorded that #739 had already taken 027. Per the convention that the unmerged branch renumbers, this one moves to 028, which no branch or merged tree holds. The file moves with `git mv`; the heading, the `[adr-028-trim]` link definition, and all seven inbound references across the guide, `contents.md`, `nextest.toml` and the test doc comment move with it. `make check-fmt` and `make markdownlint` pass, and `.config/nextest.toml` remains structurally identical to `origin/main` under `tomllib`.
The earlier renumber to 027 was itself a collision. PR #699 minted docs/adr-027-command-placeholder-contract.md at 2026-09-18T23:52Z, 31 minutes before this branch's ac3d3e9 landed its own 027. Renumbering off main's ceiling (026) missed it because an ADR number is spent as soon as any branch mints it, not when it merges. A sweep of every remote branch shows 028, 029, 030, and 031 are taken too — 029 by two branches independently: 026 merged on main 027 PR #699 028 issue-693-trim-the-split-build-dir-harness-test... 029 docs/hexagonal-hardening-and-checking and make-the-build-standard-the-default 030 docs/hexagonal-hardening-and-checking 031 docs/hexagonal-hardening-and-checking 032 free Take 032 and leave 027 to #699, which claimed it first. Update the four references in lockstep: the ADR's own H1, the docs/contents.md index entry, and the two inbound links in developers-guide.md and netsuke-design.md. No other file mentions the number.
Two fixes from the first review round on the new ADR. CodeRabbit asked for a bare `Accepted.` status and a date-only `Date`. That was right, and it caught a real error rather than a style preference: the style guide defines `Date` as the date the ADR was *created*, and this one was created today, not on 2026-09-17 when the decision was taken. Every other ADR in the repository uses the bare form, with the decision summary in its own section, which is where the "deferred, not closed" sentence has moved. The measurement dates stay in the context section, where they are read as evidence rather than as the record's identity. The second fix is a collision. Three open branches claimed ADR 027 -- PR #739's `adr-027-windows-reparse-point-same-handle-open.md`, PR #699's `adr-027-command-placeholder-contract.md`, and this one. Checking only `origin/main` for the ceiling was not enough, and the note that triggered the check recorded that #739 had already taken 027. Per the convention that the unmerged branch renumbers, this one moves to 028, which no branch or merged tree holds. The file moves with `git mv`; the heading, the `[adr-028-trim]` link definition, and all seven inbound references across the guide, `contents.md`, `nextest.toml` and the test doc comment move with it. `make check-fmt` and `make markdownlint` pass, and `.config/nextest.toml` remains structurally identical to `origin/main` under `tomllib`.
The earlier renumber to 027 was itself a collision. PR #699 minted docs/adr-027-command-placeholder-contract.md at 2026-09-18T23:52Z, 31 minutes before this branch's ac3d3e9 landed its own 027. Renumbering off main's ceiling (026) missed it because an ADR number is spent as soon as any branch mints it, not when it merges. A sweep of every remote branch shows 028, 029, 030, and 031 are taken too — 029 by two branches independently: 026 merged on main 027 PR #699 028 issue-693-trim-the-split-build-dir-harness-test... 029 docs/hexagonal-hardening-and-checking and make-the-build-standard-the-default 030 docs/hexagonal-hardening-and-checking 031 docs/hexagonal-hardening-and-checking 032 free Take 032 and leave 027 to #699, which claimed it first. Update the four references in lockstep: the ADR's own H1, the docs/contents.md index entry, and the two inbound links in developers-guide.md and netsuke-design.md. No other file mentions the number.
* Record the deferred split-build-dir harness trim (#693) The 156s figure that motivated trimming `harness_compiles_under_a_split_build_dir` came from the contended distribution, where the two isolated-Cargo tests each roughly halved the other. #687 moved the other one off the Windows lane, so this test got faster without being touched and the saving a trim could return fell with it. Measured across the three runs after #687, the test's exclusive tail is 62.3s, 87.2s and 85.5s, so trimming it is worth about 85s rather than 156s. The lane itself fell from a 1468s median to about 848s across #687, #690 and #691, which makes that 85s roughly ten percent of what remains. Record the decision not to trim it now, the ten-run revisit gate under which that decision is reconsidered, and the three alternatives already ruled out with their measured costs: `cargo check` (114s against 102s cold, and it writes nothing into the target directory so the uplift the regression exists to catch stops happening), warming the compiler cache (about three percent), and sharing a target directory (blocked by E0460 races with the `#[once]` fixture). Describe what a fixture-crate replacement would have to carry, so the design is on record while the trim is deferred: the fidelity argument naming the regression it still guards and the coverage it drops, and the Windows response-file pressure, which a one-dependency fixture would stop exercising unless it generates enough search paths or the contract moves to its own dedicated test. * Point the harness comments at the deferred trim (#693) Neither `.config/nextest.toml` nor the test's doc comment recorded that the trim measured at 156s is now worth about 85s, so both still read as though the larger figure were available. Add the three post-#687 runs (125.3s, 170.6s and 170.7s against the 274.7s contended median) and the exclusive tails that give the 85s figure, and point the "candidate for tightening or deletion" note at the ten-run revisit gate that "Deferring the split-build-dir harness trim" in docs/developers-guide.md defines. The test's doc comment gains the fidelity argument — that the subject is the real `test_support` build rather than a fixture crate, and that the developers' guide holds the criteria for revisiting that — and the response-file note, that the long `-L dependency=` set plus the long temporary roots is what keeps the Windows response-file path exercised, so any replacement must preserve that pressure or move it to a dedicated test. No executable logic and no timeout value changes. * Address CodeRabbit findings on the trim record (#693) Two findings, both on the same passage of the nextest comment, and one prose nit on the guide. The contention narrative was genuinely ambiguous: "on those same runs" tied the 125.3s, 170.6s and 170.7s values to the runs where the two isolated-Cargo tests contended, when they are the three runs *after* the second build left the Windows lane. Say which direction the change ran — contention had been increasing each test's elapsed time, and its removal produced the shorter durations — so the sentence cannot be read as claiming the values were measured under contention. The guide's cross-reference to the new fixture-crate subsection used an inline link too long for mdtablefix to wrap at 80 columns, which check-fmt rejects. Move it to the file's reference-link convention. The link label is `fixture-constraints` rather than the longer `fixture-crate-constraints`: mdtablefix treats `[text][label]` as one atomic token, and the longer label leaves no valid wrap point, so it wanted to strand the sentence's full stop on a line of its own. The visible text, the anchor target, and the convention are unchanged. Also stop quoting the guide's subsection title inline in the nextest comment; naming the section rather than reproducing its heading reads the same and does not invite drift. * Record the serialization group in the deferred-trim evidence (#693) Main's nested-cargo-builds test group serializes this test with the other build-capable child Cargo tests. It landed after the three runs the deferred trim was sized from, so those samples describe a shape that no longer exists. Under the group the test holds the single slot and everything behind it waits, so a trim returns its whole occupancy rather than its exclusive tail: 113s to 152s on the three Windows runs after the group landed, against the 85s the uncontended reading gave. The decision to defer still stands and the trim remains open at the ten-run gate, now against the serialized shape. Recorded in the developers' guide decision record, the nextest.toml budget comment, and the harness test's doc comment. * Count the group without a fixed number (#693) Main added a member to the nested-cargo-builds group while this branch was open, so the prose count of the other members was already stale by one. Name the membership instead of counting it, so a later addition to the group does not silently falsify the sentence. The group's contract test discovers members rather than listing only the pinned ones, which is why it stays correct through such an addition. * Correct the serialized-trim figures against the run logs (#693) Re-deriving every figure from the three Windows job logs showed three claims in the record were not what the logs support. The harness is not the last test to finish in any of the three runs; the prose said "two of the three". The group's cheap tail members trail it by under seven seconds in each case. The saving is not the group occupancy in every run. It is the run's end less whichever of the trimmed group chain and the last non-group test finishes later: 152.0s, 149.0s and 113.4s, where only the first two equal the occupancy and the third is capped because non-group work becomes the binding constraint once the harness is gone. The table's middle column is the group chain's end with the trim applied, not the run's end, so it is now named that way. All nine figures in the table reproduce from the logs; the changes are to what the prose claimed about them. * Record the fourth post-group sample for the harness trim (#693) The guide stated that trimming `harness_compiles_under_a_split_build_dir` returned 113s to 152s, from three Windows runs under the `nested-cargo-builds` group. This pull request's own Windows lane supplies a fourth: run 35400200137, the harness held the group's slot for 115.7s and a trim there returns 95.0s, below the low end of the published range. Add the row, widen the range to 95s to 152s, and correct the per-run analysis, which claimed the figure was the occupancy exactly in one run and short of it in one. It is short in the last two: 113s against 125s, and 95s against 116s, both because unrelated non-group work becomes the run's next binding constraint once the harness is gone. The four samples are also not uniform, so say so: the first three are trunk pushes and this one is a pull-request lane. The deferral decision is unchanged; only the evidence for it moves. Both files keep their existing shape, and `.config/nextest.toml` remains a comment-only change. Co-Authored-By: Claude Code <noreply@anthropic.com> * Anchor the post-group sample instead of counting it (#693) The post-group write-up counted its own samples: "the four runs", "the last two", "estimated from five". Every push to this branch starts another Windows run, so each count was stale by the time the next one landed, and the fifth run — 35403273264, occupancy 141.6s, trim returns 136.3s — left three of them wrong. Anchor the claims rather than re-counting them. The table gains the fifth run, the range stays 95s to 152s, and the prose now says across-the-sample and names the date the sample was taken. The "falls short of occupancy" case is stated as a pattern rather than as an ordinal claim about which runs did it, since it has now happened in three of the five. The caution paragraph notes the sample mixes trunk pushes with pull-request lanes and is a snapshot rather than a running total, so later runs belong to the revisit gate rather than to this table. The revisit gate now reads "the sample above" instead of a figure, and the nextest.toml range carries the same 2026-09-18 date. The deferral decision is unchanged. `.config/nextest.toml` remains a comment-only change. Co-Authored-By: Claude Code <noreply@anthropic.com> * Wrap the serialized-value link definition (#693) The definition line ran to 88 columns. markdownlint's MD013 does not flag it — it permits an over-length line when nothing follows the last space before the limit, and this line's only space sits at column 19 — but the repository already wraps long definitions this way, at `[github-actions-validation-test]` further up the same file. Put the fragment on an indented continuation line. The reference still resolves: every `][serialized-value]` use has a matching definition, and no definition is left unused. Co-Authored-By: Claude Code <noreply@anthropic.com> * Anchor the trim figure to its mechanism, not a running range (#693) The seventh Windows sample returned 162.9s, above the 152s upper bound the guide had published. The bound was the wrong shape: it read like a settled range when it was only a running maximum over a sample that keeps growing, since every push spawns a fresh Windows run. Replace it with the mechanism. A trim can never return more than the test's own duration, because the duration *is* the occupancy it gives back; and it returns exactly that whenever the shortened group chain is still what bounds the run. It returns less only when unrelated non-group work binds first. The figure therefore tracks the test's own cost rather than converging on the 85s exclusive tail, which belonged to an uncontended lane that no longer exists. The claim "four of the seven runs above" required the `35405043577` row, which the table was missing; added, and the count verified programmatically. Both the guide and the nextest comment are dated to the 2026-09-18 sample so later runs accrue to the revisit gate rather than falsifying a bound. * State the tail invariant structurally, not as a 0.1s margin (#693) The paragraph below the post-group table claimed the group's tail members trail the harness by "under seven seconds in every run". Re-derived across all seven rows, the worst gap is 6.9s against that 7.0s bound. The claim is true, but it is the same shape of hazard that already forced two rewrites here: a hard numeric bound over a sample that grows by one run on every push. The invariant underneath it does not decay, because it follows from the group's structure rather than from the measurement -- tail members cannot start until the harness frees the single slot, so they necessarily finish after it. State that, and drop the number. The figures the paragraph exists to support are unchanged; the seven-row table, its trim-returns column and the mechanism below it were re-verified against the raw job logs while checking this. * Separate the group's rationale from the coverage it owes (#693) CodeRabbit found the paragraph that follows the post-group table claiming the response-file pressure "makes the group entry necessary in the first place". That conflates two independent things, and the guide says the opposite two sections earlier: `nested-cargo-builds` exists because four nextest workers each starting a four-job child Cargo build on four vCPUs is what it prevents. Response-file pressure is a coverage requirement a replacement would owe, not a reason to serialize anything. A build-capable replacement does not change the contention rationale at all, so stating them as one requirement would misdirect exactly the fixture work this section exists to constrain. Split them, and keep the response-file clause attached to the coverage obligation where the constraints section already develops it. * Qualify the doc comment's occupancy claim the way the guide does (#693) The comment on `harness_compiles_under_a_split_build_dir` said a trim "would return its whole occupancy rather than only the tail it finishes on", unconditionally. The guide conditions exactly this figure: the trim returns the whole occupancy only when the shortened group chain still bounds the run, and returns less when unrelated work becomes the run's next binding constraint once the slot frees. That qualification is not decoration. It is the mechanism the guide was rewritten around, and the comment stated the stronger, unqualified form of it next to the measurements that contradict it. The two now agree. Raised as an unreproduced CodeRabbit finding on an aborted review pass. It was not corroborated on re-review, but it is correct on its merits, so it is fixed rather than dismissed on provenance. * Move the trim decision out of the guide and into ADR-027 (#693) The decision record was living in `docs/developers-guide.md` as two subsections. That is the wrong home for it. The style guide reserves the developer's guide for current responsibilities and points design rationale and trade-offs at decision records, and it asks the guide to stay synchronized with them. A deferral with a revisit gate is a decision with a status, not a description of how the suite works today. `docs/adr-027-defer-split-build-dir-harness-trim.md` now carries it: context, the options already measured and rejected, the rule that a trim can never return more than the test's own duration, the serialized-lane sample, the ten-run revisit gate, and the consequences. The guide keeps a short summary of the decision and links to the ADR; it still owns the constraints a fixture-crate replacement would have to preserve, because those are implementation requirements rather than decision rationale. The serialized-measurement table moved rather than being duplicated. Two copies of a seven-row sample that each push can date further is the drift this branch already corrected twice, so the ADR is the single owner and the guide points at it. `contents.md` gains the ADR-027 entry, and the three inbound references that named the old section -- in the guide's cross-reference block, in the Windows override comment in `.config/nextest.toml`, and in the doc comment on `harness_compiles_under_a_split_build_dir` -- now name the ADR. The nextest.toml change remains comment-only: verified structurally identical to `origin/main` under `tomllib`, and no non-comment line appears in its diff. Every figure in the ADR was re-derived programmatically from its own table: seven rows, four returning the full duration, the 115.7s to 162.9s duration span, the 95s to 163s saving span, the three short rows, and zero ceiling violations. Nine gates green on this tree: check-fmt, markdownlint, lint, typecheck, test (3196 passed, 5 skipped), doc-coverage (98.80%), and nixie. * Correct the ADR header and renumber it to 028 (#693) Two fixes from the first review round on the new ADR. CodeRabbit asked for a bare `Accepted.` status and a date-only `Date`. That was right, and it caught a real error rather than a style preference: the style guide defines `Date` as the date the ADR was *created*, and this one was created today, not on 2026-09-17 when the decision was taken. Every other ADR in the repository uses the bare form, with the decision summary in its own section, which is where the "deferred, not closed" sentence has moved. The measurement dates stay in the context section, where they are read as evidence rather than as the record's identity. The second fix is a collision. Three open branches claimed ADR 027 -- PR #739's `adr-027-windows-reparse-point-same-handle-open.md`, PR #699's `adr-027-command-placeholder-contract.md`, and this one. Checking only `origin/main` for the ceiling was not enough, and the note that triggered the check recorded that #739 had already taken 027. Per the convention that the unmerged branch renumbers, this one moves to 028, which no branch or merged tree holds. The file moves with `git mv`; the heading, the `[adr-028-trim]` link definition, and all seven inbound references across the guide, `contents.md`, `nextest.toml` and the test doc comment move with it. `make check-fmt` and `make markdownlint` pass, and `.config/nextest.toml` remains structurally identical to `origin/main` under `tomllib`. * Drop the full stop from the ADR's Date value (#693) The style guide gives the Date field the format `YYYY-MM-DD`, and the trailing full stop on `2026-09-19.` was mine rather than the convention's. The three most recent ADRs before this one -- 023, 024 and 026 -- carry the bare date; 022 and 025 carry the stop, so the repository is mixed and this is genuinely minor. The spec is unambiguous, so the bare form wins. `make check-fmt` and `make markdownlint` pass (143 files, 0 errors), with mdtablefix leaving all 142 files unchanged. The ADR's presence in markdownlint's scope was confirmed three ways: a count reconciliation against the config's ignore set, a glob check against its ignores, and a scoped probe linting the file alone. The change is a single character on one line. * Name the third Windows run behind the 170.7s figure (#693) CodeRabbit flagged an inconsistency: the guide said "the new shape has three samples" while its table carried only two run columns. The premise was wrong -- the third sample is real -- but the finding was right that the set was not traceable, and the fault was mine. My first commit replaced main's "on those same two runs ... 170.6s" with "125.3s, 170.6s and 170.7s on the three runs that followed" and never named the third run. The exclusive tails 62.3s, 87.2s and 85.5s came across from the measurement work, so the prose was consistent with itself; only the identifier was missing. The third run is 34080385050, recovered from the branch that produced the other two, `measure-windows-isolated-cargo-tests`. It is third in sequence after 34075197897 and 34079222917 and it reports the harness at 170.741s, which is the 170.7s the comment cites. It is now a table column rather than a bare number, with its own values measured the same way as the other two: 6.8s for the packaging test, a 253.8s nextest phase, and a 358s `Test` step. The extraction was validated before it was trusted. Run against the two runs whose values were already published, it reproduces the per-test durations to the decimal and the `Test` step to the second. It also caught its own first error: a naive phase measurement (`cargo nextest` invocation to summary) disagreed with both published values, because the published figure is nextest's own summary line, which excludes build time. The corrected reading matches to 0.1s on both, which is what licenses using it on the third. `nextest.toml` gains the three identifiers for the same reason, so the comment's 170.7s is traceable without the guide. The guide's lead-in sentence also said "the first two under the new shape" while enumerating two runs; it now names all three, which the scrutineer flagged as a stale clause after the first pass. `make check-fmt` leaves all 142 files unchanged and `make markdownlint` reports 0 errors across 143, so the hand-written table padding is canonical. `.config/nextest.toml` still parses under `tomllib`. --------- Co-authored-by: leynos <leynos@rohga> Co-authored-by: Claude Code <noreply@anthropic.com>
The earlier renumber to 027 was itself a collision. PR #699 minted docs/adr-027-command-placeholder-contract.md at 2026-09-18T23:52Z, 31 minutes before this branch's ac3d3e9 landed its own 027. Renumbering off main's ceiling (026) missed it because an ADR number is spent as soon as any branch mints it, not when it merges. A sweep of every remote branch shows 028, 029, 030, and 031 are taken too — 029 by two branches independently: 026 merged on main 027 PR #699 028 issue-693-trim-the-split-build-dir-harness-test... 029 docs/hexagonal-hardening-and-checking and make-the-build-standard-the-default 030 docs/hexagonal-hardening-and-checking 031 docs/hexagonal-hardening-and-checking 032 free Take 032 and leave 027 to #699, which claimed it first. Update the four references in lockstep: the ADR's own H1, the docs/contents.md index entry, and the two inbound links in developers-guide.md and netsuke-design.md. No other file mentions the number.
…int open (#703) (#739) * Harden the Windows final component through a same-handle reparse open Replace the pre-open symlink_metadata check in open_file_checked with an open that does not traverse a reparse point and a judgement taken from the handle it returns, so the policy decision and the read share one handle on Windows as they already do on Unix through O_NOFOLLOW. The new windows_reparse submodule sets FILE_FLAG_OPEN_REPARSE_POINT via cap_std's Windows-only OpenOptionsExt::custom_flags and refuses an opened handle carrying FILE_ATTRIBUTE_REPARSE_POINT. Testing the attribute bit rather than the tag also rejects junctions and volume mount points, which std does not report as symlinks. No unsafe, no new dependency, and no lint relaxation is required. Record the decision in ADR-026 and update the design and developers guides, which described reject_windows_symlink and its known race. Refs #703 * Correct the reparse-tag rationale in the design documents The prohibition was justified by a claim that junctions are not name surrogates. They are: `IO_REPARSE_TAG_MOUNT_POINT` is `0xA000_0003`, so bit 29 is set, and `std` reports a junction as a symlink. The same holds for a volume mount point. The claim mattered because it was doing load-bearing work in the argument for testing the attribute bit. That argument survives, but on the real gap: a tag that is *not* a name surrogate — a deduplication or cloud placeholder — is missed by `FileType::is_symlink` and, worse, is reported by `std` as `is_file() == true`, so the shared regular-file check would accept it. That is what makes `reject_reparse_point` load-bearing rather than redundant with the check beside it. No change in behaviour; the policy already tested the attribute bit. The prose now states the reason the code is written that way. Refs #703 * Test the reparse policy against real tag values and add a junction test The policy was only exercised through `open_flags` and the three ABI constants. Nothing asserted the property the policy actually turns on: that testing the attribute bit refuses the tags a name-surrogate test would miss. Extract `is_prohibited_reparse_point` as a pure predicate and pin it against real tag values — symlink and mount point for the surrogate case, NFS, deduplication and cloud for the non-surrogate case — cross-checking each fixture's own bit 29 so the table cannot drift from what it claims to test. A plain file's attributes must not be refused. Add a Windows integration test that creates a real directory junction with `mklink /J` and asserts all four filters refuse it. A junction needs no privilege, so unlike the symlink fixture there is nothing to skip on an ordinary host: the fixture reports `Ok(None)` only when `cmd.exe` is absent, and every other failure propagates rather than passing green over an unexercised policy. `require_real_junction` re-reads the reparse attribute without following the link, so a fixture that degraded to a plain directory cannot invert the assertions. The junction is refused by `reject_reparse_point` and, independently, by the shared regular-file check, since `std` reports a junction as a symlink. Driving both would need a non-surrogate reparse point, which no unprivileged fixture can create; the unit test above covers that gap instead. Refs #703 * Reflow the ADR-026 prose to the mdtablefix canonical form `make check-fmt` runs `mdtablefix --wrap` over every tracked Markdown file, and the hand-wrapped paragraphs in the three touched documents did not match its line breaking. Rewrapped in place with the same flags the Makefile passes. The reflow is cosmetic: a token-stream comparison of each file before and after shows the two sequences identical, so no wording changed. Refs #703 * Use sentence case for the ADR-026 section headings The documentation style guide requires sentence case for headings, and the ADR template spells the sections that way: "Decision drivers", "Decision outcome", "Known risks and limitations", "Architectural rationale". ADR-026 was the only ADR in the tree that used title case for all four. The date keeps its trailing period. The same template writes the field as `YYYY-MM-DD.`, and ADR-025 and ADR-022 follow it, so the formatting review suggestion to drop the period conflicts with the documented convention and is not applied. No behavioural change; headings only, and nothing links to these anchors. Refs #703 * Test the reparse policy on the handle and narrow a Windows-only import Add a handle-level test that only passes if FILE_FLAG_OPEN_REPARSE_POINT reaches the open. The integration test cannot detect that: std reports a junction as a symlink, so metadata.is_file() refuses the resolved directory regardless, and the policy would look correct while the flag went unapplied. Asserting the property on the opened handle's attributes is the only place the distinction shows, so a dropped flag now fails a test instead of silently widening the policy. The fixture is extracted into a helper, which also takes the test from 69 to 41 counted lines against the 70-line threshold: it had a single line of headroom on a lane only Windows CI compiles, so any small future edit would have broken that job with no local signal. Narrow the OpenOptionsExt import in fs_utils back to #[cfg(unix)]. Its only call site is inside #[cfg(unix)] apply_unix_open_flags; the Windows arm reaches custom_flags through windows_reparse, which has its own import. Commit 77c99e7 had widened it to any(unix, windows), which leaves the import live but unused on Windows and fails lint-clippy under -D warnings. Linux gates cannot see this, because the file's Windows code is cfg-gated. Both findings come from a local cross-compile probe: the main crate cannot be built for x86_64-pc-windows-msvc here because ring needs MSVC lib.exe, so the probe mirrors the module tree with file modules (Clippy resets its nesting counter at a file module and counts it through an inline one), copies windows_reparse byte for byte, and compiles the Windows-gated integration fixtures that the Linux gates never reach. Refs #703 * Renumber ADR-026 to ADR-027 after main claimed 026 main gained adr-026-manifest-environment-access-policy.md (PR #666) while this branch was in review, so the Windows reparse-point ADR moves to 027. Also drops the trailing full stop from the Date line, matching the two most recent ADRs on main (024, 026), which write the bare YYYY-MM-DD. * test(windows): skip the junction fixture when cmd.exe is absent The unit-test junction fixture spawned `cmd.exe` unconditionally and `expect`ed the spawn to succeed, so a host without `cmd.exe` failed the test instead of reporting that its subject was unavailable. That is both a false negative for a valid Windows host and an inconsistency with the sibling integration fixture, `fallible::junction_fixture` in tests/std_filter_tests/support.rs, which maps `ErrorKind::NotFound` to a skip. Map that one error kind to `None` from `junction_fixture`, and return early from the caller with a skip notice on captured test output. Every other spawn error and a non-zero `mklink` exit still assert: a junction needs no privilege, so a missing `cmd.exe` is the only unavailability that is a skip, and a real fault must not be masked as one. The skip is printed rather than silent so a green run cannot quietly hide the regression on a host that can build a junction; the `#[expect(clippy::print_stderr, ...)]` is fulfilled by that branch. * fix(windows): satisfy the Windows-only Whitaker lints and drop an impossible assertion The Windows CI lane failed three ways, all in the reparse-point module. This addresses each, and adds a local oracle that reproduces the lane in seconds. `module_must_have_inner_docs` wanted a `//!` line as the test module's first item. Adding one pushed the module past `module_max_lines`' 400-line limit, so the tests move to a `windows_reparse_tests.rs` sibling, reached through `#[path]`, as `status_tests.rs` already does. `no_expect_outside_tests` rejected five `expect` calls in `junction_fixture`. The lint exempts `#[test]` functions, but a plain helper is production code to it however it is gated, and `#[cfg(test)]` on the enclosing module does not register: rustc strips `cfg` attributes before the HIR the lint inspects. The fixture now returns `Result` and propagates, with `ensure!` rather than `assert!` because the repo denies `panic_in_result_fn`. The opt-in half of the junction test asserted something impossible. `mklink /J` records an absolute target, and `cap_std`'s resolver refuses to follow a reparse point whose destination leaves the capability, reporting `escape_attempt()` as `PermissionDenied`; a capability open cannot traverse a junction under either policy. That half is dropped, and the security-critical half — the default policy's handle *is* the reparse point — is kept and strengthened with an ordinary-directory control, so the refusal is shown to be about the link rather than about directories generally. The handle is taken ambiently because it must be. ADR-027 gains the capability-limitation risk and a Verification section that says which layer carries which property, since `std` reports a junction as a symlink and the integration test alone cannot see the flag. Verified with `cargo dylint --target x86_64-pc-windows-msvc` against the probe crate, which reproduces the Windows lint verdict exactly, plus clippy on the same target. A deliberate syntax error in the test file fails that run, so the oracle is genuinely compiling the Windows-gated code. * docs(tests): spell "recognizes" in the junction fixture comment The `-ize` form is the en-GB-oxendict preference this repository enforces elsewhere, and the surrounding prose already uses it. No behavioural change: the edit is inside a doc comment. Co-Authored-By: Claude Code <noreply@anthropic.com> * docs: describe the Windows reparse-point policy in the user-facing guides Three guides still described the pre-open Windows check that this branch removed, or stated the policy as symlink-only when it in fact refuses every reparse point. `users-guide.md` and `stdlib-yaml-and-jinja-guide.md` now say that the Windows refusal covers junctions, volume mount points, and other tags such as deduplication or cloud placeholders, and that `follow_symlinks=true` is the opt-in for all of them. `security-network-command-audit.md` replaces the "pre-open `symlink_metadata` check on Windows" remediation with `FILE_FLAG_OPEN_REPARSE_POINT` plus the handle's `FILE_ATTRIBUTE_REPARSE_POINT`, and records that the judgement and the read share one handle. Reported by chatgpt-codex-connector on PR #739. Co-Authored-By: Claude Code <noreply@anthropic.com> * Renumber ADR-027 to ADR-032 after three later claims The earlier renumber to 027 was itself a collision. PR #699 minted docs/adr-027-command-placeholder-contract.md at 2026-09-18T23:52Z, 31 minutes before this branch's ac3d3e9 landed its own 027. Renumbering off main's ceiling (026) missed it because an ADR number is spent as soon as any branch mints it, not when it merges. A sweep of every remote branch shows 028, 029, 030, and 031 are taken too — 029 by two branches independently: 026 merged on main 027 PR #699 028 issue-693-trim-the-split-build-dir-harness-test... 029 docs/hexagonal-hardening-and-checking and make-the-build-standard-the-default 030 docs/hexagonal-hardening-and-checking 031 docs/hexagonal-hardening-and-checking 032 free Take 032 and leave 027 to #699, which claimed it first. Update the four references in lockstep: the ADR's own H1, the docs/contents.md index entry, and the two inbound links in developers-guide.md and netsuke-design.md. No other file mentions the number. * Correct the follow_symlinks containment mechanism CodeRabbit raised one concern three times: that `follow_symlinks=true` does not override capability containment, so the docs must not imply it does. The premise is right, but the mechanism it named — "an absolute target that leaves the capability" — is wrong, and writing it down would have misdescribed what an operator can rely on. The resolver never compares the resolved path against the capability root. It refuses a link destination containing a prefix or root component outright, so it cannot tell an absolute target that stays inside from one that does not: both are refused. The trigger is the target's *absoluteness*, not its escaping. Measured rather than inferred. Two symlinks pointing at the same file inside one capability, one written relatively and one absolutely, under the opt-in policy: the relative link opened, the absolute link was refused with "a path led outside of the filesystem". Same file, same containment, different result — only the spelling differed. Documented at all four sites that state the policy, since a partial correction would leave the wrong mechanism standing somewhere: `users-guide.md`, `stdlib-yaml-and-jinja-guide.md`, ADR-032's known risks, and the Windows unit test whose doc comment explains why the junction handle is taken ambiently. The ADR records the superseded wording explicitly rather than quietly replacing it, so a reader who met the earlier draft can see which claim was wrong and why. Verified: the junction fixture's target is a relative `"file"`, so the opt-in integration test that ADR-032 cites as covering the follow path does in fact use the supported spelling. Refs #703. * Rewrap the users-guide paragraph mdtablefix's way `make check-fmt` failed on `docs/users-guide.md` in both the Linux `build-test` job and `Windows / lint-windows` for 2ffd72d: mdtablefix wanted +2 -2 on exactly one line. The line was 80 characters. Hand-wrapping prose to fit the 80-column budget is not the same as satisfying mdtablefix's own wrap, which chose a different break point. The previous commit hand-wrapped; this one runs the formatter over the file instead, which is the only way to get the same answer the gate does. Only the wrap point moved — no wording changed, and no other paragraph was touched. Verified with the gate's own invocation: mdtablefix --check --git --include-untracked --wrap --renumber \ --breaks --ellipsis --fences -> 142 files left unchanged, exit 0 Refs #703. * State the ADR's evidence limit plainly ADR-032's Verification section described two Windows-only tests as carrying the guarantee, which reads as though continuous integration had confirmed them. It has not: the Windows test lane halts on the unrelated pre-existing failure tracked as issue 743 before nextest reaches `stdlib::path`. Measured from the job log rather than assumed: the run ends at 1078/2901 tests, and the strings `windows_reparse` and `junction` appear zero times in the whole log. A decision record that overstates its own evidence is worse than one that names the gap, so the gap is now named — what is verified (compiles, lint-clean, tests compile, on a local `x86_64-pc-windows-msvc` probe) and what is not (that the tests pass on the platform they govern), with the two-tool reason the lint evidence needs both `cargo dylint` and `cargo clippy`. Docs-only; no behaviour change. Formatted with mdtablefix rather than by hand, so `mdtablefix --check` reports 142 files unchanged, exit 0. Refs #703. * docs(adr-032): credit native Windows CI for compiling the gated code The evidence section pointed readers at the local probe crate as the main route to Windows verification, which sold the CI evidence short. The `Windows / lint-windows` lane runs `cargo clippy --workspace --all-targets --all-features -- -D warnings`, and `--all-targets` compiles the library's `cfg(test)` module and the integration test targets. Since `windows_reparse_tests.rs` is a `#[cfg(test)]` child of the lib, every Windows-gated line — the module, its tests, and the junction fixture — is compiled on Windows itself under `-D warnings`, on the platform's own toolchain rather than an approximation of it. That job is green on this head. The probe crate is still worth recording, but as what it is: the local development loop, needed because the main crate cannot be cross-compiled here (`ring` needs MSVC's `lib.exe`), not the guarantee. The limit is narrowed rather than removed. Compiling a test under `-D warnings` does not run it, and the lane that would run it still stops 1800 tests short of `stdlib::path`. Co-Authored-By: Claude Code <noreply@anthropic.com> * style(adr-032): let mdtablefix choose the wrap point My previous commit hand-wrapped a paragraph in this file and `check-fmt` failed in `build-test`: `docs/adr-032-...md +2 -2`, `1 file would be reformatted`, `make: *** [Makefile:314: check-fmt] Error 1`. The lines were within 80 columns; the break position was wrong. That is the trap recorded in the formatter memory, and I walked into it a second time in this same file. Fixed by running the tool rather than the ruler: mdtablefix --in-place --wrap --renumber --breaks --ellipsis --fences \ docs/adr-032-windows-reparse-point-same-handle-open.md only the break between "-- all-features" and "Whitaker's" moved. Verified with the invocation the gate itself uses, `mdtablefix --check --git --include-untracked --wrap --renumber --breaks --ellipsis --fences` -> `142 files left unchanged`, exit 0. Markdown-only; no code or behaviour touched. Co-Authored-By: Claude Code <noreply@anthropic.com> * docs(adr-032): pin the test-lane figure to its commit `1078/2901 (1077 passed, 1 failed, 2 skipped)` is the one claim in this record most likely to age — it moves the moment issue 743 is fixed and a green Windows lane starts running these tests. Cite it against commit `2d8e5305` and name the test it died on, so a later reader can tell a stale number from a contradicted one. Re-measured on `2d8e5305` rather than carried over from the earlier run: same figures, and `windows_reparse`/`junction` still appear zero times in the job log. Verified with `mdtablefix --check --git --include-untracked --wrap --renumber --breaks --ellipsis --fences` -> `142 files left unchanged`, exit 0, and the file was formatted with the tool rather than by hand. Co-Authored-By: Claude Code <noreply@anthropic.com> * docs(stdlib guide): trim the reparse detail to a cross-link CodeRabbit finding 2 was the one finding in its batch with a valid basis: this guide sat at 397 lines on `main` and my earlier prose took it to 405. The other four findings cite a file-length rule that `AGENTS.md:31` scopes to "code file" and that only pylint enforces (`pyproject.toml:175`, `max-module-lines`), against documents already 2000-7300 lines on `main`; those are not this PR's to fix and are not defects. The cap claim is still worth honouring where this branch caused the crossing, so the added prose is trimmed from 12 inserted lines to 5. The dropped material enumerated reparse tags and restated the `follow_symlinks` containment behaviour, both of which `users-guide.md#configure-file-reading-limits` already carries in full — and this guide already cross-linked to that manual once, at line 387, so the pattern is established. Sibling guides updated alongside cover the security audit rather than the user-facing policy. Result: 400 lines, no longer over the threshold. Verified with `mdtablefix --check --git --include-untracked --wrap --renumber --breaks --ellipsis --fences` -> `142 files left unchanged`, exit 0; the file was formatted with the tool. Co-Authored-By: Claude Code <noreply@anthropic.com> * test(windows): assert the reparse policy at compile time The Windows-gated tests only run on a Windows host, and the Windows test lane has been known to stop short of `stdlib::path` (ADR-032 records the figure), so the policy's two load-bearing branches had no evidence that ran on every push. A `const` assertion does not depend on a test lane reaching the module: rustc evaluates it whenever the module is compiled, and `Windows / lint-windows` compiles every Windows-gated line through `cargo clippy --all-targets`, which is green on every head of this branch. Both `pub(super)` functions are now pinned in `const` contexts: `open_flags(false)` must set `FILE_FLAG_OPEN_REPARSE_POINT`, `open_flags(true)` must not, either policy must keep `FILE_FLAG_BACKUP_SEMANTICS` so a directory can still be opened and rejected by the shared regular-file check, and `is_prohibited_reparse_point` must accept an attribute value carrying `FILE_ATTRIBUTE_REPARSE_POINT` and refuse an ordinary one. No production behaviour changes, no dependency is added, no function's visibility changes, and the runtime tests are untouched — they still cover the handle behaviour and the diagnostics that a compile-time assertion cannot see. The oracle was shown live rather than assumed. Against a probe crate mirroring the module tree, the true code compiles clean (exit 0); dropping the flag from the default branch aborts the build with `E0080: evaluation panicked: the default policy must not traverse a reparse point` and exit 101; and a broken attribute test fails independently on its own assertion, so neither branch is merely riding on its neighbour's assertion. Co-Authored-By: Claude Code <noreply@anthropic.com> * docs: correct the opt-in wording and the ADR evidence record Two documents described the `follow_symlinks` opt-in in symlink-only terms while on Windows it governs the final component's reparse point as a whole, which is the finding CodeRabbit raised on `docs/stdlib-yaml-and-jinja-guide.md` line 178 and `docs/security-network-command-audit.md` line 153. The premise is sound even though the wording of the finding inverted the mechanism: neither file mentions a pre-open metadata check, and `users-guide.md` already carries the requested content in full. Fixing the narrower defect the premise points at: - guide: `permits the final component to be a symlink` -> `waives that final-component refusal`; net-zero on line count (400). - audit: the same correction, and the sentence is reflowed rather than patch-edited, so the clause order stays readable (163 lines). - ADR-032: `## Verification` now records three layers — the compile-time assertion added in `5d2dab68`, the unit test, and the integration test — and `### How far this evidence actually extends` is rewritten, because the section it replaces concluded that none of these tests had ever run in CI. That was true when it was written and issue 743 is now fixed on `main`. The rewritten section also corrects a claim of mine that was wrong, not merely stale: I had argued that zero occurrences of the fixtures' skip line proved the junction was built, because nextest captures a passing test's stderr. It does not. A probe crate shows the marker is absent from nextest output under the default profile, and the `success-output = "immediate"` entries in `.config/nextest.toml` cover three unrelated test groups with no `--success-output` in CI. The argument now rests on the fixtures' single narrow quiet arm (`ErrorKind::NotFound` from spawning `cmd`, every other outcome failing) and on `require_real_junction` checking the reparse attribute before use, with the unmeasured premise — that the runner image can spawn `cmd.exe` — stated as such. The record of the error is left in place rather than tidied away. Docs only; no code, test, or build configuration changes. Applied with `mdtablefix --in-place` and verified as a no-op against the hand-written wording, so the result is formatter-canonical rather than hand-wrapped. Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: leynos <leynos@rohga> Co-authored-by: Claude Code <noreply@anthropic.com>
Plans roadmap item 4.4.1, which adds a "Security and command interpolation" section to the README and settles the three contract questions that
docs/formal-verification-methods-in-netsuke.md§Command placeholder contract leaves open.No implementation yet — this PR is the plan, and it needs approval before anything is built.
ExecPlan:
docs/execplans/4-4-1-document-command-placeholder-contract-in-readme.mdWhat the ground truth turned out to be
The contract was established by reading
src/ir/cmd_interpolate/rather than bytrusting the existing prose, and three findings shaped the plan:
$inand$outare rewritten inscript:recipes, though not incommand:recipes (find_script_substitution,src/ir/cmd_interpolate/mod.rs:269-297). Three documents state the oppositewithout qualification.
(
has_unmatched_backticks,src/ir/cmd_interpolate/mod.rs:172-174), not aquoting-aware model. It does not defend against author-written command
substitution: a balanced pair reaches the shell and executes.
shlex::splitgates POSIX and Bashcommand:recipes only — neverscript:recipes, never PowerShell — and is never used to build an argv.The three decisions
{{ ins }}/{{ outs }}in both recipe kinds;$in/$outinscript:only, documented as retained legacy with a shadowing warningrather than as a blessed feature.
documented as a conservative check that a future release may widen without
that being a breaking change; author-written backticks carry no guarantee at
all.
shlexrejection is contractual; the precise accepted set isnot, because
Cargo.toml:138is a caret requirement rather than a pin. Bothdrift directions are named, and the README states that passing the gate does
not make a command safe.
Why this is not a documentation-only change
tests/documentation_examples/requires a<!-- tested-example: ... -->markeron every README fence and compares identifiers for exact set equality. Adding
the section is therefore a test change too, which gives the item a genuine
red-green cycle. Five verification obligations make each documented claim
falsifiable, with three negative controls including a seeded mutation from
docs/verification/mutations/.Scope
Five milestones: ADR-021 recording the decisions; correction of the three
inaccurate documents (deliberately before the README, so the repository is
never in a state where two normative documents disagree); the README section
with three executable examples; the six translated READMEs, preserving their
current section-for-section parity; and the roadmap closure.
Review
A six-lens community-of-experts design review ran against revision 1 and found
four factual errors in the plan plus two obligations that should have existed
and did not — notably that the plan's own headline finding had no test behind
it, and that the section's only positive security promise was unasserted.
Revision 2 folds all of it in; the
Revision noteandSurprises & discoveriessections record what changed and why.
One decision,
D1-LEGACY, is flagged as awaiting the owner's confirmation: theevidence is genuinely ambiguous about whether the
script:-only$in/$outforms are intended or vestigial. The plan is written so that either answer
leaves the documentation honest.
References
🤖 Generated with Claude Code
Summary by Sourcery
Establish and document the implemented command placeholder and shell-validation contract ahead of the planned README publication.
Bug Fixes:
$inand$outforms and route-specific validation behavior.Enhancements:
shlexacceptance contract in ADR-027 and link it from the relevant design and documentation indexes.shlexguard.Documentation:
Chores: