From 81fabd7d785bcd662b0acaccc50b81e1bbc26786 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 06:58:10 +0000 Subject: [PATCH 1/6] =?UTF-8?q?feat(adr):=20accept=20ADR-0006=20=E2=80=94?= =?UTF-8?q?=20src/utils=20is=20a=20leaf?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first decision recovered by `kit adr derive` and then made binding. It replaces three of the nine proposed candidates with their general form: derive proposed `utils -> commands` (113 reverse edges), `utils -> adapters` (8) and `utils -> memory` (5) separately; measurement showed `utils` imports NOTHING from src/ at all, so one rule covers all three and any subsystem added later. Enforcing the general form matters because `utils` is the most fanned-in directory in the repo. A single upward import from it creates a cycle through most of the tree. Mutation-proved rather than eyeballed: green on the repo as it stands, and injecting `import "../adr.js"` into src/utils/colors.ts produces `✗ src/utils/colors.ts:1 ... (ADR-0006)` with exit 1, green again on revert. Two candidates deliberately NOT accepted, with the reason recorded in the ADR itself: - "No subsystem imports the command layer" is true of all 16 subsystems, but its general form fires on src/commands/adr-derive.test.ts, where a fixture STRING containing `import "../commands/x.js"` is read as a real import by the text-level extractor. `paths` has no negation, so "every subsystem except src/commands/**" cannot be expressed. Left underived rather than encoded wrong -- and worth noting that the tool's narrow per-directory candidates were right where the hand-written consolidation was not. - `profile` -> `exec-broker` (support 8) is a lone pair with no wider pattern. Insufficient evidence it is a decision rather than an ordering accident. Known papercut, recorded not fixed: `adr derive` still proposes the three candidates ADR-0006 now enforces. Suppressing them needs regex subsumption against accepted rules, which is not trivially decidable and not worth guessing at. --- AGENTS.md | 6 ++- CLAUDE.md | 6 ++- docs/adr/0006-utils-is-a-leaf.md | 77 ++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 docs/adr/0006-utils-is-a-leaf.md diff --git a/AGENTS.md b/AGENTS.md index 34caf471..a9ac54ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,12 +17,14 @@ This repo is managed by [kit](https://github.com/sandstream/kit) (env, secrets, ## Architecture decisions are a gate, and `kit check` does not run it -`docs/adr` holds five ADRs. The three accepted ones carry a `kit-enforce` block, which -makes them four deterministic rules — not prose: +`docs/adr` holds six ADRs. The four accepted ones carry a `kit-enforce` block, which +makes them five deterministic rules — not prose: - **ADR-0001** no model-client import anywhere in `src/**` (the zero-LLM core). - **ADR-0002** no new runtime dependency from the forbidden list — stdlib otherwise. - **ADR-0003** the check path imports no coverage-framework mappings. +- **ADR-0006** `src/utils/**` imports nothing from the repo — derived by `kit adr derive` + from an asymmetry the code had obeyed for months, then accepted. `node dist/cli.js adr check` runs them and **fails CI hard** on a violation. `kit check` does **not** include the ADR stage — only `kit review` (check + design + standards + adr + skill) diff --git a/CLAUDE.md b/CLAUDE.md index c26b8c72..f8f3a5cd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,12 +30,14 @@ This repo is managed by [kit](https://github.com/sandstream/kit) (env, secrets, ## Architecture decisions are a gate, and `kit check` does not run it -`docs/adr` holds five ADRs. The three accepted ones carry a `kit-enforce` block, which -makes them four deterministic rules — not prose: +`docs/adr` holds six ADRs. The four accepted ones carry a `kit-enforce` block, which +makes them five deterministic rules — not prose: - **ADR-0001** no model-client import anywhere in `src/**` (the zero-LLM core). - **ADR-0002** no new runtime dependency from the forbidden list — stdlib otherwise. - **ADR-0003** the check path imports no coverage-framework mappings. +- **ADR-0006** `src/utils/**` imports nothing from the repo — derived by `kit adr derive` + from an asymmetry the code had obeyed for months, then accepted. `node dist/cli.js adr check` runs them and **fails CI hard** on a violation. `kit check` does **not** include the ADR stage — only `kit review` (check + design + standards + adr + skill) diff --git a/docs/adr/0006-utils-is-a-leaf.md b/docs/adr/0006-utils-is-a-leaf.md new file mode 100644 index 00000000..08a4327c --- /dev/null +++ b/docs/adr/0006-utils-is-a-leaf.md @@ -0,0 +1,77 @@ +--- +id: ADR-0006 +title: src/utils is a leaf — it depends on nothing in the repo +status: accepted +--- + +# ADR-0006: src/utils is a leaf + +## Decision + +No file under `src/utils/**` imports anything else in this repository. It may use Node +builtins and declared dependencies; it may not reach into `src/` — not a sibling +subsystem, not the root, not the command layer. + +## Rationale + +This was not decided in a meeting. It was **derived from the code by `kit adr derive`** +and then confirmed by measurement, which is the point: a constraint the whole repo has +obeyed is a decision whether or not anyone wrote it down. + +Measured on the import graph at the time of writing: + +| direction | edges | +|---|---| +| `commands` → `utils` | 113 distinct file pairs | +| `(root)` → `utils` | 86 | +| `utils` → **anything in `src/`** | **0** | + +`kit adr derive` proposed three separate candidates from that asymmetry (`utils → +commands` at 113, `utils → adapters` at 8, `utils → memory` at 5). They are one rule: +`utils` imports **nothing**. Enforcing the general form is simpler than enforcing three +special cases, and it covers a subsystem that does not exist yet. + +Why it matters beyond tidiness: `utils` is the most fanned-in directory in the repo. +Anything it imports is imported, transitively, by almost everything. A single upward +import from `utils` into a subsystem creates a cycle through the majority of the tree and +makes the affected modules untestable in isolation. Keeping the bottom of the stack at +the bottom is what makes the layers above it movable. + +## Consequences + +- A helper in `utils` that needs a subsystem's type or function does not belong in + `utils`. Move it to the subsystem, or push the dependency the other way (pass the value + in rather than reaching for it). +- The rule is stated as "no parent-relative import", which is broader than "no sibling + subsystem". That is deliberate: `../` from `src/utils/**` can only ever leave `utils`, + so the broad form has no false positives and needs no maintenance when a directory is + added. +- Superseding this is an ADR-level act. If `utils` genuinely needs to depend on something, + amend or supersede this file in the same PR — the gate will otherwise refuse the code + and cite this ADR. + +## Scope of the evidence, stated honestly + +This records that the repository behaves as if the decision were made, and that we have +now made it. It is a snapshot of the import graph, not a reconstruction of anyone's +intent — see `kit adr derive`'s own limits (TS/JS + Python relative imports, one source +root, top-level buckets). + +Two related candidates were deliberately **not** accepted: + +- **"No subsystem imports the command layer"** — true of all 16 subsystems, not just the + six above the evidence floor, but the general form (`paths = "src/*/**"`) fires on + `src/commands/adr-derive.test.ts`, where a test fixture *string* containing + `import "../commands/x.js"` is read as a real import by the text-level extractor. The + glob grammar has no negation, so "every subsystem except `src/commands/**`" cannot be + expressed today. Left underived rather than encoded wrong. +- **`profile` does not import `exec-broker`** (support 8) — a lone pair with no wider + pattern behind it. Insufficient evidence that it is a decision rather than an ordering + accident. + +```toml kit-enforce +[[forbid_import]] +import = "^\\.\\./" +paths = "src/utils/**" +message = "src/utils is a leaf (ADR-0006) — it imports nothing from the rest of the repo; move the helper to the subsystem, or pass the value in" +``` From 5ed685312d7fc1efdce92dd72ce83db7f33e0d9e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 07:00:05 +0000 Subject: [PATCH 2/6] chore(memory): record the skill-gate decision as derived, not operator The decision belongs in the curated tier, but the provenance was the open question: an agent derived this from measurement and no human stated it, so `operator` -- the default when the field is absent -- would be exactly the mislabelling #550 was about. Written as `derived`. That has a useful second effect. The caveat left standing after #551 was that kit's own .kit/shared measured identically to before the fix: 46 entries, 41 without provenance, 0 aging, 0 stale, because operator and legacy entries never age. The corpus had nothing to classify. Measured now: entries=48 provenance={"(unset)":42, "operator":5, "derived":1} today : aging=0 stale=0 +200 days : aging=1 -> 0ed015 (derived) So the aging model has its first classifiable entry, and the 42 legacy ones stay exempt as designed. The mechanism was proven in isolation before; this is the first time the repo's own store exercises it. --- .kit/shared/memory.jsonl | 1 + 1 file changed, 1 insertion(+) diff --git a/.kit/shared/memory.jsonl b/.kit/shared/memory.jsonl index e68f63a8..95038739 100644 --- a/.kit/shared/memory.jsonl +++ b/.kit/shared/memory.jsonl @@ -45,3 +45,4 @@ {"id":"9c7fa6","area":"cli","kind":"convention","title":"A config section is declared in three places, and the third one warns","body":"Adding a .kit.toml section means kitConfig (type), CONFIG_SECTIONS (config-surface.ts, generates docs/CONFIGURATION.md) AND KNOWN_SECTIONS (config.ts), which loadConfig warns from. The first two were pinned to each other; the third had drifted by two — [supply_chain] and [coverage] are real, honoured sections that printed 'unknown section … (likely a typo)' on every kit invocation. A warning that fires on correct configuration trains the operator to ignore the one that fires on a real typo. config-surface.test.ts now pins all three in both directions.","refs":[],"author":"Peter Sandström ","ts":"2026-08-24T11:36:15.667Z","source_ref":"5855126","kid":"kid_31cd7bffcece85256671f0fced3b42df","sig":"0D2CmPQKGkbvtwM5zfM80w3jUa1h1F0AY8Nit6EFMzREVdIbI/PU+ULLVgERFb8G+f2zw46CBt1jSk4qyDAnCw=="} {"id":"f47c5a","area":"cli","kind":"convention","title":"A gate that exists but is never invoked is the default failure, not the exception","body":"kit adopted its own ADR gate in #403 and no workflow, hook or agent instruction ever called it — armed and unfired for a month, while the rules provably caught violations. A gate nobody runs emits nothing, and nothing reads exactly like a clean run. self-audit-ci already proves every script a workflow points AT exists; the inverse (a gate that exists is pointed at by something) had no rule. When adding a gate to this repo, wire the invocation AND pin it with a test that strips comments and forbids continue-on-error / || true — a gate named in a comment is not a gate, and one that cannot fail the build is a report. General case tracked in #533.","refs":[],"author":"Peter Sandström ","ts":"2026-08-25T12:06:22.827Z","source_ref":"a49e85c","kid":"kid_31cd7bffcece85256671f0fced3b42df","sig":"CNhZy+Of23FnzPSF6O95FvxAriwbLWBumewW/9QtgPN606vE4IiidXydMmKQj7JyfBJPOWRBCZM1c7QoSIr4Dw=="} {"id":"d1814d","area":"cli","kind":"decision","title":"pre-commit excludes full npm test until suite timeouts are fixed","body":"kit-public uses externally managed .githooks. [hooks].pre-commit should require staged security scan + build, not full npm test: a real pre-commit run on 2026-08-27 hit Node test file timeouts in dist/policy-gate.test.js and dist/secrets-propagate.test.js. Re-add full npm test only after those suite timeouts are fixed or the suite is split for hook use.","refs":[],"author":"Peter Sandström ","ts":"2026-08-27T09:22:36.795Z","source_ref":"3cfa838","kid":"kid_31cd7bffcece85256671f0fced3b42df","sig":"bbdKUzm6w6dIFEacsfFMdkuLhhwBjZ732zvs08X0ZPlv6NQ7J3CgDJ2GpZhf+M8k7Yff8gvlpPej4b+sYHWOBA=="} +{"id":"0ed015","area":"cli","kind":"decision","title":"A linter that lints skills must pass its own lint, and something must run it","body":"kit shipped a skill linter with a working --gate (exit 1) that nothing ever invoked -- not CI, not kit review, not verify-suite.sh -- while kit's only shipped SKILL.md failed that linter's scope check for as long as the linter had existed (no allowed-tools, so the skill implicitly claimed every tool). Fixed in d64c58e: the skill declares allowed-tools: Bash with its surface pinned in a snapshot, src/skill-run.ts gates every SKILL.md under skills/ and .claude/skills/, kit review gained a fifth stage, and ci.yml runs it as a hard failure with ci-adr-gate.test.ts pinning the invocation. Written as derived, not operator: an agent derived this from measurement, no human stated it. Second instance of the repo's own convention that a gate which exists but is never invoked is the default failure -- the first was the ADR gate in #403, unfired for a month.","refs":["d64c58e"],"author":"Claude ","ts":"2026-08-30T06:59:28.396Z","source_ref":"81fabd7","provenance":"derived","confidence":"high"} From b45d65c3514d96015473dee3a25f6e358b49c697 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 10:10:26 +0000 Subject: [PATCH 3/6] fix(lint): clear every eslint error, and stop a test from depending on a missing tool Provisioning the standards gates turned up more than the gates themselves. **The eslint findings were not where I said they were.** I had reported 174 findings "all in examples/", based on the ten sample lines kit happened to print. Measured properly: 174 = 6 errors + 168 warnings, and by directory it is src=161, examples=10, packages=3. The six errors are now fixed: - two scanners (snyk, sentrux) rethrew a JSON parse failure without attaching `cause`, discarding the only thing that says WHERE the output went wrong. In a scanner that turns "cannot verify" into an opaque failure, so both now pass `{ cause: err }`. - a `require()` in an ESM example, now a `node:crypto` import - three unused imports/bindings Two more warnings were dead `eslint-disable` directives for `no-control-regex` that suppressed nothing -- a suppression that suppresses nothing is a lie in the code for the next reader. Removed. 174 -> 166, and 0 errors. The remaining 166 are structural: complexity=71, no-explicit-any=32, max-lines-per-function=27, max-depth=18, max-lines=11, max-params=6. That is the same debt lizard and scc report, and refactoring it is a program rather than a task. It is in the frozen baseline, which is what makes the gate usable at all. **And a test whose green depended on the machine.** `collectStandardsKeys` "returns empty slices when the general tools are absent" asserted against the real cwd with the comment "lizard/jscpd/scc are not installed in this environment". Installing lizard and jscpd turned it red with no code change. A test that passes because the box happens to be missing something proves nothing on a box that has it. It now empties PATH for the duration of the call, so the absent-tools condition is forced rather than hoped for, and it passes on both kinds of machine. Baseline: .kit-baseline.json now carries 833 frozen standards findings from the first run of complexity/duplication/size. It includes the six eslint errors fixed above -- a frozen finding that no longer occurs simply never matches -- and the 49 scc findings, so the file size gate does not report 49 "new" findings the day scc is provisioned somewhere. --- .kit-baseline.json | 845 ++++++++++++++++++++- examples/cdn-cache-headers.ts | 3 +- examples/team-management-workflows.ts | 3 +- packages/kit-plugin-sentrux/src/scan.ts | 6 +- packages/kit-plugin-snyk/src/scan.ts | 6 +- packages/kit-plugin-supabase/src/rotate.ts | 1 - src/check-standards.test.ts | 17 +- src/commands/usage.test.ts | 2 +- src/commands/usage.ts | 2 +- 9 files changed, 873 insertions(+), 12 deletions(-) diff --git a/.kit-baseline.json b/.kit-baseline.json index bb0b6471..a626c363 100644 --- a/.kit-baseline.json +++ b/.kit-baseline.json @@ -94,8 +94,851 @@ "src/utils/promptSelect.ts", "src/validation/dynamic-schema.ts" ] + }, + "standards": { + "complexity": [ + "packages/kit-plugin-aisle/src/scan.test.ts:(anonymous)", + "packages/kit-plugin-aisle/src/scan.ts:(anonymous)", + "packages/kit-plugin-aisle/src/scan.ts:normalizeSeverity", + "packages/kit-plugin-railway/src/railway-deploy.ts:provision", + "scripts/gen-plugin-registry.mjs:collect", + "skills/triage/scripts/triage.py:_resolve_npm_spec", + "skills/triage/scripts/triage.py:_resolve_npm_spec.satisfies", + "skills/triage/scripts/triage.py:_resolve_pip_spec", + "skills/triage/scripts/triage.py:triage_npm", + "skills/triage/scripts/triage.py:triage_pip", + "skills/triage/scripts/triage.py:triage_repo", + "src/adapters-flyio.test.ts:(anonymous)", + "src/adapters-railway.test.ts:(anonymous)", + "src/adapters/expo-eas.ts:provision", + "src/adapters/ingest.ts:parseSarif", + "src/adapters/searxng-instance.ts:provision", + "src/adapters/vercel-hosting.ts:provision", + "src/adr-derive.ts:deriveLayerCandidates", + "src/adr.ts:scalar", + "src/advisory-baseline.ts:visit", + "src/agent-audit.test.ts:(anonymous)", + "src/agent-audit.ts:auditMcpServers", + "src/agent-config.test.ts:(anonymous)", + "src/agent-config.test.ts:(anonymous)", + "src/agent-config.test.ts:(anonymous)", + "src/agent-config.ts:(anonymous)", + "src/agent-config.ts:codexGateCommands", + "src/agent-config.ts:managedWrapperProblems", + "src/analyze.ts:detectTestRunners", + "src/analyze.ts:gitCommitPrefixes", + "src/analyze.ts:renderClaudeMd", + "src/approval.test.ts:(anonymous)", + "src/approval.test.ts:(anonymous)", + "src/approval.ts:requestApproval", + "src/audit-anchor-all.test.ts:(anonymous)", + "src/audit-anchor-all.test.ts:(anonymous)", + "src/audit-anchor.test.ts:(anonymous)", + "src/audit-anchor.test.ts:(anonymous)", + "src/audit-anchor.test.ts:(anonymous)", + "src/audit-anchor.test.ts:(anonymous)", + "src/audit.test.ts:(anonymous)", + "src/audit.test.ts:(anonymous)", + "src/audit.test.ts:(anonymous)", + "src/audit.ts:logAuditEvent", + "src/browser.test.ts:(anonymous)", + "src/browser.ts:diagnoseBrowser", + "src/budget.test.ts:(anonymous)", + "src/check-attestation.test.ts:(anonymous)", + "src/check-attestation.ts:verifyAttestation", + "src/check-decision-ledger.test.ts:(anonymous)", + "src/check-decision-ledger.ts:checkDecisionLedger", + "src/check-deploy.test.ts:(anonymous)", + "src/check-deploy.ts:checkVercelDeploy", + "src/check-deploy.ts:parseVercelEnvNames", + "src/check-design.ts:stripComments", + "src/check-disk-encryption.ts:checkDiskEncryption", + "src/check-gitignore.test.ts:(anonymous)", + "src/check-hooks.test.ts:(anonymous)", + "src/check-lock.test.ts:(anonymous)", + "src/check-lock.ts:checkLockFiles", + "src/check-policy-ops.test.ts:(anonymous)", + "src/check-run.ts:runCheckGate", + "src/check-security-cwd.test.ts:(anonymous)", + "src/check-security.test.ts:(anonymous)", + "src/check-security.test.ts:(anonymous)", + "src/check-security.test.ts:(anonymous)", + "src/check-security.ts:checkAllowScripts", + "src/check-security.ts:checkGuardDog", + "src/check-security.ts:checkMavenAudit", + "src/check-security.ts:checkPinnedVersions", + "src/check-security.ts:checkSecretsInCode", + "src/check-security.ts:checkServiceExposure", + "src/check-security.ts:checkTrivyConfig", + "src/check-standards.test.ts:(anonymous)", + "src/check-standards.ts:checkStandards", + "src/check-web-search.ts:checkWebSearch", + "src/cli.test.ts:(anonymous)", + "src/cli.test.ts:(anonymous)", + "src/cli.test.ts:(anonymous)", + "src/cli.test.ts:(anonymous)", + "src/cli.ts:cmdHelp", + "src/cli.ts:main", + "src/clone.test.ts:(anonymous)", + "src/commands/adr-derive.ts:adrDerive", + "src/commands/adr.ts:tryFile", + "src/commands/broker.test.ts:(anonymous)", + "src/commands/broker.test.ts:(anonymous)", + "src/commands/browser.ts:cmdBrowser", + "src/commands/check.ts:cmdCheck", + "src/commands/ci.ts:cmdCi", + "src/commands/config.test.ts:(anonymous)", + "src/commands/config.ts:migrateConfigFile", + "src/commands/context.ts:cmdContext", + "src/commands/context.ts:cmdContextCheck", + "src/commands/coverage.ts:cmdCoverage", + "src/commands/coverage.ts:cmdSelfAudit", + "src/commands/env.ts:cmdEnv", + "src/commands/hooks.test.ts:(anonymous)", + "src/commands/hooks.ts:cmdHooks", + "src/commands/hooks.ts:cmdHooksAdd", + "src/commands/insight.ts:insightUnused", + "src/commands/mcp.ts:cmdMcp", + "src/commands/memory.ts:memMerge", + "src/commands/memory.ts:memPal", + "src/commands/memory.ts:memStats", + "src/commands/memory.ts:memVerify", + "src/commands/panic.ts:cmdPanic", + "src/commands/profile.test.ts:(anonymous)", + "src/commands/project.ts:cmdClone", + "src/commands/repomap.ts:cmdMap", + "src/commands/review.test.ts:(anonymous)", + "src/commands/scan.ts:cmdScan", + "src/commands/secrets.ts:(anonymous)", + "src/commands/secrets.ts:cmdSecrets", + "src/commands/secrets.ts:cmdSecretsMigrate", + "src/commands/secrets.ts:cmdSecretsOneCli", + "src/commands/secrets.ts:cmdSecretsPropagateStandalone", + "src/commands/secrets.ts:cmdSecretsPurgeHistory", + "src/commands/security.ts:cmdSecurity", + "src/commands/security.ts:cmdSecurityPrescan", + "src/commands/security.ts:cmdSecurityVerifyPull", + "src/commands/sentinel.ts:cmdSentinel", + "src/commands/setup.ts:runConfiguredCommand", + "src/commands/skill.ts:cmdSkill", + "src/commands/tools.ts:cmdTools", + "src/commands/triage.ts:cmdTriage", + "src/completions.ts:generateBashCompletion", + "src/config.test.ts:(anonymous)", + "src/config.test.ts:(anonymous)", + "src/context-lock.test.ts:(anonymous)", + "src/context-lock.ts:emit", + "src/context-lock.ts:suggestContextToml", + "src/context.test.ts:(anonymous)", + "src/cost-monitor.test.ts:(anonymous)", + "src/cost-monitor.test.ts:(anonymous)", + "src/cost-monitor.ts:detectCostAnomalies", + "src/create-plugin.test.ts:(anonymous)", + "src/database.test.ts:(anonymous)", + "src/decision-ledger.test.ts:(anonymous)", + "src/decision-ledger.ts:(anonymous)", + "src/doctor.test.ts:(anonymous)", + "src/doctor.ts:checkIdentityKeystore", + "src/doctor.ts:checkNodeVersion", + "src/elevation-scopes.test.ts:(anonymous)", + "src/elevation.test.ts:(anonymous)", + "src/env-diff.test.ts:(anonymous)", + "src/environment.test.ts:(anonymous)", + "src/escalate.test.ts:(anonymous)", + "src/exec-broker/broker.test.ts:(anonymous)", + "src/exec-broker/broker.test.ts:(anonymous)", + "src/exec-broker/broker.ts:collectDenials", + "src/exec-broker/policy-cwd.test.ts:(anonymous)", + "src/exec-broker/policy.test.ts:(anonymous)", + "src/exec-broker/policy.ts:validateBrokerPolicy", + "src/exec-broker/profile-policy.test.ts:(anonymous)", + "src/external-findings.ts:parseExternalFindings", + "src/fix-cwd.test.ts:(anonymous)", + "src/fix.ts:(anonymous)", + "src/fix.ts:fixDeploy", + "src/flag-surface.test.ts:(anonymous)", + "src/flag-surface.test.ts:stringLiterals", + "src/governance-middleware.test.ts:(anonymous)", + "src/governance-middleware.test.ts:(anonymous)", + "src/governance-middleware.test.ts:(anonymous)", + "src/governance-middleware.test.ts:(anonymous)", + "src/governance.test.ts:(anonymous)", + "src/guard.test.ts:(anonymous)", + "src/health-sensors/bitbucket-pipelines.test.ts:deps", + "src/health-sensors/github-actions.test.ts:(anonymous)", + "src/hints.test.ts:(anonymous)", + "src/hitl.test.ts:(anonymous)", + "src/hitl.ts:stripInfoCommand", + "src/hook-floor.test.ts:(anonymous)", + "src/hook-floor.test.ts:(anonymous)", + "src/hooks.test.ts:(anonymous)", + "src/hooks.test.ts:(anonymous)", + "src/hooks.test.ts:(anonymous)", + "src/hooks.test.ts:(anonymous)", + "src/identity.test.ts:(anonymous)", + "src/insight/unused.ts:computeUnused", + "src/install-gate.test.ts:(anonymous)", + "src/install-gate.test.ts:(anonymous)", + "src/install-gate.test.ts:(anonymous)", + "src/install-gate.ts:argStart", + "src/install-gate.ts:pipInstallerArgStart", + "src/install.test.ts:(anonymous)", + "src/install.test.ts:(anonymous)", + "src/keyless/http-sig.test.ts:(anonymous)", + "src/keyless/sign-request.test.ts:(anonymous)", + "src/keyless/sign-request.test.ts:(anonymous)", + "src/keystore/active.test.ts:(anonymous)", + "src/keystore/command-store.test.ts:(anonymous)", + "src/keystore/mandate.test.ts:(anonymous)", + "src/keystore/resolve.test.ts:(anonymous)", + "src/keystore/revoked-guard.test.ts:counting", + "src/keystore/trust-store.test.ts:fakeStore", + "src/kit-wrapper-entry.test.ts:(anonymous)", + "src/kit-wrapper.test.ts:(anonymous)", + "src/kit-wrapper.ts:ensureKitWrapper", + "src/lock.test.ts:(anonymous)", + "src/lock.test.ts:(anonymous)", + "src/login.test.ts:(anonymous)", + "src/login.ts:loginServices", + "src/mcp-dependency-surface.test.ts:(anonymous)", + "src/mcp-orchestrator.test.ts:(anonymous)", + "src/mcp-server.test.ts:(anonymous)", + "src/mcp-server.test.ts:(anonymous)", + "src/mcp-server.test.ts:(anonymous)", + "src/mcp-server.test.ts:(anonymous)", + "src/mcp-server.test.ts:(anonymous)", + "src/mcp-server.test.ts:(anonymous)", + "src/mcp-server.ts:(anonymous)", + "src/mcp-server.ts:(anonymous)", + "src/mcp-server.ts:register_kit_fix", + "src/mcp-server.ts:register_kit_init", + "src/mcp-triage.ts:stableStringify", + "src/memory/amazonq.test.ts:(anonymous)", + "src/memory/codex.ts:indexFile", + "src/memory/db.test.ts:(anonymous)", + "src/memory/db.test.ts:(anonymous)", + "src/memory/db.ts:insertMessage", + "src/memory/db.ts:searchMessages", + "src/memory/db.ts:toFtsMatchQuery", + "src/memory/droid.test.ts:(anonymous)", + "src/memory/droid.ts:indexFile", + "src/memory/hook.test.ts:(anonymous)", + "src/memory/hook.ts:sessionStartRecovery", + "src/memory/injection.test.ts:(anonymous)", + "src/memory/kiro.test.ts:(anonymous)", + "src/memory/learn.test.ts:(anonymous)", + "src/memory/merge.test.ts:(anonymous)", + "src/memory/merge.ts:projectKeyFor", + "src/memory/obsidian.test.ts:mk", + "src/memory/opencode.ts:indexOpenCodeDb", + "src/memory/pal.test.ts:(anonymous)", + "src/memory/pal.ts:palSyncFindings", + "src/memory/parser.test.ts:(anonymous)", + "src/memory/parser.ts:indexFile", + "src/memory/remote-sync.test.ts:(anonymous)", + "src/memory/remote-sync.test.ts:(anonymous)", + "src/memory/remote-sync.test.ts:(anonymous)", + "src/memory/scan.ts:(anonymous)", + "src/memory/shared.test.ts:(anonymous)", + "src/memory/sync.test.ts:(anonymous)", + "src/memory/threads.test.ts:(anonymous)", + "src/memory/write-gate.test.ts:(anonymous)", + "src/memory/write-gate.ts:evaluateWriteGate", + "src/open.test.ts:(anonymous)", + "src/opencli.test.ts:(anonymous)", + "src/output.ts:printSecurityTable", + "src/pkg.test.ts:(anonymous)", + "src/plugin-loader.test.ts:(anonymous)", + "src/plugin-write-gates.test.ts:(anonymous)", + "src/plugins.test.ts:(anonymous)", + "src/policy-check.test.ts:(anonymous)", + "src/policy-check.ts:evaluatePolicy", + "src/policy-doc.ts:validatePolicy", + "src/policy-doc.ts:verifyPolicy", + "src/policy-gate.test.ts:(anonymous)", + "src/policy-gate.test.ts:(anonymous)", + "src/policy-gate.test.ts:(anonymous)", + "src/policy-gate.test.ts:(anonymous)", + "src/policy-trust.test.ts:(anonymous)", + "src/post-pull-audit.test.ts:(anonymous)", + "src/profile/portable.ts:importBundle", + "src/profile/reconcile.test.ts:(anonymous)", + "src/rbac/policy-schema.test.ts:(anonymous)", + "src/rbac/rbac.test.ts:(anonymous)", + "src/rbac/rbac.test.ts:(anonymous)", + "src/rbac/resolve.test.ts:(anonymous)", + "src/read-only-surface.test.ts:(anonymous)", + "src/recommended.ts:recommendPosture", + "src/repomap/ownership.ts:globMatch", + "src/resend-email.test.ts:(anonymous)", + "src/review-cwd.test.ts:(anonymous)", + "src/run.test.ts:(anonymous)", + "src/run.ts:(anonymous)", + "src/run.ts:executeCommand", + "src/scan-diff.ts:classifyChange", + "src/scan-staged.test.ts:(anonymous)", + "src/scanners.ts:isLocalSemgrepConfig", + "src/scanners.ts:verifyAirGapScanners", + "src/scope-needs-adoption.test.ts:extractContextObject", + "src/scope-needs-adoption.test.ts:stripNoise", + "src/secret-backends.test.ts:(anonymous)", + "src/secret-expiration.test.ts:(anonymous)", + "src/secrets-propagate.test.ts:(anonymous)", + "src/secrets-rotate-cli.ts:buildPlaybook", + "src/secrets-rotate-cli.ts:cmdSecretsRotate", + "src/secrets-rotate-cli.ts:cmdSecretsRotateSupabaseMgmt", + "src/secrets-rotate-cli.ts:pickBackendOpts", + "src/secrets-vault-migrate.ts:readSecretFromBackend", + "src/secrets-vault-migrate.ts:vaultMigrate", + "src/secrets.test.ts:(anonymous)", + "src/secrets.ts:generateSecrets", + "src/security-prescan.test.ts:(anonymous)", + "src/security-prescan.test.ts:(anonymous)", + "src/security-prescan.test.ts:(anonymous)", + "src/security-prescan.ts:runPrescan", + "src/self-audit-ci.test.ts:(anonymous)", + "src/self-audit-ci.ts:runCiScriptAudit", + "src/self-audit-docs.test.ts:(anonymous)", + "src/self-audit-docs.test.ts:(anonymous)", + "src/self-audit-docs.test.ts:(anonymous)", + "src/self-audit-docs.ts:extractDocCommandRefs", + "src/self-audit-wiring.test.ts:(anonymous)", + "src/self-audit.test.ts:(anonymous)", + "src/self-audit.ts:run", + "src/self-audit.ts:stripStringsAndComments", + "src/sentinel.ts:parseSuppressions", + "src/service-adapter.test.ts:(anonymous)", + "src/service-adapter.test.ts:(anonymous)", + "src/service-adapter.test.ts:(anonymous)", + "src/service-registry.test.ts:(anonymous)", + "src/skill-run.test.ts:(anonymous)", + "src/skill/attribute.test.ts:(anonymous)", + "src/skill/test.test.ts:(anonymous)", + "src/skill/test.test.ts:(anonymous)", + "src/source-walk.test.ts:(anonymous)", + "src/stack-detector.test.ts:(anonymous)", + "src/stack-detector.ts:resolveNodeVersion", + "src/stack-detector.ts:resolvePythonVersion", + "src/standards-plugins-exec.test.ts:(anonymous)", + "src/standards-plugins.test.ts:(anonymous)", + "src/standards-plugins.ts:checkStandardsPlugins", + "src/standards-plugins.ts:evaluatePlugin", + "src/standards-plugins.ts:loadStandardPlugins", + "src/status.test.ts:(anonymous)", + "src/supply-chain.ts:runSupplyChain", + "src/toml-generator.test.ts:(anonymous)", + "src/toml-generator.test.ts:(anonymous)", + "src/toml-generator.ts:generateToml", + "src/toml-generator.ts:parseEnvTemplateKeys", + "src/toml-generator.ts:secretsSection", + "src/toml-generator.ts:setupSection", + "src/tool-inventory.ts:readToolVersion", + "src/tool-latest.test.ts:deps", + "src/tool-provenance.ts:classifyToolPath", + "src/triage-ecosystem-parity.test.ts:(anonymous)", + "src/triage.test.ts:(anonymous)", + "src/triage.test.ts:(anonymous)", + "src/triage.ts:verdictPassed", + "src/update-check.test.ts:(anonymous)", + "src/usage-prove.ts:controlSecretScanBlocksCommit", + "src/usage-report.test.ts:(anonymous)", + "src/usage-report.ts:coverageFromRuns", + "src/usage-report.ts:keysFromConfigAndRun", + "src/utils/promptMultiSelect.test.ts:(anonymous)", + "src/utils/promptSelect.ts:promptSelect", + "src/utils/shellSplit.ts:shellSplit" + ], + "duplication": [ + ".github/workflows/security.yml|.github/workflows/security.yml", + ".kit.toml|README.md:toml", + "AGENTS.md:markdown|CLAUDE.md:markdown", + "AGENTS.md:markdown|README.md:markdown", + "README.md:markdown|README.md:markdown", + "README.md:markdown|docs/COMMANDS.md:markdown", + "README.md:markdown|docs/adr/0003-core-coverage-isolation.md:markdown", + "README.md:toml|docs/COMMANDS.md:toml", + "contracts/kit.opencli.json|contracts/kit.opencli.json", + "docs/ADAPTER_GUIDE.md:typescript|docs/CUSTOM_ADAPTERS.md:typescript", + "docs/AIR_GAP.md:markdown|docs/AIR_GAP.md:markdown", + "docs/AUDIT_ATTESTATION.md:markdown|docs/CLI_STABILITY.md:markdown", + "docs/AUDIT_ATTESTATION.md:markdown|docs/COMMANDS.md:markdown", + "docs/AUDIT_ATTESTATION.md:markdown|docs/PLATFORM_SUPPORT.md:markdown", + "docs/COMMANDS.md:markdown|docs/COMMANDS.md:markdown", + "docs/CUSTOM_ADAPTERS.md:typescript|docs/PLUGIN_DEVELOPMENT.md:typescript", + "docs/plans/2026-06-21-kit-health-v1a.md:typescript|src/cli-health.test.ts", + "docs/plans/2026-06-21-kit-health-v1a.md:typescript|src/commands/info.ts", + "docs/plans/2026-06-21-kit-health-v1a.md:typescript|src/findings-track.ts", + "docs/plans/2026-06-21-kit-health-v1a.md:typescript|src/health-sensors/github-actions.test.ts", + "docs/plans/2026-06-21-kit-health-v1a.md:typescript|src/health-sensors/github-actions.ts", + "docs/plans/2026-06-21-kit-health-v1a.md:typescript|src/health-track.test.ts", + "docs/plans/2026-06-21-kit-health-v1a.md:typescript|src/health-track.ts", + "docs/plans/2026-06-21-kit-health-v1a.md:typescript|src/health.test.ts", + "docs/plans/2026-06-21-kit-health-v1a.md:typescript|src/health.ts", + "packages/adapter-sdk/CHANGELOG.md:markdown|packages/adapter-sdk/README.md:markdown", + "packages/adapter-sdk/tsconfig.json|packages/kit-plugin-aisle/tsconfig.json", + "packages/adapter-sdk/tsconfig.json|packages/kit-plugin-cloudflare/tsconfig.json", + "packages/adapter-sdk/tsconfig.json|packages/kit-plugin-fly/tsconfig.json", + "packages/adapter-sdk/tsconfig.json|packages/kit-plugin-github/tsconfig.json", + "packages/adapter-sdk/tsconfig.json|packages/kit-plugin-railway/tsconfig.json", + "packages/adapter-sdk/tsconfig.json|packages/kit-plugin-sentrux/tsconfig.json", + "packages/adapter-sdk/tsconfig.json|packages/kit-plugin-sentry/tsconfig.json", + "packages/adapter-sdk/tsconfig.json|packages/kit-plugin-snyk/tsconfig.json", + "packages/adapter-sdk/tsconfig.json|packages/kit-plugin-stripe/tsconfig.json", + "packages/adapter-sdk/tsconfig.json|packages/kit-plugin-supabase/tsconfig.json", + "packages/adapter-sdk/tsconfig.json|packages/kit-plugin-vercel/tsconfig.json", + "packages/adapter-sdk/tsconfig.json|packages/kit-plugin-wiz/tsconfig.json", + "packages/kit-plugin-aisle/package.json|packages/kit-plugin-cloudflare/package.json", + "packages/kit-plugin-aisle/package.json|packages/kit-plugin-fly/package.json", + "packages/kit-plugin-aisle/package.json|packages/kit-plugin-sentrux/package.json", + "packages/kit-plugin-aisle/package.json|packages/kit-plugin-sentry/package.json", + "packages/kit-plugin-aisle/package.json|packages/kit-plugin-stripe/package.json", + "packages/kit-plugin-cloudflare/src/mgmt-api.ts|packages/kit-plugin-fly/src/mgmt-api.ts", + "packages/kit-plugin-cloudflare/src/mgmt-api.ts|packages/kit-plugin-github/src/mgmt-api.ts", + "packages/kit-plugin-cloudflare/src/mgmt-api.ts|packages/kit-plugin-sentry/src/mgmt-api.ts", + "packages/kit-plugin-cloudflare/src/mgmt-api.ts|packages/kit-plugin-stripe/src/mgmt-api.ts", + "packages/kit-plugin-cloudflare/src/mgmt-api.ts|packages/kit-plugin-supabase/src/mgmt-api.ts", + "packages/kit-plugin-cloudflare/src/mgmt-api.ts|packages/kit-plugin-vercel/src/mgmt-api.ts", + "packages/kit-plugin-github/package.json|packages/kit-plugin-railway/package.json", + "packages/kit-plugin-github/package.json|packages/kit-plugin-supabase/package.json", + "packages/kit-plugin-github/package.json|packages/kit-plugin-vercel/package.json", + "packages/kit-plugin-github/src/mgmt-api.ts|packages/kit-plugin-vercel/src/mgmt-api.ts", + "packages/kit-plugin-railway/src/railway-deploy.ts|src/adapters/flyio-hosting.ts", + "packages/kit-plugin-sentrux/package.json|packages/kit-plugin-snyk/package.json", + "packages/kit-plugin-sentrux/package.json|packages/kit-plugin-wiz/package.json", + "packages/kit-plugin-sentrux/src/scan.ts|packages/kit-plugin-snyk/src/scan.ts", + "scripts/gen-opencli.mjs|scripts/gen-public-surface.mjs", + "src/adapters-flyio.test.ts|src/adapters-railway.test.ts", + "src/adapters/flyio-hosting.ts|src/adapters/railway-hosting.ts", + "src/agent-audit.test.ts|src/agent-audit.test.ts", + "src/agent-audit.test.ts|src/ci-audit.test.ts", + "src/agent-audit.ts|src/agent-audit.ts", + "src/agent-config.test.ts|src/agent-config.test.ts", + "src/agent-config.ts|src/agent-config.ts", + "src/analyze.ts|src/analyze.ts", + "src/approval.test.ts|src/approval.test.ts", + "src/audit-anchor-all.test.ts|src/audit-anchor-all.test.ts", + "src/audit-anchor-all.test.ts|src/cli.test.ts", + "src/audit-anchor-all.test.ts|src/flag-surface.test.ts", + "src/audit-anchor-all.test.ts|src/hook-floor.test.ts", + "src/audit-anchor.test.ts|src/audit-anchor.test.ts", + "src/audit-anchor.ts|src/elevation.ts", + "src/audit.test.ts|src/audit.test.ts", + "src/broker/gates.test.ts|src/exec-broker/profile-policy.test.ts", + "src/budget.test.ts|src/budget.test.ts", + "src/budget.test.ts|src/check-hooks.test.ts", + "src/budget.test.ts|src/check-lock.test.ts", + "src/budget.test.ts|src/governance-middleware.test.ts", + "src/check-attestation.test.ts|src/check-attestation.test.ts", + "src/check-client-exposure.test.ts|src/check-nested-projects.test.ts", + "src/check-deploy.test.ts|src/check-deploy.test.ts", + "src/check-design.ts|src/check-design.ts", + "src/check-gitignore.test.ts|src/check-gitignore.test.ts", + "src/check-hooks.test.ts|src/check-hooks.test.ts", + "src/check-hooks.test.ts|src/hooks.test.ts", + "src/check-lock.test.ts|src/check-lock.test.ts", + "src/check-run.ts|src/commands/ci.ts", + "src/check-secrets.ts|src/secret-backends.ts", + "src/check-security-cwd.test.ts|src/review-cwd.test.ts", + "src/check-services.ts|src/login.ts", + "src/check-standards-platform.ts|src/check-standards-specific.ts", + "src/check-standards-specific.ts|src/check-standards-specific.ts", + "src/check-web-search.test.ts|src/check-web-search.test.ts", + "src/check-web-search.ts|src/check-web-search.ts", + "src/ci-audit.test.ts|src/self-audit-ci.test.ts", + "src/cli.test.ts|src/cli.test.ts", + "src/cli.test.ts|src/commands/hooks.test.ts", + "src/commands/agent.ts|src/commands/setup.ts", + "src/commands/agent.ts|src/plugins-cli.ts", + "src/commands/broker.test.ts|src/commands/broker.test.ts", + "src/commands/broker.test.ts|src/commands/browser.test.ts", + "src/commands/broker.test.ts|src/commands/coverage.test.ts", + "src/commands/broker.test.ts|src/commands/profile.test.ts", + "src/commands/broker.test.ts|src/commands/triage.test.ts", + "src/commands/check.ts|src/commands/ci.ts", + "src/commands/check.ts|src/commands/design.ts", + "src/commands/check.ts|src/commands/hooks.ts", + "src/commands/check.ts|src/commands/review.ts", + "src/commands/config.ts|src/commands/config.ts", + "src/commands/design.ts|src/commands/standards.ts", + "src/commands/flag-value-forms.test.ts|src/flag-form-parity.test.ts", + "src/commands/gate.ts|src/commands/gate.ts", + "src/commands/gha-audit.ts|src/commands/info.ts", + "src/commands/hooks.test.ts|src/commands/hooks.test.ts", + "src/commands/info.ts|src/commands/info.ts", + "src/commands/memory.ts|src/commands/memory.ts", + "src/commands/policy.ts|src/commands/profile.ts", + "src/commands/profile.test.ts|src/profile/reconcile.test.ts", + "src/commands/profile.ts|src/commands/profile.ts", + "src/commands/project.ts|src/plugins-cli.ts", + "src/commands/project.ts|src/run.ts", + "src/commands/secrets.ts|src/commands/secrets.ts", + "src/commands/secrets.ts|src/secrets-rotate-cli.ts", + "src/commands/security.ts|src/commands/security.ts", + "src/commands/security.ts|src/commands/setup.ts", + "src/commands/setup.ts|src/commands/upgrade.ts", + "src/commands/setup.ts|src/fix.ts", + "src/commands/tools.test.ts|src/gate-bash-refusal.test.ts", + "src/commands/upgrade.ts|src/mcp-server.ts", + "src/config-migrate-text.test.ts|src/usage-prove.ts", + "src/config-surface.test.ts|src/flag-surface.test.ts", + "src/config-surface.test.ts|src/plugin-registry.generated.test.ts", + "src/cost-monitor.test.ts|src/cost-monitor.test.ts", + "src/database.test.ts|src/database.test.ts", + "src/docs-verify-commands.test.ts|src/publish-workflow.test.ts", + "src/doctor.test.ts|src/doctor.test.ts", + "src/doctor.test.ts|src/policy-pull-rbac.test.ts", + "src/doctor.test.ts|src/policy-pull.test.ts", + "src/doctor.test.ts|src/profile/portable.test.ts", + "src/env-diff.ts|src/env-inspect.ts", + "src/env-diff.ts|src/secrets-migrate.ts", + "src/env-diff.ts|src/secrets-validate.ts", + "src/environment.test.ts|src/environment.test.ts", + "src/exec-broker/broker.test.ts|src/exec-broker/broker.test.ts", + "src/exec-broker/policy-cwd.test.ts|src/exec-broker/policy-cwd.test.ts", + "src/exec-broker/profile-policy.test.ts|src/keyless/sign-request.test.ts", + "src/exec-broker/profile-policy.test.ts|src/profile/sign.test.ts", + "src/exec-broker/scope-needs.test.ts|src/governance-middleware.test.ts", + "src/fix-cwd.test.ts|src/review-cwd.test.ts", + "src/flag-form-parity.test.ts|src/read-only-surface.test.ts", + "src/flag-surface.test.ts|src/hook-floor.test.ts", + "src/flag-surface.ts|src/flag-surface.ts", + "src/governance-middleware.test.ts|src/governance-middleware.test.ts", + "src/governance-middleware.ts|src/governance-middleware.ts", + "src/health-sensors/bitbucket-pipelines.test.ts|src/health-sensors/gitlab-ci.test.ts", + "src/health-sensors/bitbucket-pipelines.test.ts|src/health-sensors/vercel.test.ts", + "src/health-sensors/bitbucket-pipelines.ts|src/health-sensors/gitlab-ci.ts", + "src/health-sensors/github-actions.test.ts|src/health-sensors/github-actions.test.ts", + "src/health-sensors/gitlab-ci.test.ts|src/health-sensors/sentry.test.ts", + "src/health-sensors/resend.test.ts|src/health-sensors/sentry.test.ts", + "src/health-sensors/resend.test.ts|src/health-sensors/supabase-advisor.test.ts", + "src/health-sensors/resend.test.ts|src/health-sensors/vercel.test.ts", + "src/health-sensors/tls-cert.test.ts|src/health.test.ts", + "src/hook-floor.test.ts|src/hook-floor.test.ts", + "src/hooks.test.ts|src/hooks.test.ts", + "src/identity.test.ts|src/keystore/file-store.test.ts", + "src/identity.test.ts|src/policy-doc.test.ts", + "src/install-gate.test.ts|src/install-gate.test.ts", + "src/install-gate.ts|src/install-gate.ts", + "src/install.test.ts|src/install.test.ts", + "src/keyless/sign-request.test.ts|src/keyless/sign-request.test.ts", + "src/keystore/command-store.test.ts|src/keystore/trust-store.test.ts", + "src/keystore/mandate.test.ts|src/keystore/mandate.test.ts", + "src/keystore/revoked-guard.test.ts|src/keystore/trust-store.test.ts", + "src/keystore/secure-enclave-store.ts|src/keystore/tpm-store.ts", + "src/keystore/stubs.test.ts|src/keystore/stubs.test.ts", + "src/lock.test.ts|src/lock.test.ts", + "src/mcp-server.test.ts|src/mcp-server.test.ts", + "src/mcp-server.ts|src/mcp-server.ts", + "src/memory-share-cli.test.ts|src/memory-share-cli.test.ts", + "src/memory/amazonq.test.ts|src/memory/cursor.test.ts", + "src/memory/amazonq.test.ts|src/memory/kiro.test.ts", + "src/memory/amazonq.ts|src/memory/antigravity.ts", + "src/memory/amazonq.ts|src/memory/cursor.ts", + "src/memory/amazonq.ts|src/memory/kiro.ts", + "src/memory/antigravity.test.ts|src/memory/cline.test.ts", + "src/memory/antigravity.test.ts|src/memory/codex.test.ts", + "src/memory/antigravity.test.ts|src/memory/continue.test.ts", + "src/memory/antigravity.test.ts|src/memory/droid.test.ts", + "src/memory/antigravity.test.ts|src/memory/gemini.test.ts", + "src/memory/antigravity.test.ts|src/memory/parser.test.ts", + "src/memory/antigravity.ts|src/memory/cline.ts", + "src/memory/antigravity.ts|src/memory/codex.ts", + "src/memory/backup.test.ts|src/memory/backup.test.ts", + "src/memory/backup.ts|src/memory/backup.ts", + "src/memory/cline.ts|src/memory/continue.ts", + "src/memory/cline.ts|src/memory/gemini.ts", + "src/memory/codex.ts|src/memory/continue.ts", + "src/memory/codex.ts|src/memory/droid.ts", + "src/memory/codex.ts|src/memory/gemini.ts", + "src/memory/codex.ts|src/memory/opencode.ts", + "src/memory/continue.ts|src/memory/gemini.ts", + "src/memory/hook.test.ts|src/memory/hook.test.ts", + "src/memory/hook.test.ts|src/memory/shared.test.ts", + "src/memory/hook.test.ts|src/memory/suggest.test.ts", + "src/memory/install.test.ts|src/memory/install.test.ts", + "src/memory/merge.test.ts|src/memory/merge.test.ts", + "src/memory/opencode.test.ts|src/memory/opencode.test.ts", + "src/memory/pal.test.ts|src/memory/pal.test.ts", + "src/memory/remote-sync.test.ts|src/memory/remote-sync.test.ts", + "src/memory/shared.test.ts|src/memory/shared.test.ts", + "src/memory/sync.test.ts|src/memory/sync.test.ts", + "src/multi-env.test.ts|src/multi-env.test.ts", + "src/opencli.ts|src/public-surface.ts", + "src/pkg.test.ts|src/pkg.test.ts", + "src/plugin-loader.test.ts|src/plugin-loader.test.ts", + "src/plugins-cli.ts|src/plugins-cli.ts", + "src/policy-check.test.ts|src/policy-trust.test.ts", + "src/policy-check.test.ts|src/rbac/rbac.test.ts", + "src/policy-doc.ts|src/profile/sign.ts", + "src/policy-pull-rbac.test.ts|src/policy-pull.test.ts", + "src/policy-pull-rbac.test.ts|src/revocation-pull.test.ts", + "src/profile/reconcile.test.ts|src/profile/reconcile.test.ts", + "src/rbac/providers-cloud.test.ts|src/rbac/providers-cloud.test.ts", + "src/rbac/providers-cloud.test.ts|src/rbac/rbac.test.ts", + "src/rbac/providers-cloud.ts|src/rbac/providers-cloud.ts", + "src/repomap/graph.ts|src/repomap/graph.ts", + "src/scan-build.test.ts|src/scan-build.test.ts", + "src/scan-build.test.ts|src/scan-staged.test.ts", + "src/scan-build.ts|src/scan-transcripts.ts", + "src/scan-staged.test.ts|src/scan-transcripts.test.ts", + "src/scanners.test.ts|src/scanners.test.ts", + "src/secrets-purge-history.test.ts|src/secrets-purge-history.test.ts", + "src/secrets-sync.ts|src/secrets-sync.ts", + "src/security-prescan.test.ts|src/security-prescan.test.ts", + "src/self-audit-ci.test.ts|src/self-audit-ci.test.ts", + "src/self-audit-ci.ts|src/self-audit.ts", + "src/self-audit-docs.test.ts|src/self-audit-docs.test.ts", + "src/self-audit-docs.test.ts|src/self-audit-wiring.test.ts", + "src/self-audit-docs.ts|src/self-audit-docs.ts", + "src/self-audit-docs.ts|src/self-audit-wiring.ts", + "src/self-audit-docs.ts|src/self-audit.ts", + "src/self-audit.test.ts|src/self-audit.test.ts", + "src/self-audit.ts|src/self-audit.ts", + "src/service-adapter.test.ts|src/service-adapter.test.ts", + "src/skill-run.test.ts|src/skill-run.test.ts", + "src/skill/adherence.test.ts|src/skill/adherence.test.ts", + "src/skipped-commits.test.ts|src/skipped-commits.test.ts", + "src/stack-detector.test.ts|src/stack-detector.test.ts", + "src/stack-detector.ts|src/stack-detector.ts", + "src/standards-plugins-exec.test.ts|src/standards-plugins.test.ts", + "src/triage-ecosystem-parity.test.ts|src/triage-repo-host.test.ts", + "src/triage-repo-host.test.ts|src/triage-repo-host.test.ts", + "src/update-check.test.ts|src/update-check.test.ts", + "src/usage-report.test.ts|src/usage-report.test.ts", + "src/usage-report.ts|src/usage-report.ts", + "src/utils/promptMultiSelect.test.ts|src/utils/promptSelect.test.ts", + "templates/bitbucket/bitbucket-pipelines.yml|templates/bitbucket/bitbucket-pipelines.yml", + "templates/github/kit-security.yml|templates/github/kit-security.yml" + ], + "size": [ + "CHANGELOG.md", + "README.md", + "SECURITY-SCANNING.md", + "SECURITY.md", + "contracts/kit.opencli.json", + "docs/ADAPTER_PATTERNS.md", + "docs/PLUGIN_DOCUMENTATION_STANDARDS.md", + "docs/plans/2026-06-21-kit-health-v1a.md", + "skills/triage/scripts/triage.py", + "src/agent-config.test.ts", + "src/agent-config.ts", + "src/audit-anchor.test.ts", + "src/audit-anchor.ts", + "src/audit.test.ts", + "src/audit.ts", + "src/check-security.test.ts", + "src/check-security.ts", + "src/check-standards-specific.ts", + "src/cli.test.ts", + "src/cli.ts", + "src/commands/memory.ts", + "src/commands/secrets.ts", + "src/commands/security.ts", + "src/commands/setup.ts", + "src/commands/triage.ts", + "src/config.ts", + "src/context-lock.ts", + "src/doctor.test.ts", + "src/doctor.ts", + "src/exec-broker/broker.test.ts", + "src/flag-surface.ts", + "src/governance-middleware.test.ts", + "src/hooks.test.ts", + "src/install-gate.test.ts", + "src/install-gate.ts", + "src/mcp-server.test.ts", + "src/mcp-server.ts", + "src/memory/db.test.ts", + "src/memory/db.ts", + "src/memory/shared.test.ts", + "src/policy-gate.test.ts", + "src/secret-backends.ts", + "src/security-prescan.ts", + "src/self-audit-docs.test.ts", + "src/self-audit-docs.ts", + "src/self-audit.test.ts", + "src/self-audit.ts", + "src/stack-detector.test.ts", + "src/stack-detector.ts" + ], + "specific/typescript": [ + "typescript/eslint:examples/cdn-cache-headers.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:examples/cdn-cache-headers.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:examples/cdn-cache-headers.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:examples/cdn-cache-headers.ts#@typescript-eslint/no-require-imports", + "typescript/eslint:examples/mcp-tools-usage.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:examples/mcp-tools-usage.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:examples/mcp-tools-usage.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:examples/mcp-tools-usage.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:examples/team-management-workflows.ts#@typescript-eslint/no-unused-vars", + "typescript/eslint:examples/team-management-workflows.ts#@typescript-eslint/no-unused-vars", + "typescript/eslint:packages/kit-plugin-sentrux/src/scan.ts#preserve-caught-error", + "typescript/eslint:packages/kit-plugin-snyk/src/scan.ts#preserve-caught-error", + "typescript/eslint:packages/kit-plugin-supabase/src/rotate.ts#@typescript-eslint/no-unused-vars", + "typescript/eslint:src/adapters/ingest.ts#complexity", + "typescript/eslint:src/adapters/searxng-instance.ts#max-depth", + "typescript/eslint:src/adapters/searxng-instance.ts#max-depth", + "typescript/eslint:src/adapters/searxng-instance.ts#max-depth", + "typescript/eslint:src/adapters/searxng-instance.ts#max-depth", + "typescript/eslint:src/adapters/searxng-instance.ts#max-depth", + "typescript/eslint:src/adapters/searxng-instance.ts#max-lines-per-function", + "typescript/eslint:src/adapters/vercel-hosting.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/adr.ts#complexity", + "typescript/eslint:src/adr.ts#max-depth", + "typescript/eslint:src/adr.ts#max-depth", + "typescript/eslint:src/adr.ts#max-params", + "typescript/eslint:src/advisory-baseline.ts#complexity", + "typescript/eslint:src/agent-audit.ts#max-params", + "typescript/eslint:src/agent-config.ts#max-lines", + "typescript/eslint:src/audit-anchor-all.ts#complexity", + "typescript/eslint:src/audit-anchor.ts#complexity", + "typescript/eslint:src/audit-anchor.ts#complexity", + "typescript/eslint:src/audit.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/audit.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/browser.ts#max-lines-per-function", + "typescript/eslint:src/check-attestation.ts#max-lines-per-function", + "typescript/eslint:src/check-design.ts#complexity", + "typescript/eslint:src/check-design.ts#max-depth", + "typescript/eslint:src/check-lock.ts#complexity", + "typescript/eslint:src/check-run.ts#complexity", + "typescript/eslint:src/check-secrets.ts#complexity", + "typescript/eslint:src/check-security.ts#complexity", + "typescript/eslint:src/check-security.ts#complexity", + "typescript/eslint:src/check-security.ts#max-depth", + "typescript/eslint:src/check-security.ts#max-lines", + "typescript/eslint:src/check-services.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/check-services.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/check-web-search.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/check-web-search.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/check-web-search.ts#complexity", + "typescript/eslint:src/check-web-search.ts#max-lines-per-function", + "typescript/eslint:src/cli.ts#complexity", + "typescript/eslint:src/cli.ts#max-depth", + "typescript/eslint:src/cli.ts#max-depth", + "typescript/eslint:src/cli.ts#max-lines", + "typescript/eslint:src/cli.ts#max-lines-per-function", + "typescript/eslint:src/cli.ts#max-lines-per-function", + "typescript/eslint:src/commands/agent.ts#complexity", + "typescript/eslint:src/commands/audit.ts#complexity", + "typescript/eslint:src/commands/audit.ts#max-depth", + "typescript/eslint:src/commands/browser.ts#complexity", + "typescript/eslint:src/commands/check.ts#complexity", + "typescript/eslint:src/commands/check.ts#max-lines-per-function", + "typescript/eslint:src/commands/check.ts#max-lines-per-function", + "typescript/eslint:src/commands/ci.ts#max-lines-per-function", + "typescript/eslint:src/commands/ci.ts#max-lines-per-function", + "typescript/eslint:src/commands/config.ts#max-params", + "typescript/eslint:src/commands/context.ts#complexity", + "typescript/eslint:src/commands/coverage.ts#complexity", + "typescript/eslint:src/commands/coverage.ts#complexity", + "typescript/eslint:src/commands/coverage.ts#complexity", + "typescript/eslint:src/commands/hooks.ts#complexity", + "typescript/eslint:src/commands/info.ts#complexity", + "typescript/eslint:src/commands/mcp.ts#complexity", + "typescript/eslint:src/commands/memory.ts#complexity", + "typescript/eslint:src/commands/memory.ts#complexity", + "typescript/eslint:src/commands/memory.ts#max-lines", + "typescript/eslint:src/commands/memory.ts#max-lines-per-function", + "typescript/eslint:src/commands/profile.ts#complexity", + "typescript/eslint:src/commands/scan.ts#complexity", + "typescript/eslint:src/commands/scan.ts#max-lines-per-function", + "typescript/eslint:src/commands/secrets.ts#complexity", + "typescript/eslint:src/commands/secrets.ts#complexity", + "typescript/eslint:src/commands/secrets.ts#complexity", + "typescript/eslint:src/commands/secrets.ts#complexity", + "typescript/eslint:src/commands/secrets.ts#max-lines", + "typescript/eslint:src/commands/secrets.ts#max-lines-per-function", + "typescript/eslint:src/commands/secrets.ts#max-lines-per-function", + "typescript/eslint:src/commands/security.ts#complexity", + "typescript/eslint:src/commands/security.ts#complexity", + "typescript/eslint:src/commands/security.ts#complexity", + "typescript/eslint:src/commands/security.ts#max-lines", + "typescript/eslint:src/commands/security.ts#max-lines-per-function", + "typescript/eslint:src/commands/security.ts#max-lines-per-function", + "typescript/eslint:src/commands/setup.ts#complexity", + "typescript/eslint:src/commands/setup.ts#complexity", + "typescript/eslint:src/commands/setup.ts#max-lines", + "typescript/eslint:src/commands/setup.ts#max-lines-per-function", + "typescript/eslint:src/commands/setup.ts#max-lines-per-function", + "typescript/eslint:src/commands/tools.ts#complexity", + "typescript/eslint:src/commands/triage.ts#complexity", + "typescript/eslint:src/commands/triage.ts#complexity", + "typescript/eslint:src/commands/triage.ts#complexity", + "typescript/eslint:src/commands/triage.ts#max-lines-per-function", + "typescript/eslint:src/commands/usage.test.ts:46", + "typescript/eslint:src/commands/usage.ts:41", + "typescript/eslint:src/config.ts#max-lines", + "typescript/eslint:src/context-lock.ts#complexity", + "typescript/eslint:src/database.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/environment.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/environment.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/environment.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/environment.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/environment.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/environment.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/environment.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/environment.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/environment.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/environment.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/environment.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/environment.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/environment.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/environment.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/environment.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/environment.ts#complexity", + "typescript/eslint:src/fix.ts#complexity", + "typescript/eslint:src/fix.ts#complexity", + "typescript/eslint:src/fix.ts#max-lines-per-function", + "typescript/eslint:src/fix.ts#max-lines-per-function", + "typescript/eslint:src/fix.ts#max-lines-per-function", + "typescript/eslint:src/gha-audit.ts#no-useless-escape", + "typescript/eslint:src/governance-middleware.ts#complexity", + "typescript/eslint:src/governance-middleware.ts#complexity", + "typescript/eslint:src/governance-middleware.ts#max-lines-per-function", + "typescript/eslint:src/heal.ts#max-depth", + "typescript/eslint:src/install-gate.ts#complexity", + "typescript/eslint:src/install-gate.ts#max-lines", + "typescript/eslint:src/mcp-server.ts#max-lines", + "typescript/eslint:src/memory/hook.ts#complexity", + "typescript/eslint:src/memory/merge.ts#complexity", + "typescript/eslint:src/memory/merge.ts#max-lines-per-function", + "typescript/eslint:src/memory/opencode.ts#max-params", + "typescript/eslint:src/memory/parser.ts#complexity", + "typescript/eslint:src/memory/scan.ts#max-params", + "typescript/eslint:src/output.ts#complexity", + "typescript/eslint:src/output.ts#complexity", + "typescript/eslint:src/plugins-cli.ts#complexity", + "typescript/eslint:src/plugins-cli.ts#max-lines-per-function", + "typescript/eslint:src/policy-check.ts#complexity", + "typescript/eslint:src/provision.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/scan-diff.ts#complexity", + "typescript/eslint:src/secrets-rotate-cli.ts#complexity", + "typescript/eslint:src/secrets-rotate-cli.ts#complexity", + "typescript/eslint:src/secrets-rotate-cli.ts#complexity", + "typescript/eslint:src/secrets-rotate-cli.ts#max-lines-per-function", + "typescript/eslint:src/secrets-rotate-cli.ts#max-lines-per-function", + "typescript/eslint:src/secrets-sync.ts#max-params", + "typescript/eslint:src/secrets-validate.ts#complexity", + "typescript/eslint:src/secrets-vault-migrate.ts#complexity", + "typescript/eslint:src/secrets.test.ts#@typescript-eslint/no-explicit-any", + "typescript/eslint:src/self-audit.ts#max-depth", + "typescript/eslint:src/self-audit.ts#max-depth", + "typescript/eslint:src/self-audit.ts#max-depth", + "typescript/eslint:src/self-audit.ts#max-depth", + "typescript/eslint:src/self-audit.ts#max-lines", + "typescript/eslint:src/service-registry.ts#complexity", + "typescript/eslint:src/standards-plugins-exec.ts#max-depth", + "typescript/eslint:src/standards-plugins.ts#complexity", + "typescript/eslint:src/standards-plugins.ts#complexity", + "typescript/eslint:src/standards-run.ts#complexity", + "typescript/eslint:src/status.ts#complexity", + "typescript/eslint:src/toml-generator.ts#complexity", + "typescript/eslint:src/tool-provenance.ts#complexity", + "typescript/eslint:src/usage-report.ts#complexity", + "typescript/eslint:src/utils/shellSplit.ts#complexity" + ] } }, - "generated": "2026-07-31T22:34:41.913Z", + "generated": "2026-08-30T09:57:43.077Z", "version": 1 } diff --git a/examples/cdn-cache-headers.ts b/examples/cdn-cache-headers.ts index 85c64d44..9835a252 100644 --- a/examples/cdn-cache-headers.ts +++ b/examples/cdn-cache-headers.ts @@ -5,6 +5,7 @@ * to optimize CloudFront caching behavior. */ +import { createHash } from "node:crypto"; import { NextRequest, NextResponse } from "next/server"; // ─── Public API Endpoints (Cacheable) ─────────────────────────────────── @@ -232,7 +233,7 @@ export async function refreshToken(req: NextRequest) { // ─── Utility Functions ──────────────────────────────────────────────── function generateETag(data: any): string { - const hash = require("crypto").createHash("md5").update(JSON.stringify(data)).digest("hex"); + const hash = createHash("md5").update(JSON.stringify(data)).digest("hex"); return `"${hash}"`; } diff --git a/examples/team-management-workflows.ts b/examples/team-management-workflows.ts index 75d21ea0..f8ca610d 100644 --- a/examples/team-management-workflows.ts +++ b/examples/team-management-workflows.ts @@ -10,7 +10,6 @@ import { addTeamMember, removeTeamMember, updateMemberRole, - getTeam, listTeamMembers, grantPermission, revokePermission, @@ -43,7 +42,7 @@ async function createTeamWithMembers() { // Add members const members = ["user-003", "user-004", "user-005"]; members.forEach((userId) => { - const result = addTeamMember(team.team.id, userId, "member"); + addTeamMember(team.team.id, userId, "member"); console.log(`Member added: ${userId}`); }); diff --git a/packages/kit-plugin-sentrux/src/scan.ts b/packages/kit-plugin-sentrux/src/scan.ts index 2a346cd8..3c46a63e 100644 --- a/packages/kit-plugin-sentrux/src/scan.ts +++ b/packages/kit-plugin-sentrux/src/scan.ts @@ -61,7 +61,11 @@ export function parseSentruxJson(text: string): SentruxResult { try { parsed = JSON.parse(text); } catch (err) { - throw new Error(`Invalid Sentrux JSON: ${err instanceof Error ? err.message : err}`); + // `cause` keeps the parser's own diagnostic (offset, token) attached: a scanner that + // loses WHY it could not read its input turns "cannot verify" into an opaque failure. + throw new Error(`Invalid Sentrux JSON: ${err instanceof Error ? err.message : err}`, { + cause: err, + }); } if (!parsed || typeof parsed !== "object") { return { gatePassed: true, metrics: {}, violations: [] }; diff --git a/packages/kit-plugin-snyk/src/scan.ts b/packages/kit-plugin-snyk/src/scan.ts index a9f6df71..4ac2e9ae 100644 --- a/packages/kit-plugin-snyk/src/scan.ts +++ b/packages/kit-plugin-snyk/src/scan.ts @@ -55,7 +55,11 @@ export function parseSnykJson(text: string): SnykResult[] { try { parsed = JSON.parse(text); } catch (err) { - throw new Error(`Invalid Snyk JSON: ${err instanceof Error ? err.message : err}`); + // See sentrux/scan.ts: the underlying parse error is the only thing that says WHERE + // the output went wrong, and a scanner must not discard it. + throw new Error(`Invalid Snyk JSON: ${err instanceof Error ? err.message : err}`, { + cause: err, + }); } // multi-project: Snyk returns an array of result objects. if (Array.isArray(parsed)) { diff --git a/packages/kit-plugin-supabase/src/rotate.ts b/packages/kit-plugin-supabase/src/rotate.ts index fd22cae6..b43d4977 100644 --- a/packages/kit-plugin-supabase/src/rotate.ts +++ b/packages/kit-plugin-supabase/src/rotate.ts @@ -12,7 +12,6 @@ import { makeClient, rollJwtSecret, mintScopedKey, - listApiKeys, detectKeyMode, type MgmtClient, type RotateMode, diff --git a/src/check-standards.test.ts b/src/check-standards.test.ts index 50cfa388..927630b0 100644 --- a/src/check-standards.test.ts +++ b/src/check-standards.test.ts @@ -191,9 +191,20 @@ describe("check-standards — checkStandards gating", () => { describe("check-standards — collectStandardsKeys", () => { it("returns empty slices when the general tools are absent (nothing to freeze)", async () => { - // lizard/jscpd/scc are not installed in this environment → didNotRun → empty. - const keys = await collectStandardsKeys(process.cwd()); - assert.deepEqual(keys, { complexity: [], duplication: [], size: [] }); + // The tools are FORCED absent rather than assumed absent. This test previously read + // "lizard/jscpd/scc are not installed in this environment", which made its green depend + // on the machine: provisioning lizard and jscpd turned it red without any code changing. + // A test whose pass condition is "the box happens to be missing something" proves nothing + // on a box that has it. Emptying PATH makes the scan's spawns fail to resolve on every + // machine, which is the condition the assertion is actually about. + const realPath = process.env.PATH; + process.env.PATH = ""; + try { + const keys = await collectStandardsKeys(process.cwd()); + assert.deepEqual(keys, { complexity: [], duplication: [], size: [] }); + } finally { + process.env.PATH = realPath; + } }); }); diff --git a/src/commands/usage.test.ts b/src/commands/usage.test.ts index 341e730c..743c2328 100644 --- a/src/commands/usage.test.ts +++ b/src/commands/usage.test.ts @@ -43,7 +43,7 @@ describe("renderTab", () => { const widths = new Set( rendered .split("\n") - // eslint-disable-next-line no-control-regex + .map((l) => l.replace(/\x1b\[[0-9;]*m/g, "").length), ); assert.equal( diff --git a/src/commands/usage.ts b/src/commands/usage.ts index 00fc2d32..f39235c7 100644 --- a/src/commands/usage.ts +++ b/src/commands/usage.ts @@ -38,7 +38,7 @@ const WIDTH = 66; /** Visible length: padding maths has to ignore the colour escapes, or the box comes out ragged. */ function plain(s: string): string { - // eslint-disable-next-line no-control-regex + return s.replace(/\x1b\[[0-9;]*m/g, ""); } From 4bd1e043eeabf302c3af901bad4849601690fd07 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 11:34:20 +0000 Subject: [PATCH 4/6] feat: say what a green check covers, enforce the dependency floor, and draw the model-rig boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes for three gaps between what kit claims and what kit enforces. **1. `kit check` now states its scope next to its verdict.** scope: 42 check(s) — 41 inspect the code, 1 executes it. A pass here does not cover runtime behaviour; `kit broker` is that tier. Counted from the checks that ran, not asserted, so it cannot go stale as categories are added. The reason it matters is measured, not stylistic: multi-tier verification research (arXiv:2607.00107, 8,918 programs across four tiers) found AI-generated code ~2x as likely as human code to trigger a confirmed runtime violation -- while under STATIC analysis the two appear equally safe, a similarity the authors call misleading. kit's surface is 41/42 static, so on a weak model its green says roughly what it would have said about human-written code. That is not a wrong verdict, it is an uninformative one wearing the same colour as an informative one. Now it says so. Sibling to the existing partial-run line, for the same reason. **2. ADR-0002's dependency floor is enforced for the first time.** Found by breaking it: `kit pkg npm:jscpd` added a fifth runtime dependency and `kit adr check` reported "4 enforced ADR(s) — no new violations". The kit-enforce block is a deny-list of twelve named packages at the import level; the ADR's title says "four runtime deps". It declared more than it enforced. It is not expressible in the grammar, for two structural reasons now recorded in the ADR: package.json is not in the file set the ADR gate walks (CODE_EXTS is source extensions only), and forbid/require_pattern match LINE BY LINE, so a `dependencies` entry cannot be told apart from a `devDependencies` one. Widening the walk and adding block-aware matching to serve one rule is a bigger change than the rule is worth, so the floor is enforced by src/dependency-floor.test.ts -- exact dependency set, exact versions, no ranges. The kit-enforce block stays as the cheaper first line. **3. ADR-0007 draws the line for the model + kit residual-risk rig.** kit owns the frozen input, the ingest schema, the deterministic adjudication and the receipts; the models run outside and the repository decides whether a finding is real. kit never produces, ranks or grades a judgement. ADR-0001 is untouched -- its forbid_import over src/** is the mechanical half, which is why 0007 carries no rule of its own rather than duplicating one that could drift. Three limits are written into the consequences so they ship with any number: lower bound only, Goodhart, and unproven transfer from C++. Everything mutation-proved rather than eyeballed: a fifth dependency and a floated version each break exactly the assertion that should notice; treating every category as executing breaks three tier-notice tests; dropping the `kit broker` pointer breaks exactly the one about naming the remedy. --- AGENTS.md | 11 +- CLAUDE.md | 11 +- docs/adr/0002-dependency-floor.md | 35 ++++++ ...07-kit-owns-the-rig-never-the-judgement.md | 114 ++++++++++++++++++ src/cli-checks-shared.ts | 37 ++++++ src/commands/check.ts | 12 ++ src/dependency-floor.test.ts | 76 ++++++++++++ src/tier-notice.test.ts | 47 ++++++++ 8 files changed, 339 insertions(+), 4 deletions(-) create mode 100644 docs/adr/0007-kit-owns-the-rig-never-the-judgement.md create mode 100644 src/dependency-floor.test.ts create mode 100644 src/tier-notice.test.ts diff --git a/AGENTS.md b/AGENTS.md index a9ac54ad..c9a02406 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,15 +17,22 @@ This repo is managed by [kit](https://github.com/sandstream/kit) (env, secrets, ## Architecture decisions are a gate, and `kit check` does not run it -`docs/adr` holds six ADRs. The four accepted ones carry a `kit-enforce` block, which -makes them five deterministic rules — not prose: +`docs/adr` holds seven ADRs. The four accepted-and-enforced ones carry a `kit-enforce` +block, which makes them five deterministic rules — not prose: - **ADR-0001** no model-client import anywhere in `src/**` (the zero-LLM core). - **ADR-0002** no new runtime dependency from the forbidden list — stdlib otherwise. + The *floor itself* (exactly four runtime deps, all pinned) is **not** expressible in the + `kit-enforce` grammar and is enforced by `src/dependency-floor.test.ts`; the ADR says why. - **ADR-0003** the check path imports no coverage-framework mappings. - **ADR-0006** `src/utils/**` imports nothing from the repo — derived by `kit adr derive` from an asymmetry the code had obeyed for months, then accepted. +Three more are accepted but **documented, not enforced** — ADR-0004 (workflow skills live +above kit), ADR-0005 (browser substrate), and **ADR-0007** (kit may measure model + kit +residual risk, but owns only the rig — the frozen input, ingest schema, deterministic +adjudication and receipts — never the judgement). + `node dist/cli.js adr check` runs them and **fails CI hard** on a violation. `kit check` does **not** include the ADR stage — only `kit review` (check + design + standards + adr + skill) does. So before opening a PR that adds a dependency, moves an import, or touches diff --git a/CLAUDE.md b/CLAUDE.md index f8f3a5cd..7eadeca5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,15 +30,22 @@ This repo is managed by [kit](https://github.com/sandstream/kit) (env, secrets, ## Architecture decisions are a gate, and `kit check` does not run it -`docs/adr` holds six ADRs. The four accepted ones carry a `kit-enforce` block, which -makes them five deterministic rules — not prose: +`docs/adr` holds seven ADRs. The four accepted-and-enforced ones carry a `kit-enforce` +block, which makes them five deterministic rules — not prose: - **ADR-0001** no model-client import anywhere in `src/**` (the zero-LLM core). - **ADR-0002** no new runtime dependency from the forbidden list — stdlib otherwise. + The *floor itself* (exactly four runtime deps, all pinned) is **not** expressible in the + `kit-enforce` grammar and is enforced by `src/dependency-floor.test.ts`; the ADR says why. - **ADR-0003** the check path imports no coverage-framework mappings. - **ADR-0006** `src/utils/**` imports nothing from the repo — derived by `kit adr derive` from an asymmetry the code had obeyed for months, then accepted. +Three more are accepted but **documented, not enforced** — ADR-0004 (workflow skills live +above kit), ADR-0005 (browser substrate), and **ADR-0007** (kit may measure model + kit +residual risk, but owns only the rig — the frozen input, ingest schema, deterministic +adjudication and receipts — never the judgement). + `node dist/cli.js adr check` runs them and **fails CI hard** on a violation. `kit check` does **not** include the ADR stage — only `kit review` (check + design + standards + adr + skill) does. So before opening a PR that adds a dependency, moves an import, or touches diff --git a/docs/adr/0002-dependency-floor.md b/docs/adr/0002-dependency-floor.md index ea278d7d..1a000d81 100644 --- a/docs/adr/0002-dependency-floor.md +++ b/docs/adr/0002-dependency-floor.md @@ -20,6 +20,41 @@ Adding a runtime dependency is an ADR-level decision, not a convenience call. The rule below blocks the common utility imports outright; anything else new must argue its case in a PR that updates this ADR. +## Where the floor is actually enforced — and why not here + +**Amended 2026-08-29.** This ADR declared more than its `kit-enforce` block enforced, and +the gap was found the only way such gaps are found: by breaking the rule and watching the +gate stay green. `kit pkg npm:jscpd` added a fifth runtime dependency to `package.json`, +and `kit adr check` reported `✓ 4 enforced ADR(s) — no new violations`. + +The block below is a **deny-list of twelve named packages, matched at the import level in +`src/**`**. It cannot see a dependency that is not on the list, and it cannot see one that +has been added to the manifest but not yet imported. The title says "four runtime deps"; +the rule says "not these twelve". + +The floor is **not expressible in the `kit-enforce` grammar**, for two structural reasons: + +1. `package.json` is not in the file set the ADR gate walks — `CODE_EXTS` in + `src/commands/adr.ts` lists source extensions only, so no rule here can ever apply to a + manifest. +2. `forbid_pattern` and `require_pattern` are matched **line by line** + (`firstMatchingLine` splits on newlines). An entry in `dependencies` is textually + identical to one in `devDependencies`, so a line-based regex cannot tell them apart, and + a multi-line pattern pinning the whole block cannot match at all. + +Widening the walk and adding block-aware matching to serve one rule is a larger change than +the rule is worth. So the floor is enforced by **`src/dependency-floor.test.ts`**, which +asserts the exact runtime dependency set and that every version is pinned rather than a +range. It runs in the same CI job as everything else and fails the moment either claim +stops being true. + +The `kit-enforce` block stays: it catches the utility-import case earlier and more cheaply, +in review rather than in the suite. It is a first line, not the line. + +**A dev tool kit shells out to — a scanner, a linter — is not a runtime dependency.** It is +installed as a tool and must never be added to `dependencies`; that mistake is what exposed +this gap. + ```toml kit-enforce [[forbid_import]] import = "^(lodash|lodash-es|lodash\\.|underscore|ramda|axios|node-fetch|request|moment|dayjs|bluebird|jquery)" diff --git a/docs/adr/0007-kit-owns-the-rig-never-the-judgement.md b/docs/adr/0007-kit-owns-the-rig-never-the-judgement.md new file mode 100644 index 00000000..5b2d32e0 --- /dev/null +++ b/docs/adr/0007-kit-owns-the-rig-never-the-judgement.md @@ -0,0 +1,114 @@ +--- +id: ADR-0007 +title: kit owns the rig, never the judgement — measuring model + kit residual risk +status: accepted +--- + +# ADR-0007: kit owns the rig, never the judgement + +## Decision + +kit may measure how well a model performs **together with kit's gates**, and may publish +that measurement. Specifically, kit owns: + +- the **frozen input** — a pinned commit plus a pinned prompt/rubric, content-hashed, so + two runs are answering the same question; +- the **ingest schema** — a model's findings arrive as structured rows (file, line, claim, + severity) attributed to model, version and date; +- the **deterministic adjudication** — whether a claimed defect is real is decided by the + repository (test suite, mutation harness, broker denials), never by a model; +- the **arithmetic** — overlap, divergence, caught/missed counts, drift across runs; +- the **receipts** — the same signed, replayable evidence trail every other kit verdict + carries. + +kit does **not** produce a review, rank models by preference, score a judgement's quality, +or call a model at any point. The models run outside kit and hand their output in. + +## Rationale — this is about kit's own residual risk, not benchmarking + +The earlier framing was that model-vs-model comparison is mush because the judge is +another model. True, but it made this look like a nice-to-have. The real motive is +narrower and more uncomfortable. + +**kit's green is a claim about kit's floor, and readers take it as a claim about the +work.** Measured on this repo: `kit check` runs 42 checks, of which **41 inspect the code +and 1 executes it**. kit lives almost entirely in the static tier. + +Multi-tier verification research ([arXiv:2607.00107](https://arxiv.org/abs/2607.00107) — +8,918 C++ programs across 851 tasks, four tiers, three models plus human-authored code) +found: + +> *"AI-generated code is roughly twice as likely as human code to trigger a confirmed +> runtime violation, even after controlling for code length and test pass-rate."* + +and, critically, that under **static analysis the two appear equally safe** — a similarity +the authors call misleading. The tiers detect largely different classes of violation; no +single tier suffices. + +So on a weak model, `kit check` passing says approximately what it would have said about +human-written code. That is not a false verdict, it is an **uninformative** one presented +in the same colour as an informative one. Without a residual-risk number we cannot say +what our own green is worth — and a governance tool that cannot say that is asserting +trust it has not earned. + +## Why this does not break ADR-0001 + +The zero-LLM contract forbids a model call **in the gate loop**, because that would make +the verdict probabilistic, add egress to the trust boundary, and put a prompt-injection +surface inside the security tool. None of that applies here: + +| step | actor | deterministic? | +|---|---|---| +| produce the work | the model, outside kit | no — and it is not a verdict | +| run the gates | kit | yes | +| decide whether a finding is real | the **repository** | yes | +| count, difference, report | kit | yes | + +No SDK import in `src/**` — which ADR-0001's `forbid_import` rule already enforces, and +which is the mechanical half of this decision. No model judgement decides a pass or fail. +The output is reproducible from the frozen input. + +**The precedent is already shipped.** `kit triage` scores a third-party package +deterministically, publishes a claim about code kit does not own, and gates an install on +it — failing closed when it cannot verify rather than passing on thin evidence. This is +`kit triage` pointed at the agent instead of the package. + +## Relationship to ADR-0004 + +ADR-0004 rules that model-shaped work — grilling, specs, review framing — lives above +kit, and that a feature needing a model judgement to decide pass/fail stays out. This ADR +does not weaken that. It draws the line one notch more precisely: + +> **Producing or grading a judgement is model-shaped and stays out. Framing the question, +> receiving the answer, and adjudicating it against the repository is substrate, and is +> kit's.** + +If those two ever conflict in practice, ADR-0004 wins and the rig is wrong. + +## Consequences — three limits that ship with the number, not with its documentation + +Any residual-risk figure kit emits must carry these in its own output. Without them the +measurement becomes the false comfort it was built to remove. + +1. **Lower bound only.** You can measure only what you can seed. Defects nobody thought to + inject are invisible. The number is a floor on risk, never a ceiling, and must never be + phrased as a safety score. +2. **Goodhart.** The moment a defect corpus exists it becomes the thing that gets + optimised against. It has to rotate, and it must never be published as a benchmark. +3. **Transfer is unproven.** The 2× above is C++ competitive programming. The direction + transfers — static analysis cannot see what execution can — the constant does not, and + quoting it as kit's own figure would be borrowed precision. + +Also: **`kit check` now states its scope next to its verdict** (`tierNotice`, +`src/cli-checks-shared.ts`), counted from the checks that ran rather than asserted, so the +narrowness above is visible without needing this rig at all. That was the cheap half of +the fix and it shipped first. + +## Status of enforcement + +This ADR carries **no `kit-enforce` block**, and that is deliberate rather than an +oversight: its mechanical half is already ADR-0001's `forbid_import` over `src/**`, and +duplicating it here would give two rules that can drift apart. What this ADR adds is a +boundary that a human applies in review — which is why it is listed as *documented, not +enforced*, and why building the rig before this file existed would have been the erosion +it guards against. diff --git a/src/cli-checks-shared.ts b/src/cli-checks-shared.ts index b50e29f7..2bea326e 100644 --- a/src/cli-checks-shared.ts +++ b/src/cli-checks-shared.ts @@ -17,6 +17,43 @@ export const KIT_VERSION = ( JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")) as { version: string } ).version; +/** + * Check categories whose verdict comes from EXECUTING the code under test rather than + * reading it. Everything else — secret scans, dependency audits, import rules, manifest + * checks, registry queries — is static: it inspects text and metadata. + * + * The distinction is not pedantry. Multi-tier verification research (arXiv:2607.00107, + * 8,918 programs across four tiers) found AI-generated code roughly twice as likely as + * human code to trigger a confirmed runtime violation — while under STATIC analysis the + * two appear equally safe, a similarity the authors call misleading. The tiers catch + * largely different classes of defect. + * + * kit's check surface is almost entirely the tier that cannot tell those apart. That does + * not make its green wrong; it makes it NARROWER than a reader assumes, and a gate whose + * scope is assumed rather than stated is the failure mode this repo keeps finding. So the + * scope gets printed next to the verdict, counted rather than claimed, so it cannot go + * stale as categories are added. + */ +export const EXECUTING_CATEGORIES: readonly string[] = ["tests"]; + +/** + * One line naming what a green verdict covers. Returns "" for an empty run — there is no + * scope to state when nothing ran, and the caller already says so. + */ +export function tierNotice(checks: readonly JsonCheck[]): string { + if (checks.length === 0) return ""; + const executing = checks.filter((k) => EXECUTING_CATEGORIES.includes(k.category)).length; + const stat = checks.length - executing; + const runtime = + executing === 0 + ? "none execute the code" + : `${executing} execute${executing === 1 ? "s" : ""} it`; + return ( + `scope: ${checks.length} check(s) — ${stat} inspect the code, ${runtime}. ` + + `A pass here does not cover runtime behaviour; \`kit broker\` is that tier.` + ); +} + /** One check row in the machine-readable `kit check --json` / `kit ci --json` output. */ export interface JsonCheck { name: string; diff --git a/src/commands/check.ts b/src/commands/check.ts index dbd922d8..e1ed367b 100644 --- a/src/commands/check.ts +++ b/src/commands/check.ts @@ -36,6 +36,7 @@ import { syncSecurityFindings } from "../findings-track.js"; import { collectHints } from "../hints.js"; import { KIT_VERSION, + tierNotice, type JsonCheck, type JsonCheckOutput, autoInstallScanners, @@ -271,6 +272,17 @@ export async function cmdCheck(): Promise { }), ); + // What the verdict COVERS, counted from the checks that ran. Sibling to the + // partial-run line below and for the same reason: a scope the reader has to assume + // is a scope that gets assumed wrong. kit's surface is almost all static analysis, + // which is the tier that provably cannot distinguish AI-written code from + // human-written code (see EXECUTING_CATEGORIES) — so it says so rather than letting + // a green stand in for more than it checked. + { + const notice = tierNotice(checkRunToJsonChecks(run)); + if (notice) console.log(`${c.dim}${notice}${c.reset}`); + } + // A narrowed run must say so next to its verdict. Without this line a // `--category security` pass is visually identical to a full green. if (run.scope) { diff --git a/src/dependency-floor.test.ts b/src/dependency-floor.test.ts new file mode 100644 index 00000000..eb787244 --- /dev/null +++ b/src/dependency-floor.test.ts @@ -0,0 +1,76 @@ +/** + * ADR-0002's claim, enforced. + * + * WHY THIS FILE EXISTS. ADR-0002 is titled "Dependency floor — four runtime deps, stdlib + * otherwise", and CLAUDE.md lists it among kit's deterministic rules. Its `kit-enforce` + * block enforces something narrower: a `forbid_import` deny-list of twelve named packages + * (lodash, axios, moment, …) checked in `src/**`. Measured — by accidentally adding a + * fifth runtime dependency and watching the gate stay green — a new dependency that is not + * on that list passes silently. The ADR declared more than it enforced. + * + * WHY NOT FIX IT IN THE ADR GATE. Two structural reasons, both measured rather than + * assumed: + * + * 1. `package.json` is not in the file set the ADR gate walks. `CODE_EXTS` in + * `commands/adr.ts` is source extensions only, so no `kit-enforce` rule can ever apply + * to a manifest. + * 2. `forbid_pattern` / `require_pattern` are matched LINE BY LINE (`firstMatchingLine` + * splits on newlines). A dependency entry in `dependencies` is textually identical to + * one in `devDependencies`, so a line-based regex cannot tell them apart, and a + * multi-line pattern pinning the whole block cannot match at all. + * + * Widening the walk and adding block-aware matching to serve one rule is a larger change + * than the rule is worth. A test is the smaller, honest mechanism: it is deterministic, it + * runs in CI already, and it fails the moment the claim stops being true. + * + * ADDING A DEPENDENCY IS AN ADR-LEVEL ACT. If a fifth is genuinely needed, amend or + * supersede ADR-0002 in the same PR and update the list below. Editing this list alone to + * make the suite green is the failure this test exists to catch. + */ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); + +/** The floor ADR-0002 declares. Changing this set requires amending that ADR. */ +const RUNTIME_DEPENDENCIES = [ + "@modelcontextprotocol/sdk", + "@upstash/redis", + "smol-toml", + "zod", +] as const; + +function manifest(): { dependencies?: Record } { + return JSON.parse(readFileSync(join(REPO_ROOT, "package.json"), "utf-8")) as { + dependencies?: Record; + }; +} + +describe("ADR-0002 dependency floor", () => { + it("ships exactly the declared runtime dependencies — no more, no fewer", () => { + const declared = Object.keys(manifest().dependencies ?? {}).sort(); + assert.deepEqual( + declared, + [...RUNTIME_DEPENDENCIES].sort(), + "package.json's runtime dependencies drifted from ADR-0002's floor. A new runtime " + + "dependency is an architecture decision: amend or supersede docs/adr/0002-dependency-floor.md " + + "in the same PR. Note that a dev TOOL kit shells out to (a scanner, a linter) is not a " + + "runtime dependency and must not be added here — install it as a tool.", + ); + }); + + it("pins every runtime dependency to an exact version", () => { + // A range lets the floor move without anyone editing package.json, which would make + // the assertion above true and the claim behind it false. + for (const [name, range] of Object.entries(manifest().dependencies ?? {})) { + assert.match( + range, + /^\d+\.\d+\.\d+$/, + `${name} is pinned as "${range}" — ADR-0002's floor is only meaningful if the versions cannot float`, + ); + } + }); +}); diff --git a/src/tier-notice.test.ts b/src/tier-notice.test.ts new file mode 100644 index 00000000..aa00a6a4 --- /dev/null +++ b/src/tier-notice.test.ts @@ -0,0 +1,47 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { tierNotice, EXECUTING_CATEGORIES, type JsonCheck } from "./cli-checks-shared.js"; + +const check = (category: string): JsonCheck => ({ + name: `${category} check`, + status: "pass", + detail: "", + category, +}); + +describe("tierNotice", () => { + it("counts the tiers rather than asserting them, so it cannot go stale", () => { + const notice = tierNotice([ + check("security/secrets"), + check("security/supply-chain"), + check("tests"), + ]); + assert.match(notice, /3 check\(s\)/); + assert.match(notice, /2 inspect the code/); + assert.match(notice, /1 executes it/); + }); + + it("says none execute the code when no executing category ran", () => { + // The case that matters: a run made entirely of static checks must not leave the + // reader thinking anything was actually run. + const notice = tierNotice([check("security/secrets"), check("deploy")]); + assert.match(notice, /2 inspect the code, none execute the code/); + }); + + it("names the runtime tier, so the limitation comes with its remedy", () => { + assert.match(tierNotice([check("tests")]), /kit broker/); + }); + + it("returns nothing for an empty run — there is no scope to state", () => { + assert.equal(tierNotice([]), ""); + }); + + it("derives the split from EXECUTING_CATEGORIES, not from a hardcoded count", () => { + // Proves the notice tracks the constant: every category listed there counts as + // executing, and a category absent from it counts as static. + for (const cat of EXECUTING_CATEGORIES) { + assert.match(tierNotice([check(cat)]), /0 inspect the code, 1 executes it/); + } + assert.match(tierNotice([check("not-a-real-category")]), /1 inspect the code/); + }); +}); From 1e50cdf4b9e26acd5f03fa381594e05f99776db2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 16:17:48 +0000 Subject: [PATCH 5/6] feat(adr): let an ADR name where it is enforced when the grammar cannot hold it Future-proofing for a pattern that showed up three times this week rather than once. Three limits of the `kit-enforce` grammar are now measured, each found by trying to write a rule and failing: - `paths` has no negation, so "every subsystem except src/commands/**" is inexpressible - `forbid_pattern`/`require_pattern` match LINE BY LINE, so a JSON block cannot be pinned and package.json is not in the walked file set anyway - the import extractor is text-level, so a test fixture STRING that looks like an import counts as one Today, when a rule hits one of those, nothing happens. The decision quietly does not get encoded and the ADR goes on declaring more than it enforces. ADR-0002 sat in exactly that state for months: titled "four runtime deps", enforcing "not these twelve imports". The gap was invisible because a narrowly-enforced ADR and a fully-enforced one print identically. So an ADR can now name its real enforcement point: enforced_by: [src/dependency-floor.test.ts] `adr list` prints it under the ADR, so "documented, not enforced" is never confused with "not enforced anywhere". And `adr check` FAILS when a named file does not exist -- which is the half that matters. A pointer nobody verifies is worse than no pointer, because it reads as coverage. Same rule class as ci-adr-gate.test.ts (a gate must be invoked) and self-audit (a workflow's script must exist), applied to enforcement claims. Checked only for `accepted` ADRs: a proposal's pointer is an intention, not yet a claim. Mutation-proved end to end: adding a second, non-existent pointer to ADR-0002 fails `adr check` with the file cited; deleting the real dependency-floor test fails it too; turning the accepted-status guard off, or on for every status, each breaks exactly the one test that should notice. The three grammar limits are now written into CLAUDE.md and AGENTS.md alongside the mechanism, so the next person who cannot express a rule reaches for this instead of dropping it silently. --- AGENTS.md | 8 ++++++ CLAUDE.md | 8 ++++++ docs/COMMANDS.md | 2 +- docs/adr/0002-dependency-floor.md | 1 + src/adr.test.ts | 29 ++++++++++++++++++++ src/adr.ts | 45 ++++++++++++++++++++++++++++++- src/commands/adr.test.ts | 41 ++++++++++++++++++++++++++++ src/commands/adr.ts | 34 +++++++++++++++++++++-- 8 files changed, 164 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c9a02406..24fb572b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,3 +40,11 @@ does. So before opening a PR that adds a dependency, moves an import, or touches Adding one of those imports is an ADR-level decision, not a code change: amend or supersede the ADR in the same PR, or the gate will refuse the code and cite the ADR that refused it. + +**When a decision does not fit the `kit-enforce` grammar, say where it lives instead of +dropping it.** Three limits are measured and will bite again: `paths` has no negation, +`forbid_pattern`/`require_pattern` match line by line, and the import extractor is +text-level (a fixture string that looks like an import counts as one). An ADR in that +position declares `enforced_by: [src/x.test.ts]` in its frontmatter — `adr list` prints it +and `adr check` fails if the file is missing. Silently declaring more than you enforce is +the failure this exists to prevent; ADR-0002 sat that way for months. diff --git a/CLAUDE.md b/CLAUDE.md index 7eadeca5..3ebe0e94 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,3 +53,11 @@ does. So before opening a PR that adds a dependency, moves an import, or touches Adding one of those imports is an ADR-level decision, not a code change: amend or supersede the ADR in the same PR, or the gate will refuse the code and cite the ADR that refused it. + +**When a decision does not fit the `kit-enforce` grammar, say where it lives instead of +dropping it.** Three limits are measured and will bite again: `paths` has no negation, +`forbid_pattern`/`require_pattern` match line by line, and the import extractor is +text-level (a fixture string that looks like an import counts as one). An ADR in that +position declares `enforced_by: [src/x.test.ts]` in its frontmatter — `adr list` prints it +and `adr check` fails if the file is missing. Silently declaring more than you enforce is +the failure this exists to prevent; ADR-0002 sat that way for months. diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 0ede4e6f..a416cfbe 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -98,7 +98,7 @@ port = 3107 | `kit standards [--category general\|specific\|plugins\|platform\|] [--enforce]` | Dev-standards gate: general metrics (complexity/duplication/size via lizard/jscpd/scc) + per-language linters (11 langs) + user plugins (`.kit/standards.d/`) + container (hadolint). Warn by default; `--enforce` fails net-new findings AND setup gaps. | | `kit standards freeze` | Snapshot only the standards dimensions into `.kit-baseline.json`. | | `kit review` | Meta-runner — `check + design + standards + adr + skill` gate for PR. The `skill` stage runs module discipline over every shipped `SKILL.md` (contract, trigger collision, bounded tool scope, snapshot drift); a repo with no skills skips honestly. | -| `kit adr [check\|list\|freeze\|derive]` | ADR → gate: enforce accepted ADRs' `kit-enforce` rules (`forbid_pattern` / `require_pattern` / `forbid_import`, incl. transitive and cross-package via `follow_packages`), cited to the ADR. `list` shows status; `freeze` baselines existing findings; `derive` proposes ADRs the code already obeys (absent import edges with a populated reverse), each re-run through the real evaluator before it is shown and emitted as `status: proposed` so it gates nothing until a human accepts it. Zero-LLM (prose is never interpreted). | +| `kit adr [check\|list\|freeze\|derive]` | ADR → gate: enforce accepted ADRs' `kit-enforce` rules (`forbid_pattern` / `require_pattern` / `forbid_import`, incl. transitive and cross-package via `follow_packages`), cited to the ADR. `list` shows status plus any `enforced_by:` frontmatter pointing at enforcement the grammar cannot express (verified to exist by `check`, so it cannot rot); `freeze` baselines existing findings; `derive` proposes ADRs the code already obeys (absent import edges with a populated reverse), each re-run through the real evaluator before it is shown and emitted as `status: proposed` so it gates nothing until a human accepts it. Zero-LLM (prose is never interpreted). | | `kit baseline [freeze]` | Snapshot current acceptable warnings (incl. standards + ADR) to `.kit-baseline.json`. | | `kit analyze [--write]` | Mine git history + framework markers → draft `CLAUDE.md` / `RULES.md`. | diff --git a/docs/adr/0002-dependency-floor.md b/docs/adr/0002-dependency-floor.md index 1a000d81..bf9dd025 100644 --- a/docs/adr/0002-dependency-floor.md +++ b/docs/adr/0002-dependency-floor.md @@ -2,6 +2,7 @@ id: ADR-0002 title: Dependency floor — four runtime deps, stdlib otherwise status: accepted +enforced_by: [src/dependency-floor.test.ts] --- # ADR-0002: Dependency floor diff --git a/src/adr.test.ts b/src/adr.test.ts index 592c777b..12442cdf 100644 --- a/src/adr.test.ts +++ b/src/adr.test.ts @@ -463,3 +463,32 @@ describe("resolveRelative", () => { assert.equal(resolveRelative("src/web/h.ts", "./nope.js", set), null); }); }); + +describe("parseAdr — enforced_by", () => { + const withFrontmatter = (extra: string): string => + `---\nid: ADR-0099\ntitle: T\nstatus: accepted\n${extra}---\n\n# body\n`; + + it("parses an inline list", () => { + const adr = parseAdr(withFrontmatter("enforced_by: [src/a.test.ts, src/b.test.ts]\n")); + assert.deepEqual(adr?.enforcedBy, ["src/a.test.ts", "src/b.test.ts"]); + }); + + it("parses a block list", () => { + const adr = parseAdr(withFrontmatter("enforced_by:\n - src/a.test.ts\n - src/b.test.ts\n")); + assert.deepEqual(adr?.enforcedBy, ["src/a.test.ts", "src/b.test.ts"]); + }); + + it("strips quotes", () => { + const adr = parseAdr(withFrontmatter(`enforced_by: ["src/a.test.ts"]\n`)); + assert.deepEqual(adr?.enforcedBy, ["src/a.test.ts"]); + }); + + it("is an empty list when absent — the field is optional, never undefined", () => { + // Callers iterate it directly; an undefined here would be a crash at the call site. + assert.deepEqual(parseAdr(withFrontmatter(""))?.enforcedBy, []); + }); + + it("yields nothing rather than a guess for a shape it does not understand", () => { + assert.deepEqual(parseAdr(withFrontmatter("enforced_by: src/a.test.ts\n"))?.enforcedBy, []); + }); +}); diff --git a/src/adr.ts b/src/adr.ts index 8172e32d..9f1eb2d8 100644 --- a/src/adr.ts +++ b/src/adr.ts @@ -66,6 +66,21 @@ export interface Adr { rules: AdrRule[]; /** True when a ```toml kit-enforce block was present (even if it parsed to zero rules). */ hasEnforceBlock: boolean; + /** + * Repo-relative paths that enforce this ADR's claim OUTSIDE the `kit-enforce` block. + * + * Some decisions are not expressible in the block's grammar — measured cases: `paths` + * has no negation, `forbid_pattern`/`require_pattern` match line by line so a JSON + * block cannot be pinned, and the manifest is not in the walked file set at all. Before + * this field the only options were to encode the rule wrong or to leave the ADR + * declaring more than it enforced, silently. ADR-0002 sat in the second state for + * months: titled "four runtime deps", enforcing "not these twelve imports". + * + * Naming the real enforcement point turns that silence into a claim, and a claim can be + * checked — `adr check` fails when a path listed here does not exist, so the pointer + * cannot rot into a lie. + */ + enforcedBy: string[]; } export interface AdrViolation { @@ -91,6 +106,33 @@ function scalar(frontmatter: string, key: string): string | undefined { return m ? m[1].trim().replace(/^["']|["']$/g, "") : undefined; } +/** + * A frontmatter list, in either shape: + * + * enforced_by: [src/a.test.ts, src/b.test.ts] + * enforced_by: + * - src/a.test.ts + * - src/b.test.ts + * + * Deliberately tiny: this is not a YAML parser, it is two shapes an ADR author writes by + * hand. Anything else yields an empty list rather than a guess. + */ +function list(frontmatter: string, key: string): string[] { + const inline = frontmatter.match(new RegExp(`^${key}:[ \\t]*\\[(.*)\\][ \\t]*$`, "mi")); + if (inline) { + return inline[1] + .split(",") + .map((v) => v.trim().replace(/^["']|["']$/g, "")) + .filter(Boolean); + } + const blocked = frontmatter.match(new RegExp(`^${key}:[ \\t]*\\n((?:[ \\t]*-[^\\n]*\\n?)+)`, "mi")); + if (!blocked) return []; + return blocked[1] + .split("\n") + .map((line) => line.replace(/^[ \t]*-[ \t]*/, "").trim().replace(/^["']|["']$/g, "")) + .filter(Boolean); +} + function str(o: Record, k: string): string | undefined { return typeof o[k] === "string" ? (o[k] as string) : undefined; } @@ -112,6 +154,7 @@ export function parseAdr(raw: string): Adr | null { const status: AdrStatus = (STATUSES as string[]).includes(rawStatus) ? (rawStatus as AdrStatus) : "unknown"; + const enforcedBy = list(frontmatter, "enforced_by"); // A fenced ```toml kit-enforce block anywhere in the body. const block = body.match(/```toml\s+kit-enforce\s*\n([\s\S]*?)\n```/); @@ -154,7 +197,7 @@ export function parseAdr(raw: string): Adr | null { rules = []; // malformed TOML → zero rules, but hasEnforceBlock stays true (surfaced) } } - return { id, title, status, rules, hasEnforceBlock }; + return { id, title, status, rules, hasEnforceBlock, enforcedBy }; } function arr(v: unknown): Record[] { diff --git a/src/commands/adr.test.ts b/src/commands/adr.test.ts index 36464ac8..00ae742d 100644 --- a/src/commands/adr.test.ts +++ b/src/commands/adr.test.ts @@ -84,3 +84,44 @@ describe("collectAdrFindings + freezeAdrBaseline (temp repo)", () => { assert.equal(live.length, 0, "the frozen violation is suppressed on re-check"); }); }); + +describe("collectAdrFindings — an enforced_by pointer must point at something real", () => { + let dir = ""; + + const seed = (frontmatterExtra: string, status = "accepted"): string => { + const root = mkdtempSync(join(tmpdir(), "kit-enforcedby-")); + mkdirSync(join(root, "docs", "adr"), { recursive: true }); + writeFileSync( + join(root, "docs", "adr", "0001-x.md"), + `---\nid: ADR-0001\ntitle: X\nstatus: ${status}\n${frontmatterExtra}---\n\n# X\n`, + ); + return root; + }; + + after(() => rmSync(dir, { recursive: true, force: true })); + + it("fails when the named file does not exist — a claim of coverage that is not coverage", () => { + dir = seed("enforced_by: [src/gone.test.ts]\n"); + const f = collectAdrFindings(dir); + assert.equal(f.violations.length, 1); + assert.equal(f.violations[0].detail, "src/gone.test.ts"); + assert.match(f.violations[0].message, /does not exist/); + }); + + it("passes when the file is there", () => { + dir = seed("enforced_by: [src/here.test.ts]\n"); + mkdirSync(join(dir, "src"), { recursive: true }); + writeFileSync(join(dir, "src", "here.test.ts"), "// enforcement lives here\n"); + assert.equal(collectAdrFindings(dir).violations.length, 0); + }); + + it("ignores a non-accepted ADR — a proposal's pointer is not yet a claim", () => { + dir = seed("enforced_by: [src/gone.test.ts]\n", "proposed"); + assert.equal(collectAdrFindings(dir).violations.length, 0); + }); + + it("is silent when the field is absent", () => { + dir = seed(""); + assert.equal(collectAdrFindings(dir).violations.length, 0); + }); +}); diff --git a/src/commands/adr.ts b/src/commands/adr.ts index a45b0d4c..d315004f 100644 --- a/src/commands/adr.ts +++ b/src/commands/adr.ts @@ -10,7 +10,13 @@ * * kit never interprets ADR prose (off-charter); it enforces only the explicit * toml block. Only `accepted` ADRs gate; an accepted ADR with no rules is surfaced - * as "documented, not enforced" — never silently green. A transitive forbid_import + * as "documented, not enforced" — never silently green. + * + * Some decisions are not expressible in the block's grammar (no `paths` negation; + * patterns match line by line; the manifest is not in the walked file set). Those name + * their real enforcement point in frontmatter — `enforced_by: [src/x.test.ts]` — which + * `adr list` prints and `adr check` verifies exists, so the pointer cannot rot into a + * claim of coverage that is not coverage. A transitive forbid_import * that hits an unresolvable relative import is a `gap` (can't prove), not a pass. * * This file also owns the impure `node_modules` resolver injected into the pure evaluator @@ -215,7 +221,26 @@ export function collectAdrFindings(cwd: string): AdrFindings { // One resolver per run: its caches are what make a cross-package walk affordable. Rules // without `follow_packages` never call it, so this costs nothing when nobody opted in. const packages = createNodeModulesResolver(cwd); - for (const { adr } of adrs) { + for (const { adr, file } of adrs) { + // An `enforced_by` pointer is a CLAIM that the decision is enforced somewhere the + // kit-enforce grammar cannot reach. A pointer at a file that does not exist is worse + // than no pointer: it reads as coverage. Checked for every accepted ADR, enforced or + // not — a documented-only ADR may still name an external enforcement point. + if (adr.status === "accepted") { + for (const target of adr.enforcedBy) { + if (!exists(pathJoin(cwd, target))) { + violations.push({ + adrId: adr.id, + file, + line: 1, + rule: "require-pattern", + detail: target, + message: `${adr.id} declares enforced_by: ${target}, which does not exist — an enforcement claim must point at something real`, + kind: "violation", + }); + } + } + } if (!adrIsEnforced(adr)) continue; enforcedCount++; for (const v of evaluateAdr(adr, files, { packages })) { @@ -398,6 +423,11 @@ export async function cmdAdr(): Promise { ? `${c.yellow}documented, not enforced${c.reset}` : `${c.dim}${adr.status}${c.reset}`; console.log(` ${adr.id} ${adr.title} [${state}] ${c.dim}${file}${c.reset}`); + // Where the claim is held when the grammar cannot hold it. Printed under the ADR so + // "documented, not enforced" is never confused with "not enforced anywhere". + for (const target of adr.enforcedBy) { + console.log(` ${c.dim}└ also enforced by ${target}${c.reset}`); + } } return true; } From 8247420b86af730a9cee29d4bf5791b661f158f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 16:29:50 +0000 Subject: [PATCH 6/6] style: run prettier over the three files CI flagged MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `npm run format:check` failed on src/adr.ts, src/commands/usage.ts and src/commands/usage.test.ts. Formatting only — no behaviour change. Two causes, both mine: `eslint --fix` left trailing whitespace where it removed the dead `no-control-regex` disable comments, and the `list()` helper added to adr.ts had lines past the print width. The real miss is the process one. I ran build, tsc, eslint, the full suite and `review --stages adr,skill` before pushing, and never ran `format:check` — which is a step CI runs and a contributor runs locally. Reproduced the failure first, then confirmed the same command passes ("All matched files use Prettier code style!"), rather than assuming --write was sufficient. --- src/adr.ts | 11 +++++++++-- src/commands/usage.test.ts | 2 +- src/commands/usage.ts | 1 - 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/adr.ts b/src/adr.ts index 9f1eb2d8..000d97a3 100644 --- a/src/adr.ts +++ b/src/adr.ts @@ -125,11 +125,18 @@ function list(frontmatter: string, key: string): string[] { .map((v) => v.trim().replace(/^["']|["']$/g, "")) .filter(Boolean); } - const blocked = frontmatter.match(new RegExp(`^${key}:[ \\t]*\\n((?:[ \\t]*-[^\\n]*\\n?)+)`, "mi")); + const blocked = frontmatter.match( + new RegExp(`^${key}:[ \\t]*\\n((?:[ \\t]*-[^\\n]*\\n?)+)`, "mi"), + ); if (!blocked) return []; return blocked[1] .split("\n") - .map((line) => line.replace(/^[ \t]*-[ \t]*/, "").trim().replace(/^["']|["']$/g, "")) + .map((line) => + line + .replace(/^[ \t]*-[ \t]*/, "") + .trim() + .replace(/^["']|["']$/g, ""), + ) .filter(Boolean); } diff --git a/src/commands/usage.test.ts b/src/commands/usage.test.ts index 743c2328..3294a3ff 100644 --- a/src/commands/usage.test.ts +++ b/src/commands/usage.test.ts @@ -43,7 +43,7 @@ describe("renderTab", () => { const widths = new Set( rendered .split("\n") - + .map((l) => l.replace(/\x1b\[[0-9;]*m/g, "").length), ); assert.equal( diff --git a/src/commands/usage.ts b/src/commands/usage.ts index f39235c7..c9e7f526 100644 --- a/src/commands/usage.ts +++ b/src/commands/usage.ts @@ -38,7 +38,6 @@ const WIDTH = 66; /** Visible length: padding maths has to ignore the colour escapes, or the box comes out ragged. */ function plain(s: string): string { - return s.replace(/\x1b\[[0-9;]*m/g, ""); }