{"body":"Closes #18.\n\nSpec section this change targets: **SPEC.md §Tag placement enforcement** (new), with a cross-reference from §Tag placement review, a row in §Findings, and acceptance example 10.\n\n## The defect\n\n`check` verified coverage only — whether a requirement has evidence *somewhere*. Nothing read the lines around a tag, so a tag that had drifted off the evidence it names still scored its stage complete and `check` exited 0. Measured twice against v0.3.0, most recently on a **single**-tagged requirement where exit was 0 both before *and* after moving that requirement's only tag away from its test. With one tag and one displacement, nothing about the output changes: placement was not weakly enforced, it was never inspected.\n\n## The rule as implemented\n\nA tag is **attached** to the item it sits inside, or to the item that starts directly below it across only blank, comment, attribute, and decorator lines — the same attach-above convention `symbols.rs` already implements for `enclosingSymbol`. Anything else in between (a statement, an interposed `const`) is what the tag attaches to instead. The attached item must be **stage-eligible**:\n\n| Stage | Attaches to |\n|-------|-------------|\n| `doc` | any item, or documentation context (file comment header, Markdown document) |\n| `impl` | any declaration or statement |\n| `unit`, `int` | an item with a body — function, type, module, block, statement; **not** a bare value declaration |\n| declared custom stages | as `doc` — the spec assigns their evidence no shape, so their project decides |\n\nA tag in a file's leading comment header is a **module banner**: documentation context for the prose stages, a finding for `impl`/`unit`/`int` unless the manifest opts in. Violations emit `misplaced_tag` — one per tag, with `requirementId`, `stage`, `path`, `line`, `criterion = \"tag-placement\"`, and a message naming the item the tag landed on:\n\n```\n[must] misplaced_tag REQ-ONE criterion=tag-placement stage=unit src/lib.rs:1 — unit tag\nattaches to `CASES` at line 2, which is a value declaration; unit evidence must be a\nfunction, type, module, or block that can hold a test\n```\n\nSeverity follows the requirement's priority (so `--fail-on` applies), and like `undeclared_id` the finding is file hygiene that `--group`/`--ids` do not hide.\n\n### Rollout and escape hatch\n\n```toml\n[placement]\nenforce = \"on\"            # \"on\" (default) | \"off\"\nmodule_banner = \"reject\"  # \"reject\" (default) | \"accept\"\n```\n\nOn by default. `enforce = \"off\"` restores coverage-only checking; any other value for either key is a `manifest_error`.\n\n## Consumer impact, measured\n\nRun against an 8,024-tag consumer repository (spt-core), read-only:\n\n| | findings |\n|---|---|\n| module-banner (`// [impl->REQ-X]` under the file's `//!` docs, above the imports) | 269 |\n| `unit`/`int` tags in Markdown design documents | 8 |\n| unattached / ineligible-item false positives | **0** |\n| **total** | **277** |\n\nThe 269 are one deliberate convention that `module_banner = \"accept\"` admits in a single manifest line, leaving 8 — exactly the shape the rule exists to name. Reaching zero false positives took four corrections found by measuring rather than reasoning, each of which is now a rule of its own: `pub use` re-exports are evidence (a private `use` is not), scripts and call-shaped test frameworks (`test('...', ...)`, shell commands, CMake commands, PowerShell pipelines) anchor to statements because they declare nothing, custom stages are not held to a shape this spec never gave them, and a value declaration below a tag never steals the anchor from the item the tag is already inside.\n\nThis repository's own manifest stays at **0 findings** with all seven new requirements traced doc/impl/unit/int.\n\n## Deliberate silences\n\n- **Languages with no grammar** (TLA+, `[scan.extensions]`-mapped families) are skipped, as they already are for `enclosingSymbol`.\n- **Parses that recovered from errors** are skipped. A recovered parse drops constructs the grammar could not follow, and a dropped construct is indistinguishable from a tag attached to nothing. Measured on a 400-line PowerShell script whose grammar gaps produce 23 error nodes: judging it would have reported a correctly placed tag as misplaced.\n- **Test-ness is not proven.** `unit`/`int` require an item with a body, not a `#[test]`. Deciding test-ness is per-framework and would reject correct tags in every framework the tool has not been taught.\n- **Semantics stay with `review`.** Whether the function a tag sits on actually implements the requirement is unchanged and still agent-evaluated.\n\n## Implementation shape\n\nThe item table lives beside the change tier's symbol table in `symbols.rs` rather than in a second parser. Every declaration and script statement the walk sees is recorded with an `ItemKind`; only spans the change tier already called symbols carry `symbol: true`, and `enclosing()` filters on that flag. `enclosingSymbol` output is therefore unchanged — held by the existing symbol unit tests, by the `expected-change-compile.json` fixture, and by a new test asserting a `struct` is an anchor for placement and *not* a change-tier symbol.\n\n`placement.rs` owns the rule; `main.rs` composes it after the completeness join, since placement needs the source files back.\n\n## Tests\n\n`cargo test` — **257 passed, 0 failed** (217 before this change).\n\n- **23** unit tests in `placement.rs`: positive controls for all four stages, the interposed-`const` shape, banners with and without opt-in, dead space, Python/C++ equivalents, Markdown, and the policy plumbing.\n- **5** new unit tests in `symbols.rs`: item-kind classification, header detection, the recovered-parse skip, and the change-tier isolation test.\n- **14** integration tests in `tests/placement.rs` driving the binary: correctly placed tags for all four stages exit 0; the canonical interposed-`const` negative; a file-top banner far from any item; a **single-tagged requirement whose only tag is displaced** (asserting the stage still reports complete, so `misplaced_tag` is the only signal); a **multi-tagged requirement where one of four tags is displaced** (exactly one finding, naming that line); a code-stage tag in Markdown; the human diagnostic naming file:line, requirement, and stage; scoping; both config keys; and the unparsed-language skip.\n\nNegative controls run before finishing:\n\n1. Neutering `placement::check` to return no findings turned **8 of 13** integration tests red (the 5 that stayed green assert exit 0, as they should).\n2. Inserting a `const` into the four-stage positive-control fixture turned that test red.\n\nBoth were reverted; the suite is green.\n\n`cargo fmt --check` and `cargo clippy --all-targets -- -D warnings` are clean, matching CI. The fixture goldens are updated: `example/expected.json` gains the acceptance-10 finding, `expected-change-compile.json` gains the new tag entry (with **no** `enclosingSymbol`, which is the change-tier isolation showing up in the golden).\n\nFour pre-existing test fixtures needed real content — a `.md` file holding nothing but a tag, and `.rs` files holding nothing but tags. Those are true positives of the new rule, and their tests assert unrelated things, so each got a heading or a function rather than an exemption.\n\n**Version left unbumped** — the release cut is the reviewer's call. `misplaced_tag` is a new finding code: additive within `schemaVersion = 1`, no existing code renamed or changed in meaning, which is the `schemaVersion` discussion this repo's PR checklist asks for.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nhttps://claude.ai/code/session_01Qoox8YyetkbEBWsBdmB5uW\n","files":[{"path":"AGENTS.md","additions":2,"deletions":1,"changeType":"MODIFIED"},{"path":"CHANGELOG.md","additions":50,"deletions":0,"changeType":"MODIFIED"},{"path":"README.md","additions":2,"deletions":0,"changeType":"MODIFIED"},{"path":"SKILL.md","additions":1,"deletions":0,"changeType":"MODIFIED"},{"path":"SPEC.md","additions":50,"deletions":1,"changeType":"MODIFIED"},{"path":"example/expected-change-compile.json","additions":2,"deletions":1,"changeType":"MODIFIED"},{"path":"example/expected.json","additions":14,"deletions":2,"changeType":"MODIFIED"},{"path":"example/tests/auth_test.py","additions":11,"deletions":0,"changeType":"MODIFIED"},{"path":"rust/src/change.rs","additions":1,"deletions":0,"changeType":"MODIFIED"},{"path":"rust/src/lib.rs","additions":2,"deletions":0,"changeType":"MODIFIED"},{"path":"rust/src/lint.rs","additions":1,"deletions":0,"changeType":"MODIFIED"},{"path":"rust/src/main.rs","additions":13,"deletions":3,"changeType":"MODIFIED"},{"path":"rust/src/manifest.rs","additions":49,"deletions":0,"changeType":"MODIFIED"},{"path":"rust/src/output.rs","additions":12,"deletions":0,"changeType":"MODIFIED"},{"path":"rust/src/placement.rs","additions":794,"deletions":0,"changeType":"ADDED"},{"path":"rust/src/review.rs","additions":2,"deletions":0,"changeType":"MODIFIED"},{"path":"rust/src/symbols.rs","additions":590,"deletions":26,"changeType":"MODIFIED"},{"path":"rust/src/trace.rs","additions":21,"deletions":0,"changeType":"MODIFIED"},{"path":"rust/tests/fixture.rs","additions":8,"deletions":2,"changeType":"MODIFIED"},{"path":"rust/tests/issues.rs","additions":7,"deletions":2,"changeType":"MODIFIED"},{"path":"rust/tests/placement.rs","additions":497,"deletions":0,"changeType":"ADDED"},{"path":"rust/traceable-reqs.toml","additions":39,"deletions":0,"changeType":"MODIFIED"}],"mergeCommit":{"oid":"82d8b1424d929a36eba4b1edebdd2123fa2e6c54"},"title":"check: enforce tag placement adjacency and stage eligibility"}
