Repository navigation
Releases
0.21.0 — issue #72: the per-group bracket entries (a bracketed diagram entry resolves even when other bracketed groups share its line)
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)
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)
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)
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.tsxnever 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_REbuilds its extension alternation from the seam, so a code pointer may name every artifact file the anchor machinery can address. The.mdtier's captures stay byte-identical (extraction only ever widens). - The second resolution tier — a non-
.mdtarget the corpus keys don't know resolves against the workspace filesystem (resolveSpecFilecontainment, issue #10; bothcans/…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 ridescheckArtifactAnchor— the ONE shared verdict function the corpus refs engine uses for its on-disk artifact refs (#58/#49/#60), riding the #68 cleaned-vs-cleanedanchorMatchesseam — 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 acodebase.anchor.no-targetwarning, 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' --codebasenow answers, matching by resolved path with the same anchor-insensitive comparison; refusals name both forms.- Kelir-shaped fixture extension (the
host-runtime.txttext-heading artifact,src/plugins/artifact-wiring.rswith 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)
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.tsas the single shared implementation (stripAnchorTrailingPunct);stripCodebaseAnchorPunctdelegates (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 anchorcans slugreturns 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)
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, andcans slugagrees.#UID/GID-mapping-isolationwas read as#UID,#Dynamic-W^X-JIT-execution-mediationas#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 refsquery grammar re-mirrors the extractor.cans refs "09-plugins.md#Invariants-and-policy-(transaction)" --codebaseanswers, and a spelling pasted from prose with a glued closer resolves too — theQUERY_REmirror 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
#Qutastypo stays broken on purpose. rawstill echoes the written span verbatim; only the resolvedtargetSlugis cleaned.- 47 new tests (27 engine + 20 contract/e2e over the kelir-shaped
codebase-workspacefixture extension); full suite green on Bun (1605 pass / 0 fail / 10 skip) andtsc --noEmitclean.
Full Changelog: v0.15.0...v0.16.0
v0.15.0 — Codebase Inbound References (issue #66)
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 embeddedsee: <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: warningdowngrades to the exit-1 band).--jsoncarries 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/excludeglobs,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,checkoutput 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 --noEmitclean.
Full Changelog: v0.14.0...v0.15.0
v0.14.0 — issue #65: aliasing crosses the corpus boundary
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/_adraux 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,
#leafanchor match per #58, external roots, containment, duplicate-home, orphan_check); a broken alias definition fails the run, not just the uses; unused = warning; belowmin_uses= error;styleenforced; - 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|collapseoperate on plugin files under the same rules — lossless, idempotent, graph-identical including#leaftargets;- 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 acans/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;includeis a PARAMETER (likescope/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 --noEmitclean); 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.mdandcans 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)
Issues closed
- #63 — RFC: Configurable Agent Guardrails in
_rules.yamland AGENTS.md (Workspace-Only) - #64 — TOML schematic parser false parse-error on valid
\nescapes 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 (devlogtouched_filesclaims, atomic-write residue); devlog existence + required Markdown sections; the AGENTS.md negative-invariants anchor with non-empty content. Findings ride thestructurecategory with machine-readableguardrails.*rule keys (§18 vocabulary table);severity: warndowngrades to warnings. - AGENTS.md standardization (§21/§33): the init template now ships
## Agent Execution Policy(prefixed targets, copy-modify-swap on protected manifests, append-onlylog/devlog.mdhandoff withtouched_files/test_outcomes/still_open) and## Negative Invariants— the default anchor.templates/_rules.yamlships 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 +
--jsonoutput), 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 --noEmitclean;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 onfeat/agent-guardrailsbefore landing onmain.
v0.12.0 — references.forbidden_targets: per-source layering bans on pointer targets
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_targetsis a list of{ source, pattern, severity, message?, except? }:sourceis a glob selecting the files the ban applies to (matched against the referrer's workspace-relative path with thecans/prefix stripped),patternis a regex selecting forbidden target shapes (SEARCH semantics, case-sensitive — anchor with^/$yourself),severityiserror(the default) orwarning,messagereplaces the default report text VERBATIM, andexceptexempts 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.mdand./plugins/x.mdare ONE target, and./..segments fold so acans/../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.brokencounts only the broken one); severity follows thenode_length/multilineconvention (errorfails the run,warningdoes not); one finding per matching rule, rule keyrefs.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, missingsource/pattern, invalid regexes and invalid severities are line-numbered config errors, never a silent pass. _rules.yamlgrows 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.mdpointer 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 —
--fixneither removes nor rewrites a forbidden ref: a layering violation is an author decision. The finding stays, thesee:line stays on disk. - Docs — §12 forbidden-targets subsection, §18 key + parser note, README user guide, and the
_rules.yamltemplate 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.