Skip to content

Document the command placeholder contract in the README (4.4.1) - #699

Draft
leynos wants to merge 10 commits into
mainfrom
4-4-1-document-command-placeholder-contract-in-readme
Draft

leynos wants to merge 10 commits into
mainfrom
4-4-1-document-command-placeholder-contract-in-readme

Conversation

@leynos

@leynos leynos commented Sep 9, 2026

Copy link
Copy Markdown
Owner

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.md

What the ground truth turned out to be

The contract was established by reading src/ir/cmd_interpolate/ rather than by
trusting the existing prose, and three findings shaped the plan:

  • $in and $out are rewritten in script: recipes, though not in
    command: recipes (find_script_substitution,
    src/ir/cmd_interpolate/mod.rs:269-297). Three documents state the opposite
    without qualification.
  • The backtick guard is a whole-string odd-parity count
    (has_unmatched_backticks, src/ir/cmd_interpolate/mod.rs:172-174), not a
    quoting-aware model. It does not defend against author-written command
    substitution: a balanced pair reaches the shell and executes.
  • shlex::split gates POSIX and Bash command: recipes only — never
    script: recipes, never PowerShell — and is never used to build an argv.

The three decisions

  • D1{{ ins }}/{{ outs }} in both recipe kinds; $in/$out in
    script: only, documented as retained legacy with a shadowing warning
    rather than as a blessed feature.
  • D2 — the marker invariant is promised; the backtick parity check is
    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.
  • D3 — the shlex rejection is contractual; the precise accepted set is
    not, because Cargo.toml:138 is a caret requirement rather than a pin. Both
    drift 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: ... --> marker
on 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 note and Surprises & discoveries
sections record what changed and why.

One decision, D1-LEGACY, is flagged as awaiting the owner's confirmation: the
evidence is genuinely ambiguous about whether the script:-only $in/$out
forms 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:

  • Correct inaccurate documentation about command placeholder interpolation, including the script-only legacy $in and $out forms and route-specific validation behavior.

Enhancements:

  • Record the command placeholder, backtick-handling, and shlex acceptance contract in ADR-027 and link it from the relevant design and documentation indexes.
  • Clarify the distinctions between guaranteed marker protection, conservative backtick parity checking, author-written shell command substitution, and the limited scope of the shlex guard.
  • Update user, developer, and formal-verification documentation to reflect the implemented interpolation contract and its limitations.
  • Add and document an execution plan for publishing the contract in the README and translated README editions.

Documentation:

  • Add ADR-027 defining the command placeholder contract and its compatibility implications.
  • Update the users' guide, developers' guide, formal-verification methods, and design documentation to align with the implementation.

Chores:

  • Correct the command interpolation module documentation and stale source reference.

@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This 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 handling

flowchart 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
Loading

Flow diagram for planned documentation and verification milestones

flowchart 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
Loading

File-Level Changes

Change Details Files
Adds a detailed execution plan for documenting and formalising the command placeholder contract without changing production behaviour.
  • Defines decisions for placeholder rewriting, backtick handling, and the shlex rejection gate.
  • Records implementation facts, constraints, risks, milestones, recovery procedures, and review history.
  • Requires an ADR, corrections to conflicting documentation, README content, translations, and roadmap closure.
docs/execplans/4-4-1-document-command-placeholder-contract-in-readme.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@codescene-access codescene-access Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No quality gates enabled for this code.

@leynos

leynos commented Sep 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai please create a new GitHub issue for the following:

$in and $out should not be emitted as ninja input and output markers. If present in scripts, they should be escaped as shell variables.

Term of art — "marker". In this plan, marker means a Netsuke-owned placeholder that Netsuke rewrites: {{ ins }}, {{ outs }}, and (in scripts only) $in and $out. Internal token means the INS_TOKEN /OUTS_TOKEN sentinel strings that exist only between stages 1 and 2. Shell variable means text such as $PATH or $ins that Netsuke deliberately leaves alone. These three are distinct and the existing documentation conflates them; not conflating them is a goal of this plan.

@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

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 leynos

Length 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 leynos

Length 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`' || true

Length 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 leynos

Length of output: 2125


@leynos Created issue #737 and assigned it to you. The issue links to PR #699 and the originating comment.


✏️ Learnings added
Learnt from: leynos
URL: https://github.com/leynos/netsuke/pull/699

Timestamp: 2026-09-18T23:34:39.858Z
Learning: In `leynos/netsuke`, distinguish these terms in command interpolation implementation, tests, and documentation: a marker is a Netsuke-owned placeholder that Netsuke rewrites (`{{ ins }}`, `{{ outs }}`, and in `script:` recipes `$in` and `$out`); an internal token is the `INS_TOKEN` or `OUTS_TOKEN` sentinel that exists only between interpolation stages; a shell variable is text such as `$PATH` or `$ins` that Netsuke deliberately leaves unchanged. Do not conflate these terms.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@leynos

leynos commented Sep 18, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai please create a new GitHub issue for the following:

roadmap 4.2.3 is marked complete but its execplan header says IN PROGRESS. docs/roadmap.md:490-504 checks all six sub-items; docs/execplans/4-2-3-kani-harnesses-for-command-interpolation.md:8 reads Status: IN PROGRESS.

Assess the completion status of the execplan and update it to COMPLETE if all acceptance criteria are met.

@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

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 1

Length 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 leynos

Length of output: 2609


@leynos: Created and assigned issue #738.

The issue records that the ExecPlan must remain IN PROGRESS until the full capped Kani-suite requirement and all other acceptance criteria have current evidence. It requires reconciliation of the roadmap and ExecPlan status.

You are interacting with an AI system.

leynos and others added 5 commits September 19, 2026 01:45
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>
@leynos
leynos force-pushed the 4-4-1-document-command-placeholder-contract-in-readme branch from 2e5e9e2 to 3594b56 Compare September 18, 2026 23:53
@leynos leynos changed the title Plan: Document the command placeholder contract in the README (4.4.1) Document the command placeholder contract in the README (4.4.1) Sep 18, 2026
codescene-access[bot]

This comment was marked as outdated.

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>
codescene-access[bot]

This comment was marked as outdated.

leynos and others added 2 commits September 19, 2026 02:38
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>
codescene-access[bot]

This comment was marked as outdated.

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>
codescene-access[bot]

This comment was marked as outdated.

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>
leynos pushed a commit that referenced this pull request Sep 19, 2026
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`.
leynos pushed a commit that referenced this pull request Sep 19, 2026
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`.
leynos pushed a commit that referenced this pull request Sep 19, 2026
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.
leynos pushed a commit that referenced this pull request Sep 20, 2026
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`.
leynos pushed a commit that referenced this pull request Sep 20, 2026
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.
leynos added a commit that referenced this pull request Sep 20, 2026
* 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>
leynos pushed a commit that referenced this pull request Sep 20, 2026
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.
leynos added a commit that referenced this pull request Sep 20, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant