@bongos/core 1.20.3 → 1.20.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.20.3",
6
- "core_contract": "1.20.3",
7
- "source_commit": "08c1ed047c5b8d898c41f3be382772312100c240",
5
+ "core_version": "1.20.4",
6
+ "core_contract": "1.20.4",
7
+ "source_commit": "cb1154ab6f5663fcf5ec4b1b2de9d59f4d53c0cf",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-30T03:02:28.909Z",
9
+ "built_at": "2026-09-30T03:24:03.992Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 554,
13
13
  "agent_docs_stubbed": 25,
14
- "functional_verbatim": 2609,
14
+ "functional_verbatim": 2611,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3189,
20
- "tree_sha256": "b2f8df195cd671a690f009c403ebe37ab58c8f1a3237c20aafbbc28fb5a08cb3",
19
+ "file_count": 3191,
20
+ "tree_sha256": "979ab93ed2588479c67154f837a646ebb982d48e470267a63835464662744650",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -312,7 +312,7 @@
312
312
  {
313
313
  "path": "README.md",
314
314
  "mode": "0000644",
315
- "sha256": "74e177ab9a2b3a6c2e2de5fdac803944053c249d9e5ad794a838e2cbc497d848"
315
+ "sha256": "9c9e99127efafe70d60e1d464f07815397e436e69dc0b82c2e67f41dbd8dc8d6"
316
316
  },
317
317
  {
318
318
  "path": "bin/bongos.js",
@@ -787,7 +787,7 @@
787
787
  {
788
788
  "path": "docs/adr/0073-secrets-scan-exclude-uri-detector.md",
789
789
  "mode": "0000644",
790
- "sha256": "80b3539d259105ad7e5284f622857a1aa2889182e856026d56b4138c1cf1bda8"
790
+ "sha256": "05107d37f497f8f5d8d1ca4d2a8d19e688fc9258352d7b9bfe1976c4ab1fee79"
791
791
  },
792
792
  {
793
793
  "path": "docs/adr/0074-memory-map-stays-light.md",
@@ -2762,7 +2762,7 @@
2762
2762
  {
2763
2763
  "path": "docs/module-api-changelog.md",
2764
2764
  "mode": "0000644",
2765
- "sha256": "b1141ad77bcd4d780bfb80787cce1fea081b764b377bd72c083aaef520e11f70"
2765
+ "sha256": "fc86b2b7f4ff0f0825ddbef82d3d33286d642a86e5f4dcf6fb62d6ad2efe3558"
2766
2766
  },
2767
2767
  {
2768
2768
  "path": "docs/modules-contract.md",
@@ -8822,12 +8822,12 @@
8822
8822
  {
8823
8823
  "path": "package-lock.json",
8824
8824
  "mode": "0000644",
8825
- "sha256": "09e758156a2344894e786908b8d30cfcdf39b7eee6e03a9bdb6671e08791e77a"
8825
+ "sha256": "a2051b26efcb3819d5b8190bff59b4695bbd0cd69efed2f95a4af8717e6a1c38"
8826
8826
  },
8827
8827
  {
8828
8828
  "path": "package.json",
8829
8829
  "mode": "0000644",
8830
- "sha256": "5059b19984a6fdd0997b4f6062001f43c537c9cc02b983039af1ea1f56a958e2"
8830
+ "sha256": "895cf915952f0155ee5d26315afd456d0630e5ab39ce65c22fa10bc0fa5aa7ae"
8831
8831
  },
8832
8832
  {
8833
8833
  "path": "public-docs/index.html",
@@ -8847,7 +8847,7 @@
8847
8847
  {
8848
8848
  "path": "release-notes.json",
8849
8849
  "mode": "0000644",
8850
- "sha256": "42d334cbd78f69784c937c7b857aedfe54019ceaa072893b9a2a520c651a6703"
8850
+ "sha256": "b0f0a951c309fec69d3e8c0a4b30516bf56b1fcb4ce24490d84a89bd14ed01a0"
8851
8851
  },
8852
8852
  {
8853
8853
  "path": "scripts/bongos-mcp.js",
@@ -9197,7 +9197,7 @@
9197
9197
  {
9198
9198
  "path": "scripts/gds/control-manifest.js",
9199
9199
  "mode": "0000644",
9200
- "sha256": "270101b46772cc8e280bc5a36f66cfae578a901f07b4b5e1f959c9a566ebb5fd"
9200
+ "sha256": "063e4e14f040bf78de4a8212222405cc361520d2475453b1d516329a7ca9f070"
9201
9201
  },
9202
9202
  {
9203
9203
  "path": "scripts/gds/copy-apply.js",
@@ -9319,6 +9319,11 @@
9319
9319
  "mode": "0000644",
9320
9320
  "sha256": "17d0fbc570b6dea3dfcf2425058510cd70b23f066fdf1ed751072852713284a3"
9321
9321
  },
9322
+ {
9323
+ "path": "scripts/gds/doc-control-claims.js",
9324
+ "mode": "0000644",
9325
+ "sha256": "94c89c10620d3d6678fa3b1a8decebc6d338c0b0b2a3799880e49311d2cd2770"
9326
+ },
9322
9327
  {
9323
9328
  "path": "scripts/gds/docs-entropy.js",
9324
9329
  "mode": "0000644",
@@ -9407,7 +9412,7 @@
9407
9412
  {
9408
9413
  "path": "scripts/gds/fitness.js",
9409
9414
  "mode": "0000644",
9410
- "sha256": "d1f22a1eb3b7ae5ac51b795e2392bff5c9e0a72aea7de4c1947351752510fa89"
9415
+ "sha256": "dee3e8db3ad7fff3c1e30a59aa58bab00972cba9710bd463e10ab8a3edc71e38"
9411
9416
  },
9412
9417
  {
9413
9418
  "path": "scripts/gds/gate-review.js",
@@ -10732,7 +10737,7 @@
10732
10737
  {
10733
10738
  "path": "src/bongos/module-scope-map.js",
10734
10739
  "mode": "0000644",
10735
- "sha256": "322900d024e3737b18c2341d408a7832c2b0dffc9c36ff510768cbca78236098"
10740
+ "sha256": "cc78b33cc9d0fca6d217a7c6aaf66f64de9cbe1447a352fc769eb8556207a453"
10736
10741
  },
10737
10742
  {
10738
10743
  "path": "src/bongos/module-store.js",
@@ -10942,7 +10947,7 @@
10942
10947
  {
10943
10948
  "path": "src/module-api.js",
10944
10949
  "mode": "0000644",
10945
- "sha256": "1961978ac4aca79f758766be8172a32e3bcbe9f447fa767d700689eaeec97542"
10950
+ "sha256": "e3e6394ab165f0c08e998c0c43fe18d1ec92872a84490deb7a9a01712656c024"
10946
10951
  },
10947
10952
  {
10948
10953
  "path": "src/module-loader/catalog.js",
@@ -12184,6 +12189,11 @@
12184
12189
  "mode": "0000644",
12185
12190
  "sha256": "f51d3c747fbb3201ed3388abb1386d0547c3a3be70d7ad7237b9a0f5fdd2347b"
12186
12191
  },
12192
+ {
12193
+ "path": "tests/doc_control_claims.mjs",
12194
+ "mode": "0000644",
12195
+ "sha256": "499d0436bc000e9d1749d99d3e1e9886cb1a4bdd4388f5a165bc099359c7132a"
12196
+ },
12187
12197
  {
12188
12198
  "path": "tests/docs_entropy.mjs",
12189
12199
  "mode": "0000644",
package/README.md CHANGED
@@ -48,28 +48,19 @@ When it finishes, open Claude Code and run `/builder-start` to see what tasks ar
48
48
 
49
49
  ## Secrets scanning
50
50
 
51
- We keep secrets (`.env` files, API keys, private keys) out of the repo with two layers. Both are **instance topology** — the git hooks (`.husky/`) and CI (`.github/`) are excluded from the published core artifact, and a secrets pre-commit hook is an instance addition (nothing provisions one automatically). This repo tracks `.husky/pre-push` — the ADR 0043 permission gate, a separate wall from secrets scanning. The pattern below is the recommended posture:
51
+ We keep secrets (`.env` files, API keys, private keys) out of the repo with three layers. The CI and patrol layers are **instance topology**: `.github/` is excluded from the published core artifact, so an instance that wants them carries its own copies.
52
52
 
53
- - **Pre-commit hook** (`.husky/pre-commit`) — scans your *staged* changes before each commit and aborts if it finds a secret. On an instance that ships `.husky/`, activate it **once per clone**:
53
+ - **Prevent — the secret-gate hook** (`.claude/hooks/secret-gate.js`, wired in `.claude/settings.json`). A Claude Code PreToolUse hook that refuses to write secret-shaped content (provider-prefixed tokens, PEM blocks) into the working tree or a shell. It is the cooperative layer: it fails open on any error, so the two gates below are what actually hold.
54
+ - **Gate — the CI secret scan** (the `Secret scan` step in `.github/workflows/unit.yml`). Every PR runs a pinned, checksum-verified gitleaks over the checked-out tree against `.gitleaks.toml`. This must pass before merge. It scans current files, not history.
55
+ - **Patrol — the weekly history scan** (`scripts/gds/redteam-patrol.js`, run by `.github/workflows/redteam-patrol.yml`). Scans the full git history with the same config, which is how a secret committed and later deleted is still caught.
54
56
 
55
- ```sh
56
- node scripts/gds/install-git-hooks.js
57
- ```
57
+ A local pre-commit hook is optional and an instance addition: this repo tracks `.husky/pre-push` (the ADR 0043 permission gate, a separate wall) but no secrets pre-commit hook. If your instance adds hooks under `.husky/`, activate them once per clone with `node scripts/gds/install-git-hooks.js` (it no-ops on a checkout without a `.husky/`; `node scripts/gds/doctor.js` reports which case you're in).
58
58
 
59
- Prefer that over setting `core.hooksPath` by hand: it no-ops cleanly on a checkout without a `.husky/` (e.g. a scaffolded instance that hasn't added hooks), whereas pointing `core.hooksPath` at a directory that isn't there leaves git with a dangling hooks path and no gate. `node scripts/gds/doctor.js` reports which case you're in.
59
+ ### If a scan blocks you, here's how to investigate
60
60
 
61
- It always runs a built-in guard (blocks `.env`, `*.pem`, credential files, obvious key patterns). If [TruffleHog](https://github.com/trufflesecurity/trufflehog) is installed (`brew install trufflehog` on macOS; `scoop install trufflehog` / `choco install trufflehog` or the GitHub release on Windows) it also runs a deep scan; if not, it warns and lets the commit through — CI is the real gate.
62
-
63
- > **Windows:** git runs the `.husky/pre-commit` hook through the Git-Bash `sh` bundled with Git for Windows, so the hook works if you installed Git for Windows (the usual case). If you don't have a `sh` available, the local hook silently no-ops — CI's full-history TruffleHog scan is then your only gate (and it always runs regardless of platform).
64
-
65
- - **CI scan** (`.github/workflows/secrets-scan.yml`) — every PR to `main` runs TruffleHog over the **full git history**. This must pass before merge.
66
-
67
- ### If a commit is blocked, here's how to investigate
68
-
69
- 1. **Read the output.** The hook prints the offending file(s) and why.
70
- 2. **Is it a real secret?** Then it must never be committed. Remove it from the commit (`git restore --staged <file>`), and move the value out of the repo — local secrets go in `.env.local` (gitignored; run `/builder-setup`); production secrets live in your instance's secret store, never in the tree. If it was *already committed in an earlier commit*, the history scan will keep failing until the history is scrubbed — flag this to the owner, since rewriting shared history needs care.
71
- 3. **Is it a false positive?** (e.g. an example key in docs, a test fixture, a Stripe `pk_test_` publishable key.) Add a path regex — one per line — to `.husky/secrets-allowlist.txt` (created on first use). Both the hook and CI read that allowlist. Keep entries narrow and only add them after confirming the match is genuinely not a secret.
72
- 4. **Genuine emergency commit** (you're sure it's clean and need to move now): `git commit --no-verify` skips the local hook — but CI will still scan the PR, so this only defers the check, it doesn't bypass it.
61
+ 1. **Read the output.** The hook and gitleaks both name the offending file and the rule that matched.
62
+ 2. **Is it a real secret?** Then it must never be committed. Remove it from the commit (`git restore --staged <file>`), and move the value out of the repo — local secrets go in `.env.local` (gitignored; run `/builder-setup`); production secrets live in your instance's secret store, never in the tree. If it was *already committed in an earlier commit*, the weekly history patrol will keep reporting it until the history is scrubbed — flag this to the owner, since rewriting shared history needs care.
63
+ 3. **Is it a false positive?** (e.g. an example key in docs, a test fixture, a Stripe `pk_test_` publishable key.) Add a narrow path to the `[allowlist]` in `.gitleaks.toml`, and the matching entry to the `ALLOWLIST` in `.claude/hooks/secret-gate.js` — the two lists are kept in step. Only add one after confirming the match is genuinely not a secret.
73
64
 
74
65
  ## If your CLI starts 401'ing
75
66
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Date:** 2026-06-20
4
4
  **Context:** Secrets-scanning gate (`.github/workflows/secrets-scan.yml` + the `.husky/pre-commit` early-catch). Tasks [#1318](https://example.com/builders#/task/1318) and [#1320](https://example.com/builders#/task/1320). Relates to [ADR 0022](0022-secrets-policy.md) (secrets policy) and [ADR 0071](0071-box-confirm-before-destroyed-and-drift-reconcile.md) (dev-box teardown — the trigger).
5
- **Status:** Accepted.
5
+ **Status:** Superseded — the TruffleHog gate this ADR tunes did not survive the core extraction; the live CI secret scan is gitleaks in the unit workflow against `.gitleaks.toml` (task 1003126, ADR 0022). Kept as the record of why the URI detector was excluded. Marked by task 1003418.
6
6
  **Track:** `internal` (development system / CI gates).
7
7
 
8
8
  ## Problem — a noisy detector turned a green gate red repo-wide
@@ -2663,5 +2663,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2663
2663
  landed since 1.20.1 with no explicit bump. run 36660725673. (task 1002620)
2664
2664
  1.20.3 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2665
2665
  landed since 1.20.2 with no explicit bump. run 36662547009. (task 1002620)
2666
+ 1.20.4 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2667
+ landed since 1.20.3 with no explicit bump. run 36664167210. (task 1002620)
2666
2668
  ---------------------------------------------------------------------------
2667
2669
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.20.3",
3
+ "version": "1.20.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.20.3",
9
+ "version": "1.20.4",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.20.3",
3
+ "version": "1.20.4",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -8058,5 +8058,11 @@
8058
8058
  "id": "1004073",
8059
8059
  "text": "Adopting a speciality now walks you through its skills one at a time: what each does, when you'd use it, and what it costs, and you choose on or off for each before seeing the next. Nothing is switched on unless you choose it."
8060
8060
  }
8061
+ ],
8062
+ "1.20.4": [
8063
+ {
8064
+ "id": "1003418",
8065
+ "text": "The project's automatic checks now catch any document that describes a safety check which no longer exists. On its first run it found the README still telling people their code was scanned for leaked passwords by a tool that w"
8066
+ }
8061
8067
  ]
8062
8068
  }
@@ -141,6 +141,15 @@ const CONTROLS = [
141
141
  wiredIn: { file: 'scripts/gds/ship-flow.js', needle: 'ship-honesty' },
142
142
  alsoExists: ['tests/ship_cannot_lie.mjs'],
143
143
  },
144
+ {
145
+ // Task 1003418 (audit A14). The manifest's own blind spot: it covers the rows
146
+ // someone listed. This guard derives claims from the docs instead, so a
147
+ // described-but-absent control reds the gate with no row to add.
148
+ label: 'guard: a doc cannot describe a control that does not exist (A14)',
149
+ file: 'scripts/gds/doc-control-claims.js',
150
+ wiredIn: { file: 'scripts/gds/fitness.js', needle: 'doc-control-claims' },
151
+ alsoExists: ['tests/doc_control_claims.mjs'],
152
+ },
144
153
  ];
145
154
 
146
155
  // Pure evaluation — exported for the test. deps.exists/deps.read override fs.
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/doc-control-claims.js — prose cannot describe a control that does not
3
+ // exist (task 1003418, audit ref A14 of the 2026-08-29 security audit, goal 1000084).
4
+ //
5
+ // WHY. control-manifest.js exists because ADR 0022 named three controls that had
6
+ // silently stopped existing, and it works — for the 14 controls someone remembered
7
+ // to list. Every phantom-control finding since (A6, A13, B31, B32, the row-8 patrol
8
+ // that read green for months while never running) was the same failure one row
9
+ // outside that list. A hand list only catches what a person already thought of, so
10
+ // the manifest cannot be the whole answer. This check reads the claims FROM THE DOCS:
11
+ // a doc sentence that says an enforcement artifact does something is a claim that
12
+ // the artifact exists, and a claim whose artifact is gone reds the gate by
13
+ // construction, with nobody having to add a row. The README's secrets section was
14
+ // the proof on first run: it still sent readers to a TruffleHog workflow and a
15
+ // pre-commit hook that had not existed since the core extraction — the ADR 0022
16
+ // finding itself, one file over.
17
+ //
18
+ // WHAT COUNTS AS A CLAIM, deliberately narrow:
19
+ // · a backticked path to an ENFORCEMENT artifact — a hook, a workflow, a script
20
+ // under scripts/gds or a hooks dir (CONTROL_PATH_RE);
21
+ // · in a sentence that uses control vocabulary — enforce, gate, guard, block,
22
+ // refuse, scan, patrol, audit, fail the build (CONTROL_WORDS);
23
+ // · and does NOT speak of it in the past — removed, retired, replaced, used to,
24
+ // no longer (HISTORY_WORDS). An ADR recording what was deleted is history,
25
+ // which is not a claim; the same path in the present tense is.
26
+ // Superseded / deprecated / rejected / withdrawn ADRs are skipped whole, and so are
27
+ // the dated records (session logs, audits, limitations archives), which describe a
28
+ // moment rather than the tree.
29
+ //
30
+ // WHAT IT DOES NOT PROVE: that an existing artifact is WIRED. That stays the
31
+ // manifest's job (a file present but unhooked). The two halves are complementary:
32
+ // this one scales to every doc and asks only "is it there"; the manifest asks
33
+ // "is it connected" for the controls that matter most.
34
+ //
35
+ // ALLOWLIST. A true claim this scan cannot tell from history (an accepted ADR whose
36
+ // design was later retired by a newer ADR) is listed in ALLOW with the reason. It
37
+ // is shrink-only in practice: an entry that no longer matches anything is itself a
38
+ // violation, so a fixed doc cannot leave a dead exception behind to hide the next.
39
+ //
40
+ // Its own file for the size-ratchet reason (the doc-cli-guard.js precedent);
41
+ // fitness.js requires it in CHECKS, and control-manifest.js lists it, so this guard
42
+ // cannot silently stop existing either.
43
+
44
+ 'use strict';
45
+
46
+ const fs = require('fs');
47
+ const path = require('path');
48
+ const { trackedMarkdown } = require('./doc-cli-guard.js');
49
+
50
+ const ROOT = path.resolve(__dirname, '..', '..');
51
+ const NAME = 'a doc cannot describe an enforcement control that does not exist (task 1003418)';
52
+
53
+ // Two shapes: a script or workflow with its extension, OR a git hook by its bare
54
+ // name. Hooks under .husky/, .githooks/ and scripts/hooks/ carry no extension by
55
+ // convention, and `.husky/pre-commit` is exactly the artifact ADR 0022 lost. The
56
+ // names are git's whole documented set (githooks(5)), not a curated few.
57
+ const GIT_HOOK_NAMES = [
58
+ 'applypatch-msg', 'pre-applypatch', 'post-applypatch', 'pre-commit', 'pre-merge-commit',
59
+ 'prepare-commit-msg', 'commit-msg', 'post-commit', 'pre-rebase', 'post-checkout', 'post-merge',
60
+ 'pre-push', 'pre-receive', 'update', 'proc-receive', 'post-receive', 'post-update',
61
+ 'reference-transaction', 'push-to-checkout', 'pre-auto-gc', 'post-rewrite', 'sendemail-validate',
62
+ 'fsmonitor-watchman', 'p4-changelist', 'p4-prepare-changelist', 'p4-post-changelist',
63
+ 'p4-pre-submit', 'post-index-change',
64
+ ];
65
+ const CONTROL_PATH_RE = new RegExp(
66
+ '`((?:\\.claude/hooks|\\.github/workflows|scripts/gds|scripts/hooks|\\.githooks|\\.husky)/[A-Za-z0-9_.\\-/]+?\\.(?:js|mjs|cjs|sh|yml|yaml|ps1)'
67
+ + `|(?:\\.husky|\\.githooks|scripts/hooks)/(?:${GIT_HOOK_NAMES.join('|')}))\``,
68
+ 'g',
69
+ );
70
+ const CONTROL_WORDS = /\b(enforc\w*|gates?|gated|gating|guards?|guarded|blocks?|blocked|refuses?|refused|hard[- ]?fails?|fails? (?:the )?(?:build|ci|merge|gate)|compensating control|pre-commit|pre-push|scans?|scanned|patrol\w*|audits?)\b/i;
71
+ const HISTORY_WORDS = /\b(removed|deleted|retired|superseded|no longer|used to|never existed|did not exist|never survived|dropped|replaced|gone|historical|legacy|formerly|renamed)\b/i;
72
+ const INACTIVE_STATUS_RE = /^\s*\**\s*status\s*\**\s*:?\s*\**\s*:?\s*\**\s*(superseded|deprecated|rejected|withdrawn)/im;
73
+ const SKIP_PREFIXES = ['docs/session-logs/', 'docs/audits/', 'limitations/'];
74
+
75
+ // `${doc}::${artifact}` -> why this present-tense mention is not a phantom control.
76
+ const ALLOW = new Map([
77
+ ['docs/adr/0025-structured-criterion-task-link.md::scripts/gds/seed-v3-tasks.js',
78
+ 'describes the grep a session HAD to do before this ADR; a seed script, not a control'],
79
+ ['docs/adr/0042-builder-self-deploy-ci-auto-merge.md::.github/workflows/grade-gate.yml',
80
+ 'the CI grader gate this ADR designed; it was armed in the grader-off variant its own Status line records, and grading runs server-side (scripts/gds/ci-grade.js)'],
81
+ ['docs/adr/0111-instance-hosting-provisioning-module.md::scripts/gds/box.js',
82
+ 'the dev-box control-plane runner, retired with dev boxes by ADR 0346 (dev-box-guard.js keeps it out)'],
83
+ ['docs/adr/0150-box-first-boot-bringup-vendored-instances.md::scripts/gds/box.js',
84
+ 'the dev-box control-plane runner, retired with dev boxes by ADR 0346 (dev-box-guard.js keeps it out)'],
85
+ ]);
86
+
87
+ // Sentence-ish units: prose sentences, blank-line paragraphs, and list/table rows.
88
+ function sentencesOf(src) {
89
+ return String(src).split(/(?<=[.!?])\s+|\n\s*\n|\n(?=\s*(?:[-*|]|\d+\.)\s)/);
90
+ }
91
+
92
+ // Pure over its deps: docs is a list of repo-relative .md paths.
93
+ function findPhantomClaims(docs, { read, exists }) {
94
+ const claims = [];
95
+ for (const doc of docs) {
96
+ if (SKIP_PREFIXES.some((p) => doc.startsWith(p))) continue;
97
+ const src = read(doc);
98
+ if (src == null) continue;
99
+ if (INACTIVE_STATUS_RE.test(src.slice(0, 2000))) continue;
100
+ const seen = new Set();
101
+ for (const s of sentencesOf(src)) {
102
+ // Judge the WORDING with every code span blanked: a path's own name must not
103
+ // decide the tense (`gone.yml` reads as "gone", `guard.js` as "guard").
104
+ const prose = s.replace(/`[^`]*`/g, ' ');
105
+ for (const m of s.matchAll(CONTROL_PATH_RE)) {
106
+ const artifact = m[1];
107
+ if (seen.has(artifact) || exists(artifact)) continue;
108
+ if (!CONTROL_WORDS.test(prose) || HISTORY_WORDS.test(prose)) continue;
109
+ seen.add(artifact);
110
+ claims.push({ doc, artifact, sentence: s.replace(/\s+/g, ' ').trim().slice(0, 160) });
111
+ }
112
+ }
113
+ }
114
+ return claims;
115
+ }
116
+
117
+ function evaluate(docs, deps, allow = ALLOW) {
118
+ const claims = findPhantomClaims(docs, deps);
119
+ const hit = new Set();
120
+ const violations = [];
121
+ for (const c of claims) {
122
+ const key = `${c.doc}::${c.artifact}`;
123
+ if (allow.has(key)) { hit.add(key); continue; }
124
+ violations.push(`${c.doc}: names \`${c.artifact}\` as a live control, and it does not exist — "${c.sentence}"`);
125
+ }
126
+ for (const key of allow.keys()) {
127
+ if (!hit.has(key)) violations.push(`stale allowance: ${key} no longer matches a claim — delete it from ALLOW in scripts/gds/doc-control-claims.js`);
128
+ }
129
+ return { violations, claims };
130
+ }
131
+
132
+ const readRel = (rel) => { try { return fs.readFileSync(path.join(ROOT, rel), 'utf8'); } catch { return null; } };
133
+ const existsRel = (rel) => fs.existsSync(path.join(ROOT, rel));
134
+
135
+ function checkDocControlClaims({ files = null } = {}) {
136
+ const docs = files || trackedMarkdown();
137
+ // A lint that reports on nothing is worse than one that fails (docs-entropy).
138
+ if (!docs.length) {
139
+ return { name: NAME, ok: false, hardFail: true, warnings: [],
140
+ violations: ['scan defect — enumerated 0 tracked markdown files; a broken enumeration, not a clean result.'],
141
+ note: 'static scan of enforcement artifacts named in docs.' };
142
+ }
143
+ const { violations, claims } = evaluate(docs, { read: readRel, exists: existsRel });
144
+ return {
145
+ name: NAME,
146
+ ok: violations.length === 0,
147
+ hardFail: violations.length > 0,
148
+ violations,
149
+ warnings: [],
150
+ note: violations.length
151
+ ? 'restore the control, correct the doc to name what really enforces it, or (for an accepted ADR a newer one retired) add an ALLOW entry saying which'
152
+ : `${docs.length} tracked .md file(s) scanned; every enforcement artifact named in a present-tense control sentence exists (${claims.length} allowlisted, each with its reason).`,
153
+ };
154
+ }
155
+
156
+ function main() {
157
+ const r = checkDocControlClaims();
158
+ for (const v of r.violations) console.error(`✗ ${v}`);
159
+ console.log(`${r.hardFail ? 'FAIL' : 'PASS'} ${r.note}`);
160
+ process.exit(r.hardFail ? 1 : 0);
161
+ }
162
+
163
+ if (require.main === module) main();
164
+
165
+ module.exports = {
166
+ checkDocControlClaims, findPhantomClaims, evaluate, sentencesOf,
167
+ ALLOW, GIT_HOOK_NAMES, CONTROL_PATH_RE, CONTROL_WORDS, HISTORY_WORDS, INACTIVE_STATUS_RE,
168
+ };
@@ -1342,6 +1342,7 @@ const CHECKS = [
1342
1342
  checkGovernmentRenameIdentity, checkPrelaunchVocabulary, // 19c task 1003120 (`X`→`X` rename arrow) + 19d task 1003152 ("stealth" is the product's word) — sharing a line: this file is AT the 1,500 budget (Check 23/24 precedent below; task 1003825 buys it back)
1343
1343
  checkWriteRoutesValidated, // Check 18 — BV1 R12 / task 1999: every body-reading write route validates (ADR 0118)
1344
1344
  require('./fitness-ratchets.js').checkQualityRatchets, // Check 20 — task 1003129: quality budgets only tighten (mechanics + knip CI step live in fitness-ratchets.js)
1345
+ require('./doc-control-claims.js').checkDocControlClaims, // Check 37 — task 1003418 / audit A14: a doc cannot describe an enforcement control that does not exist (derived from the docs, not a hand list)
1345
1346
  require('./doc-cli-guard.js').checkDocNamesRealCli, // Check 22 — task 1003165 / G01: a doc that names a CLI invocation must match the real CLI
1346
1347
  require('./skill-preflight.js').checkOpsSkillsDeclareMachine, // Check 23 — task 1003167 / G03: an ops skill declares the machine it needs
1347
1348
  // Check 24 — task 1003166 / G02: no ops script hardcodes a version id. It landed
@@ -435,7 +435,7 @@ const MODULE_GLOBS = {
435
435
  'scripts/gds/skill-lint.js', 'scripts/gds/run-routine.js',
436
436
  'scripts/gds/adr-namespace.js', 'scripts/gds/baseline-staleness.js',
437
437
  'scripts/gds/category-advisory-guard.js', 'scripts/gds/client-baseurl-guard.js',
438
- 'scripts/gds/doc-cli-guard.js', 'scripts/gds/doc-comment-coverage.js',
438
+ 'scripts/gds/doc-cli-guard.js', 'scripts/gds/doc-control-claims.js', 'scripts/gds/doc-comment-coverage.js',
439
439
  'scripts/gds/exec-path-guard.js', 'scripts/gds/fitness-lib.js',
440
440
  'scripts/gds/fitness-checks-error-envelope.js', 'scripts/gds/fitness-checks-identity.js',
441
441
  'scripts/gds/fitness-checks-packaging.js', 'scripts/gds/fitness-checks-route-pins.js',
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.20.3'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.20.4'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,133 @@
1
+ // tests/doc_control_claims.mjs — a doc cannot describe an enforcement control that
2
+ // does not exist (task 1003418). The check is itself a control, so this proves it
3
+ // passes on the real tree AND that it can fail: each case below is a way a
4
+ // phantom-control claim can be written, or a way an honest mention must not trip it.
5
+ import assert from 'node:assert/strict';
6
+ import { test } from 'node:test';
7
+ import { createRequire } from 'node:module';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const require = createRequire(import.meta.url);
12
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
13
+ const dcc = require(path.join(ROOT, 'scripts', 'gds', 'doc-control-claims.js'));
14
+
15
+ // A fake tree: docs maps path -> text; `present` is the set of artifacts on disk.
16
+ function run(docs, present = [], allow = new Map()) {
17
+ const have = new Set(present);
18
+ return dcc.evaluate(Object.keys(docs), {
19
+ read: (p) => (p in docs ? docs[p] : null),
20
+ exists: (p) => have.has(p),
21
+ }, allow);
22
+ }
23
+
24
+ test('the live tree passes', () => {
25
+ const r = dcc.checkDocControlClaims();
26
+ assert.equal(r.hardFail, false, `unexpected violations:\n ${r.violations.join('\n ')}`);
27
+ });
28
+
29
+ test('THE REPORTED CASE: the README sentence naming a deleted secrets workflow is caught', () => {
30
+ // Verbatim shape of the README line this task found (the ADR 0022 finding, one file over).
31
+ const r = run({ 'README.md': '- **CI scan** (`.github/workflows/secrets-scan.yml`) — every PR to `main` runs TruffleHog over the **full git history**. This must pass before merge.\n' });
32
+ // "This must pass before merge" is the next sentence; the claim sentence carries "scan".
33
+ assert.equal(r.violations.length, 1, r.violations.join('\n'));
34
+ assert.match(r.violations[0], /README\.md: names `\.github\/workflows\/secrets-scan\.yml`/);
35
+ });
36
+
37
+ test('the same sentence is clean once the artifact exists', () => {
38
+ const r = run({ 'README.md': 'The CI scan (`.github/workflows/unit.yml`) gates every PR.\n' }, ['.github/workflows/unit.yml']);
39
+ assert.deepEqual(r.violations, []);
40
+ });
41
+
42
+ test('each kind of enforcement artifact is recognised', () => {
43
+ for (const a of ['.claude/hooks/x-gate.js', '.github/workflows/x.yml', 'scripts/gds/x-guard.js', '.husky/x.sh', 'scripts/hooks/x.sh']) {
44
+ const r = run({ 'docs/x.md': `The \`${a}\` hook blocks every bad write.\n` });
45
+ assert.equal(r.violations.length, 1, `${a} should be a claim`);
46
+ }
47
+ });
48
+
49
+ test('a git hook named by its bare, extensionless name is recognised (the ADR 0022 artifact)', () => {
50
+ for (const a of ['.husky/pre-commit', '.husky/pre-push', '.githooks/commit-msg', 'scripts/hooks/pre-receive',
51
+ '.husky/pre-applypatch', '.githooks/push-to-checkout', '.husky/pre-merge-commit']) {
52
+ const r = run({ 'README.md': `- **Pre-commit hook** (\`${a}\`) — scans your staged changes and aborts on a secret.\n` });
53
+ assert.equal(r.violations.length, 1, `${a} should be a claim`);
54
+ }
55
+ // ...and an existing one is clean.
56
+ assert.deepEqual(run({ 'README.md': 'The `.husky/pre-push` hook blocks a raw push.\n' }, ['.husky/pre-push']).violations, []);
57
+ // A non-hook bare name under a hooks dir is not an artifact reference.
58
+ assert.deepEqual(run({ 'README.md': 'The `.husky/README` guards nothing.\n' }).violations, []);
59
+ });
60
+
61
+ test('an incidental "was" does not turn a present-tense claim into history', () => {
62
+ const r = run({ 'docs/x.md': 'The `.github/workflows/gone.yml` gate, which was rewritten last week, blocks every PR.\n' });
63
+ assert.equal(r.violations.length, 1);
64
+ });
65
+
66
+ test('history is not a claim: a sentence about what was removed does not trip it', () => {
67
+ const r = run({ 'docs/adr/0999-x.md': 'The `.github/workflows/secrets-scan.yml` gate was removed at the core extraction.\n' });
68
+ assert.deepEqual(r.violations, []);
69
+ });
70
+
71
+ test('a mention without control vocabulary is not a claim', () => {
72
+ const r = run({ 'docs/x.md': 'See `scripts/gds/old-seed.js` for the shape of a TASKS array.\n' });
73
+ assert.deepEqual(r.violations, []);
74
+ });
75
+
76
+ test('a path\'s own NAME never decides the wording, in either direction', () => {
77
+ // `x-guard.js` must not make a plain pointer read as a control claim...
78
+ assert.deepEqual(run({ 'docs/x.md': 'See `scripts/gds/x-guard.js` for the shape of a TASKS array.\n' }).violations, []);
79
+ // ...and `gone.yml` / `replaced.js` must not make a live claim read as history.
80
+ for (const a of ['.github/workflows/gone.yml', 'scripts/gds/replaced.js']) {
81
+ assert.equal(run({ 'docs/x.md': `The \`${a}\` gate blocks every PR.\n` }).violations.length, 1, a);
82
+ }
83
+ });
84
+
85
+ test('a superseded ADR is skipped whole', () => {
86
+ const r = run({ 'docs/adr/0998-x.md': '# ADR 0998\n\n**Status:** Superseded by ADR 0999.\n\nThe `.github/workflows/gone.yml` gate blocks every PR.\n' });
87
+ assert.deepEqual(r.violations, []);
88
+ });
89
+
90
+ test('an ACCEPTED ADR with the same sentence is checked', () => {
91
+ const r = run({ 'docs/adr/0997-x.md': '# ADR 0997\n\n**Status:** Accepted.\n\nThe `.github/workflows/gone.yml` gate blocks every PR.\n' });
92
+ assert.equal(r.violations.length, 1);
93
+ });
94
+
95
+ test('dated records describe a moment, not the tree, and are skipped', () => {
96
+ for (const doc of ['docs/session-logs/2026-01-01-x.md', 'docs/audits/x.md', 'limitations/v1-shipped.md']) {
97
+ const r = run({ [doc]: 'The `.github/workflows/gone.yml` gate blocks every PR.\n' });
98
+ assert.deepEqual(r.violations, [], doc);
99
+ }
100
+ });
101
+
102
+ test('an allowlisted claim passes, and says so by being counted', () => {
103
+ const docs = { 'docs/adr/0996-x.md': 'The `scripts/gds/box.js` runner gates every box.\n' };
104
+ const r = run(docs, [], new Map([['docs/adr/0996-x.md::scripts/gds/box.js', 'retired by a later ADR']]));
105
+ assert.deepEqual(r.violations, []);
106
+ assert.equal(r.claims.length, 1);
107
+ });
108
+
109
+ test('a STALE allowance is itself a violation, so a fixed doc cannot leave a dead exception', () => {
110
+ const r = run({ 'docs/x.md': 'Nothing here.\n' }, [], new Map([['docs/x.md::scripts/gds/box.js', 'was true once']]));
111
+ assert.equal(r.violations.length, 1);
112
+ assert.match(r.violations[0], /stale allowance/);
113
+ });
114
+
115
+ test('every live allowance still matches something (no dead rows in ALLOW today)', () => {
116
+ const r = dcc.checkDocControlClaims();
117
+ assert.ok(!r.violations.some((v) => /stale allowance/.test(v)));
118
+ for (const [key, why] of dcc.ALLOW) assert.ok(why && why.length > 20, `${key} must say why`);
119
+ });
120
+
121
+ test('an empty enumeration is a broken scan, not a clean result', () => {
122
+ const r = dcc.checkDocControlClaims({ files: [] });
123
+ assert.equal(r.hardFail, true);
124
+ assert.match(r.violations[0], /scan defect/);
125
+ });
126
+
127
+ test('the guard is wired: fitness runs it and the control manifest lists it', async () => {
128
+ const fs = await import('node:fs');
129
+ const fitness = fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'fitness.js'), 'utf8');
130
+ assert.match(fitness, /require\('\.\/doc-control-claims\.js'\)\.checkDocControlClaims/);
131
+ const cm = require(path.join(ROOT, 'scripts', 'gds', 'control-manifest.js'));
132
+ assert.ok(cm.CONTROLS.some((c) => c.file === 'scripts/gds/doc-control-claims.js'), 'listed in the manifest');
133
+ });