Skip to content

Releases

0.21.0 — issue #72: the per-group bracket entries (a bracketed diagram entry resolves even when other bracketed groups share its line)

Choose a tag to compare

@pylardotco pylardotco released this 09 Nov 05:30

Fixed in 0.21.0 — issue #72: cans check --codebase 0.20.0 reported 1 error on a clean kelir tree — a bracketed diagram entry SHARING its line with other bracketed groups didn't resolve: see: cans/artifacts/diagrams/wire-plane.txt#USERSPACE-SMOLTCP-INGRESS-RING over the target line [USERSPACE SMOLTCP INGRESS RING] [on_net_outbound] [on_net_inbound] fell to no node matches, because the #71 entry tier stripped one outermost […] pair only "when it is the only one" — a multi-group line minted the whole row as one entry, and the anchor naming the first group matched no node. It resolves now.

The per-group entry rule. Every cleaned line of a .txt/.text artifact target that is NOT a single embracing bracket pair mints each maximal flat bracketed group on the line — left-to-right, trimmed interior, the same ≥2-char and has-a-letter guards — as its OWN entry, beside the verbatim whole-line entry. So [USERSPACE SMOLTCP INGRESS RING] [on_net_outbound] [on_net_inbound] answers #USERSPACE-SMOLTCP-INGRESS-RING through the group entry USERSPACE SMOLTCP INGRESS RING, and the report's minimal repro [FOO BAR BAZ] [other one] answers #FOO-BAR-BAZ while [SOLO ENTRY HERE] keeps answering #SOLO-ENTRY-HERE exactly as since #71. Emission order is whole-line first, then the groups left-to-right, first-occurrence dedupe in document order.

Nothing narrows. A lone bracketed span keeps the #71 single-pair strip ([KERNEL VIRTIO NETQUEUE PAIR]); the whole-line entry keeps minting verbatim, so the whole-row spelling still resolves; single-token anchors (#SMOLTCP, #virtio-shm) keep resolving through the #70 leaf tier; a near-miss group typo still breaks, but the miss suggestion ranks the widened declaration∪entry∪leaf union, so it gets the did-you-mean at the entry spelling. .md corpus targets, schematic and code targets are untouched.

The contracts stand. #72 mints no new vocabulary type — the entry tier simply mints more entries — so the seven-vocabulary no-target advisory, the broken-anchor messages, the suggestion shapes, the exit ladder and the wire shape are byte-unchanged.

Verification. The 25-pin engine suite (the per-group grammar: nested, unbalanced, guard-failing and decorated shapes, emission order, dedupe, CRLF; the seam pins through checkArtifactAnchor — both repros, the did-you-mean at the entry spelling, the 0.8 floor from below, the whole-row spelling still resolving; the end-to-end pins), the twenty-pin acceptance suite over the extended kelir-shaped fixture (wire-plane.txt line 5 verbatim from the report, the new t.txt minimal repro, the net-refs.rs carrier with the repro pointer and the single-pair control; the 17 scanned / 56 pointers / 8 broken + 1 advisory census — the same eight records, the two repro pointers resolving; 17/56/10 on the bare 0.20.0 engine, the two false errors at fixture scale; the pure-defaults 15/54/9; the exit ladder 0 clean · 1 warnings-only with ok true · 2 errors; the frozen 7-key wire shape; the never-fuzzy reverse queries), the #66–#71 consumer re-pins over the shifted census, and the docs (README §Validation + §Codebase-references, architecture §12/§22/§23c + the extension table, templates/_rules.yaml). Full suite green on both runtimes — Bun 1810 pass / 10 skip / 0 fail, Node 1810 pass / 0 fail — and typecheck clean.

0.20.0 — issue #71: the whole-line entry tier (phrase anchors into .txt diagram targets resolve)

Choose a tag to compare

@pylardotco pylardotco released this 09 Nov 02:08

Fixed in 0.20.0 — issue #71: cans check --codebase 0.19.0 reported 2 errors on a clean kelir tree — multi-word hyphenated phrase anchors into .txt diagram targets (#Kernel-seccomp-unotify-triggers naming the whole line Kernel seccomp-unotify triggers, #USERSPACE-SMOLTCP-INGRESS-RING naming the box entry [USERSPACE SMOLTCP INGRESS RING]) fell to no node matches because the #70 leaf tier minted only single-word tokens. Both resolve now.

The entry tier. Every physical line of a .txt/.text artifact target is now an addressable entry: leading Unicode box-drawing/tree decoration (│├└─…) is stripped, one outermost ASCII […] bracket pair is stripped when it is the only one (so a box entry names itself without its brackets), and lines shorter than 2 characters or without a letter never mint. Resolution rides the same frozen checkArtifactAnchor seam as every other vocabulary, ranked between the #60 heading tier and the #70 leaf-token tier — the entry is the convention's named unit: "a pointer into a diagram means 'this entry of the diagram'", where the entry is the whole line. normalizeForAnchor already folds -↔space case-insensitively, so the hyphenated phrase form resolves with zero changes to matching itself.

Nothing narrows. Single-token anchors (#kelir-shim, #virtio-shm, #driver-avf, #SMOLTCP) keep resolving exactly as on 0.19.0 — the tier only adds. .md corpus targets, schematic (JSON/YAML/TOML) targets and code targets are untouched; a phrase typo still breaks, but the miss suggestion now ranks the widened declaration∪entry∪leaf union, so #Kernel-seccomp-unotify-trigger (singular) gets did you mean …#Kernel-seccomp-unotify-triggers? — fix the ref anchor.

The advisory stays truthful. The no-target downgrade now enumerates seven vocabularies — bullets, schematic key paths, text headings, text entries, code symbols, referenced names, text tokens — in both bands and the --fix healing parser, in lockstep. Empty and decoration-only targets keep the advisory; a contentful target whose anchor misses is still a truthful broken anchor error.

Verification. The 25-pin engine suite (the extractTextEntries grammar, the seam pins, both repros, the did-you-mean ranking, the narrowed advisory), the 15-pin acceptance suite over the extended kelir-shaped fixture (trap-flow.txt line 8 and wire-plane.txt line 5 verbatim from the report, the trap.rs/transport.rs carriers with the repro pointers, the phrase typo and the single-token control; the 16 scanned / 54 pointers / 8 broken + 1 advisory census, the exit ladder 0 clean · 1 warnings-only with ok true · 2 errors, the frozen 7-key wire shape, the never-fuzzy reverse queries), the #67–#70 consumer re-pins over the shifted census, and the docs (README §Validation + §Codebase-references, architecture §12/§20/§22/§23c, templates/_rules.yaml). Full suite green on both runtimes — Bun 1769 pass / 10 skip / 0 fail, Node 1769 pass / 0 fail — and typecheck clean.

0.19.0 — issue #70: the addressable-leaf widening (declarations AND leaves)

Choose a tag to compare

@pylardotco pylardotco released this 09 Nov 00:44

Fixed in 0.19.0 — issue #70: cans check --codebase 0.18.0 reported 16 errors + 19 warnings on a clean kelir tree. All three findings classes are resolved.

A — the missed declarations (7 false positives). The Rust symbol grammar widens to v2: column-0 declarations mint regardless of pub (the exported-only call is revised — example/demo artifact files have no API surface, and over-extraction is the module's documented fail-safe direction: a spurious name can only turn a would-be error into a hit, never mask a real one), and trait methods mint through a brace-depth trait-block tracker — pub trait ContainerService: Send + Sync { now makes the indented async fn configure_runtime(&mut self) a first-class symbol, exactly as the issue expected. pub(crate)/pub(super) and impl-block methods still never mint.

B — referenced names and diagram lines (9 errors + 19 warnings). New leaf vocabularies join the shared checkArtifactAnchor seam (the #68/#69 architecture — one verdict function): every identifier token of a code target (imports, uses, call sites, field accesses — #LedgerPlugin, #binary_blake3, #PgWireRewrite) and every word token of a .txt target (tree entries │ ├── kelir-shim/, box entries [virtio-shm], driver-rustvmm and siblings). This is the established convention the issue named: a pointer to an example means "this file demonstrates the symbol", a pointer into a diagram means "this entry of the diagram". Miss suggestions rank the declaration∪leaf union, so a typo of a referenced name (#LedgerPlugn) gets the same did you mean quality; the no-target advisory narrows to truly contentless targets (an empty .txt) and its message now names all six vocabularies. .md corpus targets and schematic (json/yaml/toml) targets are untouched.

C — the exit code, confirmed. Intended and now pinned and documented: the #41 ladder — 0 clean · 1 warnings-only (ok stays true; --strict flips ok, never the code) · 2 errors.

Proof: the kelir-shaped fixture extension reproduces all three classes pre-fix (22 broken + 1 warning on the extended tree); against the merged engine the census is 14 scanned / 50 pointers / exactly 7 broken (the 5 fixture-designed true records + the 2 deliberate typos) + 1 advisory warning — the 15 new valid pointers across every cited class (trait methods, unit struct, imported names, constructed names, field accesses, tree/box entries) all resolve end-to-end via the real CLI, typos still fail with did you mean proposals, and the exit ladder is pinned 0/1/2. 50 new pins (35 engine + 15 contract); full suite 1739 tests, 0 fail.

0.18.0 — issue #69: artifact targets for codebase pointers (extract-and-resolve, never skip)

Choose a tag to compare

@pylardotco pylardotco released this 08 Nov 23:27

Fixed in 0.18.0 — issue #69: cans check --codebase silently ignored see: pointers to non-.md targets (the extractor's file group captured .md only), and cans refs rejected those targets as malformed. In the reporter's repo, 114 of 1523 codebase pointers target non-.md artifact files (25 distinct: .rs, .ts, .py, .yaml, .txt, plus cans/_rules.yaml) — all unvalidated.

What changed:

  • The shared target-extension seam — CODEBASE_TARGET_EXTS + codebaseTargetExtAlt() (core/slug-adjacent, core/codebase.ts): the union of the extensions the artifact anchor machinery can address (.md corpus + #58 schematic .json/.yaml/.yml/.toml + #60 text .txt/.text + #60 code .ts/.tsx/.mts/.cts/.js/.jsx/.mjs/.cjs/.rs/.py/.go), longest-first so .tsx never captures as .ts. The table is also the grammar's over-capture guard: a file type no vocabulary can address (.c, .cpp, .csv, v1.2, e.g) is deliberately not a pointer target.
  • Extraction — SEE_POINTER_RE builds its extension alternation from the seam, so a code pointer may name every artifact file the anchor machinery can address. The .md tier's captures stay byte-identical (extraction only ever widens).
  • The second resolution tier — a non-.md target the corpus keys don't know resolves against the workspace filesystem (resolveSpecFile containment, issue #10; both cans/… and bare spellings). Missing file ⇒ FileNotFound (artifact target not found in the cans workspace); a file-level pointer verifies existence and nothing more; an anchored pointer rides checkArtifactAnchor — the ONE shared verdict function the corpus refs engine uses for its on-disk artifact refs (#58/#49/#60), riding the #68 cleaned-vs-cleaned anchorMatches seam — so schematic dot paths (incl. cans/_rules.yaml#codebase.enabled), YAML/TOML/JSON keys and ambiguity, text headings, code symbols and bullets all resolve identically from both engines. Verdicts map onto the frozen {FileNotFound, AnchorNotFound} union with the specific why riding message + did-you-mean suggestion; a nothing-addressable target downgrades to a codebase.anchor.no-target warning, never a fabricated finding; an unreadable target warns (codebase.io).
  • cans refs — the query grammar builds from the same table (pointer and query grammars cannot drift): cans refs 'cans/artifacts/code/plugin-trait.rs#PluginError' --codebase now answers, matching by resolved path with the same anchor-insensitive comparison; refusals name both forms.
  • Kelir-shaped fixture extension (the host-runtime.txt text-heading artifact, src/plugins/artifact-wiring.rs with three valid + three broken artifact pointers in the issue's own spellings), a 14-pin acceptance suite over the 13 scanned / 32 pointers / 5 broken census, the #66/#67/#68 consumer re-pins (the pure-default variants byte-pin a six-record oracle — a rules rewrite that drops the key a pointer names is a reported diff, never a rubber stamp), a 26-pin engine suite, and docs (README, architecture §12/§23c, templates/_rules.yaml).

Full suite: 1689 tests, 0 fail. The issue's exact repro: check --codebase --json reports 4 pointers / exactly the 2 broken anchors (AnchorNotFound); refs artifact queries answer with file:line matches.

0.17.0 — issue #68: symmetric anchor cleaning (cleaned-vs-cleaned)

Choose a tag to compare

@pylardotco pylardotco released this 08 Nov 22:31

Fixed in 0.17.0 — issue #68: cans check --codebase 0.16.0 minted 5 false anchor not found positives on kelir because the anchor cleaning pass ran ONE-SIDED: the captured anchor had its trailing sentence punctuation stripped (SQLite-UDS-bridge-mechanics: → no colon) while the heading side kept it — the slug algorithm preserves every non-separator character verbatim, so the canonical anchor cans slug prints (trailing : included) could never match.

What changed:

  • The cleaning pass moves to core/slug.ts as the single shared implementation (stripAnchorTrailingPunct); stripCodebaseAnchorPunct delegates (frozen #67 seam unchanged).
  • anchorMatches — the ONE matching seam shared by the corpus refs engine and the codebase engine — gains a cleaned-vs-cleaned clause after its untouched exact/normalized clauses: both sides get the same trailing-punctuation pass, so an anchor cans slug returns verbatim for its heading resolves. Strictly widening: everything that resolved still resolves; misspellings (#Qutas) still fail.
  • The corpus refs engine inherits the fix (a spec ref written with the canonical colon spelling resolves too).
  • Kelir-shaped fixture extension (the #65 aliasing tier, three colon-titled plugin specs, the five pointers at the kelir lines) + a 16-pin acceptance suite; the #66/#67 suites re-pin over the extended fixture; docs updated (architecture §12, README, templates/_rules.yaml).

Full suite: 1649 tests, 0 fail. The kelir five resolve end-to-end via the real CLI; only the 2 pre-existing true broken records remain (by fixture design).

v0.16.0 — Codebase Extractor + Matcher Repairs (issue #67)

Choose a tag to compare

@pylardotco pylardotco released this 08 Nov 07:10

Codebase Extractor + Matcher Repairs (issue #67)

cans check --codebase (0.15.0) reported 135 false anchor not found positives on a real workspace (kelir scaffolding @ 6bc53d3 — 748 embedded see: pointers, every one valid; plain cans check exits clean). None of the 135 was genuine rot. Two defects, both fixed:

Highlights

  • Bug 1 (127/135) — the extractor captures the full slug alphabet. The RFC #66 §2.1 sketch read anchors as [a-zA-Z0-9_-]+, truncating at the first (, ,, :, /, ^ — but the slug contract (_rules.yaml#slug.algorithm) preserves every non-separator character verbatim, and cans slug agrees. #UID/GID-mapping-isolation was read as #UID, #Dynamic-W^X-JIT-execution-mediation as #Dynamic-W. The anchor group is now the same [^\s#]+ shape the §11 corpus parser uses, followed by the #48/#58 cleaning pass carried to the codebase side: glued prose closers strip, opener-backed closers survive.
// see: cans/08-lifecycle.md#Native-checkpoint-plane-(kelir-core::checkpoint)   // ✅ resolves
// see: cans/02-plugin-model.md#FsInodeAttr-carries-full-mode,-uid,-gid,-size,-content  // ✅ resolves
  • Bug 2 (8/135) — : see:-suffixed nodes resolve by their bare title. Spec nodes routinely carry their own pointer as a suffix (- Connection parking on suspend: see: artifacts/examples/serverless-webhook.ts) while the code points at the bare leaf title — the matcher slugified the whole line and matched nothing. Resolution now tries the full node text AND the suffix-stripped title: the suffix is a reference, not part of the leaf title.
  • The cans refs query grammar re-mirrors the extractor. cans refs "09-plugins.md#Invariants-and-policy-(transaction)" --codebase answers, and a spelling pasted from prose with a glued closer resolves too — the QUERY_RE mirror contract (anything a source pointer can express is queryable) now runs in both directions.
  • Did you mean suggestions propose the suffix-stripped leaf title's canonical anchorSlug — never the suffixed line's unresolvable slug.

Compatibility

  • No wire changes, no config changes, no new flags. Resolution stays exact-after-normalization — never fuzzy (fuzzy remains suggestion-only), and genuinely broken anchors are still reported: the fixture's #Qutas typo stays broken on purpose.
  • raw still echoes the written span verbatim; only the resolved targetSlug is cleaned.
  • 47 new tests (27 engine + 20 contract/e2e over the kelir-shaped codebase-workspace fixture extension); full suite green on Bun (1605 pass / 0 fail / 10 skip) and tsc --noEmit clean.

Full Changelog: v0.15.0...v0.16.0

v0.15.0 — Codebase Inbound References (issue #66)

Choose a tag to compare

@pylardotco pylardotco released this 06 Nov 02:15

Codebase Inbound References (issue #66)

CANS now sees into your source trees. Code that annotates itself with see: pointers back to the spec —

// see: 02-auth.md#Session-validation (enforce 24h refresh window)

— can no longer rot silently, and every node answers the reverse question: which source files reference me?

Highlights

  • cans check --codebase — extracts embedded see: <file>[#<slug>] pointers from source files (Rust, TS/JS, Python, Go, C/C++, YAML comments — any text, no toolchains), resolves each against the spec graph with the same tolerant slug matching the corpus uses, and fails the run on broken pointers (severity: warning downgrades to the exit-1 band). --json carries the RFC §4.1 records verbatim: type: CodebaseBrokenReference, file, line, raw, targetFile, targetSlug, reason.
  • cans refs "<file>#<slug>" --codebase — the reverse-traceability query: every codebase occurrence referencing a spec node, with file:line and the line as written.
  • codebase: block in _rules.yaml — opt-in and generic: roots (resolved from the project root), include/exclude globs, severity. Absent = OFF; pre-0.15 workspaces check byte-identically.
  • Did you mean suggestions on broken anchors (suggestion-only — resolution never goes fuzzy).

Compatibility

  • Additive wire only: without --codebase / codebase.enabled, check output is byte-identical to 0.14.0.
  • 76 new tests (49 engine + 14 CLI + 13 e2e blackbox); full suite green on Bun (1568 pass) and the Node fallback (1558 pass); tsc --noEmit clean.

Full Changelog: v0.14.0...v0.15.0

v0.14.0 — issue #65: aliasing crosses the corpus boundary

Choose a tag to compare

@pylardotco pylardotco released this 05 Nov 20:49

Closes #65 — aliasing and leaf-anchor verification cross the corpus boundary.

What ships

references.aliasing.include (§18-validated, workspace-relative globs) opens the plugin tier: when aliasing is enabled, files matching the patterns — cans/plugins/*.md-shaped plugin files outside the spec corpus — join the checked file set:

  • parsed for refs and alias tables exactly like the _tasks/_adr aux tier, hardened: an unreadable/parse-broken plugin file is an explicit error (the author explicitly included it — never silently skipped);
  • refs join the ref graph: ref-by marks, hops, SCC, orphan relief, refs.total;
  • alias uses and definitions in plugin files resolve and verify exactly like corpus files' (file existence, #leaf anchor match per #58, external roots, containment, duplicate-home, orphan_check); a broken alias definition fails the run, not just the uses; unused = warning; below min_uses = error; style enforced;
  • definitions stay file metadata, never nodes: no structure/style/content/redundancy on plugin files, no target promotion, not --fix-rewritable in v1;
  • cans alias expand|collapse operate on plugin files under the same rules — lossless, idempotent, graph-identical including #leaf targets;
  • named-file arguments accept both spellings: workspace-relative (plugins/profile.md) and cwd-relative (cans/plugins/profile.md, the issue's own commands — the production layout keeps the workspace in a cans/ directory inside the project);
  • a named file that exists but is outside every loaded set refuses with the knob that opens it (references.aliasing.include);
  • backwards compatible: absent/empty include = corpus-only, byte-identical pre-#65 behavior; include is a PARAMETER (like scope/style/min_uses), never in the §18 delete/override reconciliation; a non-list shape, an empty member, or a glob that does not compile is a line-numbered config error.

Verification

  • 1492 tests pass / 0 fail under Bun, 1482 under the Node fallback (tsc --noEmit clean); 47 new issue-#65 pins across contract / engine / CLI / integration suites.
  • Real-world: the kelir workspace (cohesiveness) — the issue's concrete case — checks 30 files / 443 refs / 0 broken / 0 errors with the tier loaded; cans check cans/plugins/profile.md and cans alias collapse cans/plugins/profile.md --dry-run (the two commands the issue shows failing) both work; a full collapse → check round trip gives graph-identical checker output and a git-clean expand restore.

Credits

Implementation by five parallel agents on independent branches (contract → engine / cli / docs → integration), four-way merged and verified green before release.

v0.13.0 — agent guardrails (#63) + TOML newline-escape fix (#64)

Choose a tag to compare

@pylardotco pylardotco released this 05 Nov 13:34

Issues closed

  • #63 — RFC: Configurable Agent Guardrails in _rules.yaml and AGENTS.md (Workspace-Only)
  • #64 — TOML schematic parser false parse-error on valid \n escapes in basic strings

#63 — agent guardrails (workspace-only execution policies)

Opt-in top-level agent_guardrails block in _rules.yaml (§18): absent / empty / enabled: false leaves the workspace unsupervised — every pre-#63 config behaves byte-identically, nothing touches CI/CD.

  • Config contract (49b9feb): severity (error|warn), isolation (mode: require-target-prefix|off, prefix: target/agent-), manifests (atomic_writes_only, protected), devlog (enabled, path, required_sections), negative_invariants (required, anchor). Strict §18 validation: four block kinds with per-member line-numbered errors, workspace-relative path discipline (no leading /, no .., no \\), explicit-empty block = the documented OFF state, parameter-section semantics (never joins the delete/override reconciliation).
  • Engine (checkAgentGuardrails, §17 step 7): four checks over local workspace state only — prefixed build targets for active tasks plus shared-default build-dir contention; protected-manifest in-place-mutation evidence (devlog touched_files claims, atomic-write residue); devlog existence + required Markdown sections; the AGENTS.md negative-invariants anchor with non-empty content. Findings ride the structure category with machine-readable guardrails.* rule keys (§18 vocabulary table); severity: warn downgrades to warnings.
  • AGENTS.md standardization (§21/§33): the init template now ships ## Agent Execution Policy (prefixed targets, copy-modify-swap on protected manifests, append-only log/devlog.md handoff with touched_files / test_outcomes / still_open) and ## Negative Invariants — the default anchor. templates/_rules.yaml ships the block fully commented out (inert; init stays strictly valid).
  • Tests: 63 config-validation tests (every rejection path, defaults, OFF states, reconciliation neutrality, CLI blackbox), 68 engine/integration tests (incl. byte-identical pre-#63 human + --json output), 14 docs/template tests.

#64 — TOML parser false parse-error on \n escapes

tomlValue checked the DECODED string value for a newline — so any basic string containing a \n (or \u000A) escape poisoned every # anchor into that file with a false refs.anchor.parse-error. The guard now reads the raw scanned body (scanQuoted reports rawNewline), which a decoded escape can never trip — a genuine source newline is already covered by the existing unterminated basic string path. Valid escape-only TOML now parses and anchor-verifies end-to-end; genuinely malformed files still error at the pointer site (13 tests: the issue's mini repro, the full escape set, verdict parity with #58).

Verification

  • tsc --noEmit clean; bun test: 1415 pass / 10 skip / 0 fail (1425 ran, 87 files)
  • Node fallback on the five new suites: 158/158 green
  • Implemented by four parallel agents on fix/issue-64-toml-raw-newline, feat/issue-63-config-strict, feat/issue-63-engine, feat/issue-63-docs-agents-md; four-way merge verified on feat/agent-guardrails before landing on main.

v0.12.0 — references.forbidden_targets: per-source layering bans on pointer targets

Choose a tag to compare

@pylardotco pylardotco released this 02 Nov 02:03

One issue closed with a real implementation by four parallel agents, merged and verified together.

#62 — references.forbidden_targets: per-source bans on pointer targets (the layering / abstraction-leak guard)

A pointer that is valid but architecturally wrong — a core spec reaching into a plugin leaf, a public plane pointing at a private tree — is now a rule you can write down in _rules.yaml and cans check enforces. Before #62 the reference passed every check, and the violation was only caught by human review, if at all, regressing silently on the next edit.

  • The rule shape — references.forbidden_targets is a list of { source, pattern, severity, message?, except? }: source is a glob selecting the files the ban applies to (matched against the referrer's workspace-relative path with the cans/ prefix stripped), pattern is a regex selecting forbidden target shapes (SEARCH semantics, case-sensitive — anchor with ^/$ yourself), severity is error (the default) or warning, message replaces the default report text VERBATIM, and except exempts source files (the issue's kelir catalog/overview scoping need). Absent / empty / null list = feature OFF — every pre-#62 workspace checks byte-identically.
  • The checker contract (all seven non-negotiables, pinned end-to-end) — bans run AFTER alias expansion (#57: aliasing cannot dodge a ban; the default message appends , via alias "[1]"); matching runs on the file part, anchor stripped, path normalized — plugins/x.md, cans/plugins/x.md and ./plugins/x.md are ONE target, and ./.. segments fold so a cans/../plugins/ spelling cannot dodge the issue's own example rule; a forbidden target is reported even when it is also broken and vice versa (refs.broken counts only the broken one); severity follows the node_length/multiline convention (error fails the run, warning does not); one finding per matching rule, rule key refs.forbidden_target, included by --refs-only, suppressed by --structure[=strict]; banned refs keep full graph citizenship (refs.total, deep hops, orphans, duplicate-home unchanged — banning never hides consequences); strict #56 validation — unknown members, missing source/pattern, invalid regexes and invalid severities are line-numbered config errors, never a silent pass.
  • _rules.yaml grows block-form lists of objects — the parser support the issue's evidence section demanded (parseArray): lists of mappings parse with per-member line numbers, alongside the existing scalar and scalar-list forms.
  • External roots (#55) interplay — a kelir://plugins/leaf.md pointer is banned by either the written form (^kelir://plugins/) or the bare root-relative sub-path (^plugins/). Generated <!-- ref-by --> comments are not refs by construction, so a ban can never re-enter a workspace through its own back-pointers.
  • No auto-fix, by design — --fix neither removes nor rewrites a forbidden ref: a layering violation is an author decision. The finding stays, the see: line stays on disk.
  • Docs — §12 forbidden-targets subsection, §18 key + parser note, README user guide, and the _rules.yaml template documents the new key. The human report carries the finding's message verbatim — the #41 generic scrubber never touches it.

Verified: 1257 pass / 0 fail / 10 skip on Bun (82 files), 1257 / 0 on the Node fallback runtime, typecheck clean — plus an adversarial QA pass before the merge whose one real finding (a ../ spelling dodge of the ban normalizer) is fixed and pinned.