@bongos/core 1.19.769 → 1.19.771

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.19.769",
6
- "core_contract": "1.19.769",
7
- "source_commit": "5552e7252b67cc3192446a9c48ca2cd4faf10368",
5
+ "core_version": "1.19.771",
6
+ "core_contract": "1.19.771",
7
+ "source_commit": "a9e56ee098e0b0c508d92a0f880f18caaf3438da",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-16T14:54:06.807Z",
9
+ "built_at": "2026-09-16T15:34:56.672Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 483,
13
13
  "agent_docs_stubbed": 25,
14
- "functional_verbatim": 2125,
14
+ "functional_verbatim": 2129,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2633,
20
- "tree_sha256": "8c9135931a565436af92fb7fc38953f0bf6b9ca8c6b608b91e9751c88dd62fe0",
19
+ "file_count": 2637,
20
+ "tree_sha256": "7e50ea161f2670ff73a230e867e9d1239db16e738ec828b2d86f163c9e44a000",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2502,7 +2502,7 @@
2502
2502
  {
2503
2503
  "path": "docs/file-map.md",
2504
2504
  "mode": "0000644",
2505
- "sha256": "9369c319c356283bdce655d126e78f339c9a329ecefc02d17c2997c45e583475"
2505
+ "sha256": "d74253fcf1963030a0906afaed5ee3069601829fa3de71743ba21dd07ceaca10"
2506
2506
  },
2507
2507
  {
2508
2508
  "path": "docs/handoff-template.md",
@@ -2512,7 +2512,7 @@
2512
2512
  {
2513
2513
  "path": "docs/module-api-changelog.md",
2514
2514
  "mode": "0000644",
2515
- "sha256": "ffaeae011cd55895b3c16a2ee903e0593aa42a22cfceb3307c5228452c737948"
2515
+ "sha256": "6a9f97eefb85c4f420e127512fd07bc00212ebc1fc5f4411f8d2af9253c69641"
2516
2516
  },
2517
2517
  {
2518
2518
  "path": "docs/modules-contract.md",
@@ -3697,7 +3697,7 @@
3697
3697
  {
3698
3698
  "path": "modules/builder-settings/interaction-prefs.js",
3699
3699
  "mode": "0000644",
3700
- "sha256": "22368cf35709440fb21edf90c9d61ea1f604f386439586745e057f4f4e4b2b88"
3700
+ "sha256": "5405744db59cb4372b818cc207cb6f6e53a760935ddec0ef8811532ce9874b8b"
3701
3701
  },
3702
3702
  {
3703
3703
  "path": "modules/builder-settings/model-alloc-prefs.js",
@@ -4457,7 +4457,7 @@
4457
4457
  {
4458
4458
  "path": "modules/grading/generated-provenance.js",
4459
4459
  "mode": "0000644",
4460
- "sha256": "b76be7ce89c0b627d4fe14ac031af6d6b7793c25eeef7defb59aa1335f2de982"
4460
+ "sha256": "4bec6214b843daaa61118566e9c4ead8918322303df811f753369fb68c093ed3"
4461
4461
  },
4462
4462
  {
4463
4463
  "path": "modules/grading/grader-config.js",
@@ -7577,12 +7577,12 @@
7577
7577
  {
7578
7578
  "path": "package-lock.json",
7579
7579
  "mode": "0000644",
7580
- "sha256": "17b6a246e7e14d3949244aff29ed7e63dc5bc5d65a7aeff2a463f8283fa5b4cc"
7580
+ "sha256": "12fe4a7e4e34e534096d540489201021b891700e349dfd55553951e242852665"
7581
7581
  },
7582
7582
  {
7583
7583
  "path": "package.json",
7584
7584
  "mode": "0000644",
7585
- "sha256": "df0b9ff8dceb4cafd08db0a28bcbe92b1cbee04fefe4f811574257af0929901c"
7585
+ "sha256": "2d44d17619572c24cfa4b5758cc3a698b4846bccf935182c979d16934029a9b9"
7586
7586
  },
7587
7587
  {
7588
7588
  "path": "public-docs/index.html",
@@ -8017,7 +8017,7 @@
8017
8017
  {
8018
8018
  "path": "scripts/gds/exec-path-guard.js",
8019
8019
  "mode": "0000644",
8020
- "sha256": "6250c525e105540d33006b9c7e0335b9045186f7a73b714b359322a2e1ac3394"
8020
+ "sha256": "176e26885f96247bbeec1b04d3a668bbc2fac2d944fc1f1028781241a45d613c"
8021
8021
  },
8022
8022
  {
8023
8023
  "path": "scripts/gds/feedback-latest.js",
@@ -8082,7 +8082,7 @@
8082
8082
  {
8083
8083
  "path": "scripts/gds/fitness.js",
8084
8084
  "mode": "0000644",
8085
- "sha256": "4856485e5bb5bb03e47cd6953659c25214411d0b63287f6ab2faa4c34e1e27bc"
8085
+ "sha256": "cf15baceafba3863ffde95d6ee01107faa9d86ae0081d579307c9f49dcf0fdbd"
8086
8086
  },
8087
8087
  {
8088
8088
  "path": "scripts/gds/gate-review.js",
@@ -8149,6 +8149,11 @@
8149
8149
  "mode": "0000644",
8150
8150
  "sha256": "4910f973c926493f1323f9be4a9593ed1596f920dbf9d7f34abce4ff61f1ecad"
8151
8151
  },
8152
+ {
8153
+ "path": "scripts/gds/gen-interaction-posture.js",
8154
+ "mode": "0000644",
8155
+ "sha256": "86c57a2c8e595a2c7283a720ce32192fb3fa1539fa6be152c8349543b1dbb94f"
8156
+ },
8152
8157
  {
8153
8158
  "path": "scripts/gds/gen-repo-map.js",
8154
8159
  "mode": "0000644",
@@ -8924,6 +8929,11 @@
8924
8929
  "mode": "0000644",
8925
8930
  "sha256": "072c493cc549e16c6045abf5bf74a8cfe0004b45844c6f4fe9e846d55d88bf34"
8926
8931
  },
8932
+ {
8933
+ "path": "scripts/gds/stray-unit-sweep.sh",
8934
+ "mode": "0000644",
8935
+ "sha256": "7bc5a6193130085d7faebfb6a71a16b89aa09747f30fdfd612d7cf4c7a1778ff"
8936
+ },
8927
8937
  {
8928
8938
  "path": "scripts/gds/surface.js",
8929
8939
  "mode": "0000644",
@@ -9407,7 +9417,7 @@
9407
9417
  {
9408
9418
  "path": "src/module-api.js",
9409
9419
  "mode": "0000644",
9410
- "sha256": "3237d6ea069eae19ed9baf41a528984e21010defcfc5a0d96d34fd8eeca2fb4a"
9420
+ "sha256": "c14e336beea666e10221a3e524c1db662aa136207564eb3b6742f74d6c12e1c8"
9411
9421
  },
9412
9422
  {
9413
9423
  "path": "src/module-loader/catalog.js",
@@ -11022,7 +11032,7 @@
11022
11032
  {
11023
11033
  "path": "tests/grader_generated_provenance.mjs",
11024
11034
  "mode": "0000644",
11025
- "sha256": "9c41daaad5111afb380b878cb0d3374d014bbadf5c2dc1e06b8f319712024fc4"
11035
+ "sha256": "1823c173f811d52d93b63201d0ebb0d85f2b1e42a569e6331580ef097f5c05cc"
11026
11036
  },
11027
11037
  {
11028
11038
  "path": "tests/grader_panel.mjs",
@@ -11167,12 +11177,12 @@
11167
11177
  {
11168
11178
  "path": "tests/help_requests.mjs",
11169
11179
  "mode": "0000644",
11170
- "sha256": "8a4b380ebe8ad5667f20f4f0c21f8870262340e6d0f2aabca36d0737fdc81d1e"
11180
+ "sha256": "e1e1075e2f8ba4b52acba29804e5fd62443b823cfd15d4c50f0a93fcea0d13bd"
11171
11181
  },
11172
11182
  {
11173
11183
  "path": "tests/helpers.mjs",
11174
11184
  "mode": "0000644",
11175
- "sha256": "2c5ffa3ed447fdbbbc1b44e553f493bf05ab2848b154836defe3fe40e1909592"
11185
+ "sha256": "2527e761e86d289bfaf1f0c5bc416762b02c0784f0307fd0f445da656d8db549"
11176
11186
  },
11177
11187
  {
11178
11188
  "path": "tests/hierarchy-config.mjs",
@@ -11384,6 +11394,11 @@
11384
11394
  "mode": "0000644",
11385
11395
  "sha256": "a7e2b9453ea73689e098894b15de8c693b3fa07704b34cb3539f14f0e67ca36c"
11386
11396
  },
11397
+ {
11398
+ "path": "tests/interaction_posture.mjs",
11399
+ "mode": "0000644",
11400
+ "sha256": "b5f865163bd89c12f53d6596bee596146fb81d8f0b152e84ee2ce03b1f12774a"
11401
+ },
11387
11402
  {
11388
11403
  "path": "tests/interaction_prefs.mjs",
11389
11404
  "mode": "0000644",
@@ -12784,6 +12799,11 @@
12784
12799
  "mode": "0000644",
12785
12800
  "sha256": "905b7b9147fa35f74c2c5f4118175ebf96fa36107379207a7a2c59afc5a2967d"
12786
12801
  },
12802
+ {
12803
+ "path": "tests/sql_column_lines.mjs",
12804
+ "mode": "0000644",
12805
+ "sha256": "c68bf3aa9cd886de435293f0a536df8153fcc99187c48097de12c5482b1ee430"
12806
+ },
12787
12807
  {
12788
12808
  "path": "tests/sso_checkin_identity_spoof.mjs",
12789
12809
  "mode": "0000644",
@@ -12907,7 +12927,7 @@
12907
12927
  {
12908
12928
  "path": "tests/task_recommendations.mjs",
12909
12929
  "mode": "0000644",
12910
- "sha256": "cfce8e24b5844b1720399cc467e89ca457d74dfbf0783ef2a733d433f0443bd8"
12930
+ "sha256": "1c5d35d58371cbde290a3e71e023febeebf350b54031d458c2ebe58f25962aa4"
12911
12931
  },
12912
12932
  {
12913
12933
  "path": "tests/task_visual_route_gate.mjs",
package/docs/file-map.md CHANGED
@@ -188,6 +188,7 @@
188
188
  │ ├── backfill-cost.js ← read art ledger → POST /api/gds/cost (idempotent)
189
189
  │ ├── smoke-write.sh ← write-path smoke (create→claim→ship round-trip)
190
190
  │ ├── backup-dropin-sweep.sh ← (task 1003930) control-plane sweep: retires the temporary task-1003925 backup drop-in from each project whose pinned core carries the fix (1.19.750+), proving one backup succeeds without it first; survey by default, `--apply` to act; re-run after each pin moves
191
+ │ ├── stray-unit-sweep.sh ← (task 1003939) control-plane sweep: the ledger's torn-down slugs that still have a service/backup unit loaded or on disk (a torn-down project holding memory outside the ADR 0285 count); survey by default, `--apply` disables, removes, reloads, reset-failed's; DB/dumps/checkout/Caddy left alone
191
192
  │ ├── smoke-gds.js ← public-surface + 401-check smoke test (asserts /api/pms/* now 404s — removed in V3.R01-followup #308)
192
193
  │ ├── box.js ← (#598/ADR 0031) per-builder dev box lifecycle CLI: provision·park·wake·deprovision·sweep-idle·sweep-dormant·status (dry-run default, --apply to act; rank-gated)
193
194
  │ ├── smoke-box.sh ← (#598) hermetic box-lifecycle smoke (no DB/DO/network): runs tests/boxes.mjs + tests/cf_tunnel.mjs (#771 named tunnel) + route-rank + module-load
@@ -2012,5 +2012,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
2012
2012
  landed since 1.19.767 with no explicit bump. run 35110800163. (task 1002620)
2013
2013
  1.19.769 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2014
2014
  landed since 1.19.768 with no explicit bump. run 35111616379. (task 1002620)
2015
+ 1.19.770 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2016
+ landed since 1.19.769 with no explicit bump. run 35113578542. (task 1002620)
2017
+ 1.19.771 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2018
+ landed since 1.19.770 with no explicit bump. run 35116254725. (task 1002620)
2015
2019
  ---------------------------------------------------------------------------
2016
2020
  ```
@@ -68,11 +68,11 @@ const FIELDS = {
68
68
  values: {
69
69
  new: {
70
70
  label: 'New to local tooling (default)',
71
- contract: 'Toolchain familiarity: new — walk local tooling (package managers, ports, env vars, shells) through step by step rather than naming a command and moving on. Where a supported UI path exists, give that before the terminal one.',
71
+ contract: 'Toolchain familiarity: new — walk local tooling (package managers, ports, env vars, shells) through step by step rather than naming a command and moving on. Where a supported UI path exists, give that before the terminal one, and prefer a supported path over a bespoke technical fix they would then have to maintain.',
72
72
  },
73
73
  comfortable: {
74
74
  label: 'Comfortable with local tooling',
75
- contract: 'Toolchain familiarity: comfortable — no need to walk through local toolchains (Homebrew, ports, env vars) step by step.',
75
+ contract: 'Toolchain familiarity: comfortable — no need to walk through local toolchains (Homebrew, ports, env vars) step by step. Still prefer a supported path over a bespoke technical fix unless it genuinely cannot do the job.',
76
76
  },
77
77
  },
78
78
  },
@@ -71,6 +71,15 @@ const GENERATORS = [
71
71
  verify: '--check',
72
72
  owns: [{ file: 'docs/file-map.md', scope: 'fence', marker: 'BEGIN GENERATED FILE-MAP' }],
73
73
  },
74
+ {
75
+ // task 1002515: the CLAUDE.md §2 fallback posture, generated from the interaction
76
+ // knob's own defaults. Note CLAUDE.md now carries TWO fences owned by two different
77
+ // generators — the roster is per-region, not per-file, so each is verified by the
78
+ // script that actually owns it and everything outside both is graded normally.
79
+ script: 'gen-interaction-posture.js',
80
+ verify: '--check',
81
+ owns: [{ file: 'CLAUDE.md', scope: 'fence', marker: 'BEGIN GENERATED INTERACTION POSTURE' }],
82
+ },
74
83
  {
75
84
  script: 'gen-api-docs.js',
76
85
  verify: '--check',
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.769",
3
+ "version": "1.19.771",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.769",
9
+ "version": "1.19.771",
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.19.769",
3
+ "version": "1.19.771",
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",
@@ -76,7 +76,7 @@ const EXEMPTIONS = [
76
76
  + 'If that refusal is ever removed this exemption dies with it.',
77
77
  },
78
78
  {
79
- at: 'modules/grading/generated-provenance.js:156',
79
+ at: 'modules/grading/generated-provenance.js:165',
80
80
  why: '`dir` defaults to path.join(__dirname, ..., \'scripts\', \'gds\') — the SERVER\'s own tree — '
81
81
  + 'and `script` comes from the module-level GENERATORS table, which holds string literals '
82
82
  + 'only. The `scriptDir` seam exists so the unit test never spawns a process and is never '
@@ -84,8 +84,8 @@ const EXEMPTIONS = [
84
84
  + 'trusted generator against a suspect tree, which is the inverse of the B50 shape.',
85
85
  },
86
86
  {
87
- at: 'modules/grading/generated-provenance.js:223',
88
- why: 'The async twin of the call at :156 — same `dir` default, same literal GENERATORS table, '
87
+ at: 'modules/grading/generated-provenance.js:232',
88
+ why: 'The async twin of the call at :165 — same `dir` default, same literal GENERATORS table, '
89
89
  + 'same test-only `scriptDir` seam. Listed separately rather than folded in because an '
90
90
  + 'exemption is keyed to a line: if one of the two calls changes shape, only that one '
91
91
  + 're-opens for review.',
@@ -179,6 +179,16 @@ function checkGeneratedArtifactsFresh() {
179
179
  } catch {
180
180
  violations.push('The tracked CLAUDE.md §13 recent-sessions snippet is STALE — run `node scripts/gds/gen-session-index.js` and commit (ADR 0062 §8 / ADR 0110). NB docs/session-log-index.md itself is gitignored + deploy-regenerated, not gated here.');
181
181
  }
182
+ try {
183
+ // task 1002515 (criterion `accessibility-fluency-profile`, clause 2): the CLAUDE.md §2
184
+ // fallback posture is generated from the interaction knob's own defaults, so the kernel
185
+ // and modules/builder-settings/interaction-prefs.js cannot drift into two copies of one
186
+ // rule — which is what they were. Fully tracked (unlike the ADR 0110 pair above), so a
187
+ // plain --check is the right gate.
188
+ execFileSync(process.execPath, [path.join(__dirname, 'gen-interaction-posture.js'), '--check'], { cwd: REPO_ROOT, stdio: 'pipe' });
189
+ } catch {
190
+ violations.push('The CLAUDE.md §2 interaction-posture block is STALE — it is generated from modules/builder-settings/interaction-prefs.js. Run `node scripts/gds/gen-interaction-posture.js` and commit (task 1002515).');
191
+ }
182
192
  try {
183
193
  execFileSync(process.execPath, [path.join(__dirname, 'gen-file-map.js'), '--check'], { cwd: REPO_ROOT, stdio: 'pipe' });
184
194
  } catch {
@@ -192,7 +202,7 @@ function checkGeneratedArtifactsFresh() {
192
202
  }
193
203
  // NB the role packs' responsibility blocks are generated too, but their freshness
194
204
  // is asserted by role-pack-guard.js — one check owns "the packs are correct".
195
- return { name: 'generated artifacts fresh', ok: violations.length === 0, hardFail: violations.length > 0, violations, warnings: [], note: 'Tracked generated content matches source: nested CLAUDE.md symbol blocks (code) + the CLAUDE.md §13 snippet (sessions) via --check-block, plus docs/file-map.md skills/scheduled-tasks sections and docs/api/openapi.json + api-reference.md (API). The gitignored docs/repo-map.md + docs/session-log-index.md are deploy-regenerated (ADR 0110), not gated here.' };
205
+ return { name: 'generated artifacts fresh', ok: violations.length === 0, hardFail: violations.length > 0, violations, warnings: [], note: 'Tracked generated content matches source: nested CLAUDE.md symbol blocks (code) + the CLAUDE.md §13 snippet (sessions) via --check-block, plus the CLAUDE.md §2 interaction-posture block (the knob defaults), docs/file-map.md skills/scheduled-tasks sections and docs/api/openapi.json + api-reference.md (API). The gitignored docs/repo-map.md + docs/session-log-index.md are deploy-regenerated (ADR 0110), not gated here.' };
196
206
  }
197
207
 
198
208
  // ---- deterministic cron routine boundary (System 4 / ADR 0078) -------------
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // gen-interaction-posture.js — the CLAUDE.md §2 communication posture, generated
4
+ // (task 1002515; clauses 2/3/7 of criterion `accessibility-fluency-profile`).
5
+ //
6
+ // WHY. Until this generator, the posture existed TWICE: as hand-written prose in the
7
+ // always-loaded root CLAUDE.md §2, and as the injectable contract fragments in
8
+ // modules/builder-settings/interaction-prefs.js. Two copies of one rule is a drift
9
+ // machine — task 1002514 changed the knob's defaults and §2 did not move, which is
10
+ // exactly the failure clause 2 of the criterion names. Prose that a human keeps in
11
+ // sync with code is prose that will eventually lie.
12
+ //
13
+ // THE FIX, and why a generator rather than a deletion. The obvious move is to delete
14
+ // §2's prose and let the injected profile carry everything. It cannot: the injector
15
+ // (.claude/hooks/conductor.js) relays a profile resolved SERVER-SIDE, so a session
16
+ // with no Bongos session — a fresh clone, an offline checkout, a box that cannot
17
+ // reach the API — receives NOTHING, and deleting the prose would leave exactly the
18
+ // reader the criterion is about with no posture at all. So §2 keeps a fallback, and
19
+ // the fallback is GENERATED from the knob's own defaults: one source of truth, a
20
+ // bounded copy in the kernel, and a CI gate (scripts/gds/fitness.js) that fails the
21
+ // build the moment they disagree.
22
+ //
23
+ // The emitted text is the contract fragments VERBATIM — the same strings a session
24
+ // is handed. Nothing is paraphrased here, because a paraphrase is a second copy
25
+ // wearing a generator's clothes.
26
+ //
27
+ // Zero-dependency and deterministic (no timestamps, no disk scan) — so `--check` is a
28
+ // clean idempotent CI gate, the gen-session-index.js / gen-repo-map.js pattern
29
+ // (ADR 0062 §8, ADR 0063).
30
+ //
31
+ // Usage:
32
+ // node scripts/gds/gen-interaction-posture.js # rewrite the CLAUDE.md §2 block
33
+ // node scripts/gds/gen-interaction-posture.js --check # write nothing; exit 2 if the block would change
34
+ // node scripts/gds/gen-interaction-posture.js --print # print the rendered block (debug)
35
+
36
+ const fs = require('node:fs');
37
+ const path = require('node:path');
38
+ const { resolveInstanceRoot } = require('../../src/instance-config'); // ADR 0108 §1: configurable-root
39
+ const { sameContent } = require('./gen-freshness'); // eol-insensitive freshness (CRLF checkouts)
40
+
41
+ // The SOURCE is core content (the knob ships with Cloud Bongos); the TARGET is the
42
+ // INSTANCE's own always-loaded kernel. Requiring the lib relative to __dirname keeps
43
+ // the source pinned to the core even on a standalone instance where the core lives in
44
+ // node_modules/@bongos/core, while resolveInstanceRoot() puts the block in the host's
45
+ // CLAUDE.md rather than the vendored core's (the task-1002314 trap).
46
+ const prefs = require('../../modules/builder-settings/interaction-prefs.js');
47
+
48
+ const CLAUDE_PATH = path.join(resolveInstanceRoot(), 'CLAUDE.md');
49
+
50
+ const BEGIN = '<!-- BEGIN GENERATED INTERACTION POSTURE (scripts/gds/gen-interaction-posture.js — do not hand-edit) -->';
51
+ const END = '<!-- END GENERATED INTERACTION POSTURE -->';
52
+
53
+ // The fallback posture = every DEFAULT field value's contract fragment, verbatim.
54
+ // A field whose default carries no fragment contributes nothing — the same rule
55
+ // contractFor() applies, so the block can never claim a posture a session is not
56
+ // actually given (today that exemption is `machinery: show`, which means "behave as
57
+ // you otherwise would" and so has no text to state).
58
+ function renderBlock() {
59
+ const eff = prefs.effectiveProfile({});
60
+ const bullets = [];
61
+ for (const field of prefs.FIELD_KEYS) {
62
+ const fragment = prefs.FIELDS[field].values[eff[field]].contract;
63
+ if (fragment) bullets.push(`> - ${fragment}`);
64
+ }
65
+ return [
66
+ BEGIN,
67
+ '**When no profile is injected** (no Bongos session — a fresh clone, an offline checkout), fall back to the knob’s own defaults. Generated from [`modules/builder-settings/interaction-prefs.js`](modules/builder-settings/interaction-prefs.js), so this can never drift from what a session is actually handed:',
68
+ '',
69
+ ...bullets,
70
+ END,
71
+ ].join('\n');
72
+ }
73
+
74
+ // Splice the block between its markers. Markers ABSENT → leave the file untouched and
75
+ // say so: an instance whose CLAUDE.md predates this generator (or one that deliberately
76
+ // authored its own §2) must not have a block injected into it at an arbitrary offset.
77
+ function injectBlock(existing, block) {
78
+ const b = existing.indexOf(BEGIN);
79
+ const e = existing.indexOf(END);
80
+ if (b === -1 || e === -1 || e < b) return existing;
81
+ return existing.slice(0, b) + block + existing.slice(e + END.length);
82
+ }
83
+
84
+ function computeWrite() {
85
+ if (!fs.existsSync(CLAUDE_PATH)) return null;
86
+ const current = fs.readFileSync(CLAUDE_PATH, 'utf8');
87
+ if (current.indexOf(BEGIN) === -1) return { missingMarkers: true };
88
+ const next = injectBlock(current, renderBlock());
89
+ if (sameContent(current, next)) return null;
90
+ return { full: CLAUDE_PATH, content: next, rel: 'CLAUDE.md' };
91
+ }
92
+
93
+ function main() {
94
+ if (process.argv.includes('--print')) {
95
+ process.stdout.write(renderBlock() + '\n');
96
+ return;
97
+ }
98
+ const write = computeWrite();
99
+ if (write && write.missingMarkers) {
100
+ console.error('gen-interaction-posture: WARN CLAUDE.md has no posture markers — skipping injection.');
101
+ return;
102
+ }
103
+ if (process.argv.includes('--check')) {
104
+ if (write) {
105
+ console.error('gen-interaction-posture --check: STALE — would update CLAUDE.md');
106
+ console.error(' run: node scripts/gds/gen-interaction-posture.js (and commit CLAUDE.md)');
107
+ process.exit(2);
108
+ }
109
+ console.log('gen-interaction-posture --check: up to date.');
110
+ return;
111
+ }
112
+ if (!write) {
113
+ console.log('gen-interaction-posture: CLAUDE.md posture block already current — no changes.');
114
+ return;
115
+ }
116
+ fs.writeFileSync(write.full, write.content);
117
+ console.log('gen-interaction-posture: wrote CLAUDE.md');
118
+ }
119
+
120
+ if (require.main === module) main();
121
+
122
+ module.exports = { renderBlock, injectBlock, computeWrite, BEGIN, END };
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/gds/stray-unit-sweep.sh — find (and with --apply, stop + remove) a project's systemd
3
+ # units that are still on the control plane although its provisioning row is torn down
4
+ # (task 1003939, an ADR 0285 follow-up).
5
+ #
6
+ # WHY. On 2026-09-16 `demo.service` was RUNNING and enabled while the demo row read torn_down:
7
+ # a torn-down project holding ~80 MB on a box whose ceiling is 15 projects, outside the count
8
+ # the ledger keeps. The live MemAvailable floor (task 1003927) covers placement, but the memory
9
+ # is still gone and the unit still restarts on boot. The teardown leg is the fix going forward
10
+ # (task 1003940 added reset-failed); this is the BACKSTOP that finds what an older teardown
11
+ # left behind, from the ledger's own list of torn-down rows, and names it so the ship note can.
12
+ #
13
+ # Run on the control plane as a sudoer:
14
+ # bash scripts/gds/stray-unit-sweep.sh # survey only — read-only
15
+ # bash scripts/gds/stray-unit-sweep.sh --apply # disable --now, remove the unit files, reload, reset-failed
16
+ #
17
+ # Left in place on purpose, even with --apply: the project's database, its dumps under
18
+ # /var/backups/gds, any checkout, and its Caddy block — data is precious, and the address is
19
+ # task 1002897's to reclaim.
20
+ set -uo pipefail
21
+
22
+ APPLY=0; [ "${1:-}" = "--apply" ] && APPLY=1
23
+ DB="${PGDATABASE:-cloudbongos}"
24
+ ETC=/etc/systemd/system
25
+
26
+ # The torn-down slugs, from the ledger, read as the instance user (peer auth — the runner's
27
+ # own identity; nothing here needs a superuser).
28
+ slugs=$(sudo -n -u lars psql -d "$DB" -tAc "select slug from provisioning_instances where status='torn_down' order by slug" 2>/dev/null) \
29
+ || { echo "cannot read provisioning_instances from $DB as lars"; exit 1; }
30
+
31
+ found=(); cleared=()
32
+ echo "== torn-down rows that still have systemd units =="
33
+ for slug in $slugs; do
34
+ # The slug grammar provisioning.isValidSlug enforces; anything else never reaches a command.
35
+ [[ "$slug" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] || continue
36
+ units="$slug.service $slug-db-backup.service $slug-db-backup.timer"
37
+ loaded=$(systemctl list-units --all --no-legend $units 2>/dev/null | awk '{print ($1 == "●") ? $2 : $1}' | tr '\n' ' ')
38
+ files=$(for u in $units; do [ -e "$ETC/$u" ] && printf '%s ' "$u"; done)
39
+ [ -z "$loaded" ] && [ -z "$files" ] && continue
40
+ state=$(systemctl is-active "$slug.service" 2>/dev/null || true)
41
+ printf ' %-24s service=%-9s loaded=[%s] files=[%s]\n' "$slug" "$state" "${loaded% }" "${files% }"
42
+ found+=("$slug")
43
+ [ "$APPLY" = 1 ] || continue
44
+ sudo -n systemctl disable --now "$slug.service" 2>/dev/null || true
45
+ sudo -n systemctl disable --now "$slug-db-backup.timer" 2>/dev/null || true
46
+ sudo -n rm -f "$ETC/$slug.service" "$ETC/$slug-db-backup.service" "$ETC/$slug-db-backup.timer"
47
+ sudo -n systemctl daemon-reload
48
+ sudo -n systemctl reset-failed "$slug.service" "$slug-db-backup.service" 2>/dev/null || true
49
+ cleared+=("$slug")
50
+ done
51
+
52
+ echo "== summary =="
53
+ echo " stray (${#found[@]}): ${found[*]:-none}"
54
+ if [ "$APPLY" = 1 ]; then echo " cleared (${#cleared[@]}): ${cleared[*]:-none}";
55
+ else echo " (survey only — re-run with --apply to disable, remove and reset-failed them)"; fi
56
+ echo " MemAvailable: $(awk '/MemAvailable/ {print int($2/1024) " MB"}' /proc/meminfo)"
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.769'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.771'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -89,13 +89,35 @@ test('an ordinary diff implicates no generator (a normal ship pays nothing)', ()
89
89
  assert.deepEqual(prov.generatorsInDiff(null), []);
90
90
  });
91
91
 
92
- test('the session-log ship shape implicates exactly gen-session-index.js', () => {
92
+ test('the session-log ship shape implicates gen-session-index.js, and every other owner of that file', () => {
93
93
  // This IS the reproducing diff: a new session log + the regenerated §13 block.
94
+ //
95
+ // Since task 1002515, CLAUDE.md carries TWO fences owned by two generators (§13's
96
+ // session snippet and §2's interaction posture), so a diff touching CLAUDE.md
97
+ // implicates both. That is matching at FILE granularity, and it is deliberate: the
98
+ // roster has only the changed file NAMES to work from, never the hunks, so it cannot
99
+ // tell which fence moved. The result is still sound — every claim it makes ("this
100
+ // region is generator output") is true whether or not that region changed in this
101
+ // diff, and it is proved by actually running each generator's own freshness check.
102
+ // What it must never do is claim a region a generator does NOT own, which is what
103
+ // the per-generator `owned` list below pins.
94
104
  const g = prov.generatorsInDiff(['docs/session-logs/2026-08-20-x.md', 'CLAUDE.md']);
95
- assert.equal(g.length, 1);
96
- assert.equal(g[0].script, 'gen-session-index.js');
97
- assert.equal(g[0].verify, '--check-block');
98
- assert.deepEqual(g[0].owned.map((o) => o.file), ['CLAUDE.md']);
105
+ const sessionIndex = g.find((x) => x.script === 'gen-session-index.js');
106
+ assert.ok(sessionIndex, 'the session-log ship must still implicate its own generator');
107
+ assert.equal(sessionIndex.verify, '--check-block');
108
+ assert.deepEqual(sessionIndex.owned.map((o) => o.file), ['CLAUDE.md']);
109
+ // Every implicated generator must own CLAUDE.md — the diff names nothing else generated.
110
+ assert.deepEqual(
111
+ [...new Set(g.flatMap((x) => x.owned.map((o) => o.file)))],
112
+ ['CLAUDE.md'],
113
+ 'no generator may be implicated for a file this diff did not touch',
114
+ );
115
+ // Each fence is verified by the script that actually owns it, never by the other.
116
+ const markers = g.map((x) => `${x.script}:${x.owned[0].marker}`).sort();
117
+ assert.deepEqual(markers, [
118
+ 'gen-interaction-posture.js:BEGIN GENERATED INTERACTION POSTURE',
119
+ 'gen-session-index.js:BEGIN GENERATED SESSION-LOG SNIPPET',
120
+ ]);
99
121
  });
100
122
 
101
123
  test('a docs-only regeneration implicates gen-api-docs.js for each doc it owns that changed', () => {
@@ -125,10 +147,19 @@ test('a PASSING generator check marks its regions verified', () => {
125
147
  changedFiles: ['CLAUDE.md', 'docs/session-logs/x.md'],
126
148
  run: (script, flag) => { calls.push(`${script} ${flag}`); },
127
149
  });
128
- assert.deepEqual(calls, ['gen-session-index.js --check-block'], 'runs the generator ONE time, with its own flag');
129
- assert.deepEqual(m.verified.map((v) => v.file), ['CLAUDE.md']);
150
+ // Each implicated generator runs ONCE, with its OWN flag — never another's.
151
+ assert.deepEqual(calls.sort(), [
152
+ 'gen-interaction-posture.js --check',
153
+ 'gen-session-index.js --check-block',
154
+ ], 'each generator runs exactly once, with its own flag');
155
+ assert.deepEqual(m.verified.map((v) => v.file), ['CLAUDE.md', 'CLAUDE.md'], 'one entry per owned REGION, not per file');
156
+ assert.deepEqual(m.verified.map((v) => v.marker).sort(), [
157
+ 'BEGIN GENERATED INTERACTION POSTURE',
158
+ 'BEGIN GENERATED SESSION-LOG SNIPPET',
159
+ ]);
130
160
  assert.deepEqual(m.unverified, []);
131
- assert.deepEqual(m.checked, [{ script: 'gen-session-index.js', verify: '--check-block', ok: true }]);
161
+ assert.deepEqual(m.checked.map((c) => c.script).sort(), ['gen-interaction-posture.js', 'gen-session-index.js']);
162
+ assert.ok(m.checked.every((c) => c.ok === true));
132
163
  });
133
164
 
134
165
  test('a FAILING generator check marks its regions UNVERIFIED — suspicion stays where evidence is absent', () => {
@@ -137,9 +168,24 @@ test('a FAILING generator check marks its regions UNVERIFIED — suspicion stays
137
168
  run: () => { throw new Error('exit 2'); },
138
169
  });
139
170
  assert.deepEqual(m.verified, []);
140
- assert.equal(m.unverified.length, 1);
141
- assert.equal(m.unverified[0].file, 'CLAUDE.md');
142
- assert.match(m.unverified[0].reason, /did not pass/);
171
+ assert.ok(m.unverified.length >= 1);
172
+ assert.ok(m.unverified.every((u) => u.file === 'CLAUDE.md'));
173
+ assert.ok(m.unverified.every((u) => /did not pass/.test(u.reason)));
174
+ });
175
+
176
+ // task 1002515 made CLAUDE.md the first file owned by TWO generators (§13's session
177
+ // snippet and §2's interaction posture). The property that matters is that a co-owned
178
+ // file is judged per REGION: one fence going stale must not vouch for the other, and
179
+ // must not strip the verdict from the one that is genuinely fresh.
180
+ test('a co-owned file keeps its two fences judged separately', () => {
181
+ const m = prov.verifyGeneratedProvenance({
182
+ changedFiles: ['CLAUDE.md'],
183
+ run: (script) => { if (script === 'gen-interaction-posture.js') throw new Error('stale'); },
184
+ });
185
+ assert.deepEqual(m.verified.map((v) => v.marker), ['BEGIN GENERATED SESSION-LOG SNIPPET'],
186
+ 'the fresh fence keeps its claim');
187
+ assert.deepEqual(m.unverified.map((v) => v.marker), ['BEGIN GENERATED INTERACTION POSTURE'],
188
+ 'the stale fence gets none — and the file is not blanket-exempted by its sibling');
143
189
  });
144
190
 
145
191
  test('a clean diff runs NO generator at all', () => {
@@ -154,7 +200,7 @@ test('one generator failing does not taint another that passed', () => {
154
200
  changedFiles: ['CLAUDE.md', 'docs/api/openapi.json'],
155
201
  run: (script) => { if (script === 'gen-api-docs.js') throw new Error('stale'); },
156
202
  });
157
- assert.deepEqual(m.verified.map((v) => v.file), ['CLAUDE.md']);
203
+ assert.deepEqual([...new Set(m.verified.map((v) => v.file))], ['CLAUDE.md']);
158
204
  assert.deepEqual(m.unverified.map((v) => v.file), ['docs/api/openapi.json']);
159
205
  });
160
206
 
@@ -208,8 +254,8 @@ test('the async verifier is what the grade path uses, and it does not block', as
208
254
  changedFiles: ['CLAUDE.md', 'docs/api/openapi.json'],
209
255
  run: async (script, flag) => { calls.push(`${script} ${flag}`); },
210
256
  });
211
- assert.deepEqual(calls, ['gen-session-index.js --check-block', 'gen-api-docs.js --check']);
212
- assert.deepEqual(m.verified.map((v) => v.file).sort(), ['CLAUDE.md', 'docs/api/openapi.json']);
257
+ assert.deepEqual(calls.sort(), ['gen-api-docs.js --check', 'gen-interaction-posture.js --check', 'gen-session-index.js --check-block']);
258
+ assert.deepEqual([...new Set(m.verified.map((v) => v.file))].sort(), ['CLAUDE.md', 'docs/api/openapi.json']);
213
259
  });
214
260
 
215
261
  test('the async verifier runs NOTHING for a diff with no generated file', async () => {
@@ -225,8 +271,8 @@ test('the async verifier marks a rejecting check unverified, and never rejects i
225
271
  run: async () => { throw new Error('exit 2'); },
226
272
  });
227
273
  assert.deepEqual(m.verified, []);
228
- assert.equal(m.unverified.length, 1);
229
- assert.match(m.unverified[0].reason, /did not pass/);
274
+ assert.ok(m.unverified.length >= 1);
275
+ assert.ok(m.unverified.every((u) => /did not pass/.test(u.reason)));
230
276
  });
231
277
 
232
278
  test('both spawns are TIMEOUT-bounded — a generator lives in the tree being graded', () => {
@@ -252,7 +298,7 @@ test('verifyGeneratedProvenance never throws — it degrades to no claim', () =>
252
298
  assert.ok(m, 'returns a manifest rather than throwing');
253
299
  // A throwing check is a FAILED check, so the region is unverified, not exempt.
254
300
  assert.deepEqual(m.verified, []);
255
- assert.equal(m.unverified.length, 1);
301
+ assert.ok(m.unverified.length >= 1, 'every owned region of the file is unverified, none exempt');
256
302
  });
257
303
 
258
304
  test('the shared worker prompt renders a manifest carried on the bundle, and spawns nothing', () => {
@@ -10,6 +10,7 @@ import { test } from 'node:test';
10
10
  import { createRequire } from 'node:module';
11
11
  import fs from 'node:fs';
12
12
  import path from 'node:path';
13
+ import { sqlColumnLines } from './helpers.mjs';
13
14
  import { fileURLToPath } from 'node:url';
14
15
 
15
16
  const require = createRequire(import.meta.url);
@@ -127,10 +128,7 @@ test('sub-speciality is deliberately NOT modelled yet', () => {
127
128
  // "speciality" anywhere outside a `--` comment and flagged the migration's own
128
129
  // COMMENT ON TABLE, which says it is deliberately not modelled. Same
129
130
  // over-broad-source-assertion reflex as learning 1000196.
130
- const body = sql.slice(sql.indexOf('CREATE TABLE'), sql.indexOf(');'));
131
- const columns = body.split('\n')
132
- .map((l) => l.replace(/--.*$/, '').trim())
133
- .filter((l) => l && !/^(CONSTRAINT|CHECK|\))/.test(l));
131
+ const columns = sqlColumnLines(sql);
134
132
  assert.ok(!columns.some((l) => /special/i.test(l)), `no speciality column: ${columns.filter((l) => /special/i.test(l))}`);
135
133
  });
136
134
 
package/tests/helpers.mjs CHANGED
@@ -385,3 +385,39 @@ export function parseInto(host, html) {
385
385
  return null;
386
386
  });
387
387
  }
388
+
389
+ // The COLUMN LINES of a CREATE TABLE, with SQL line comments removed (task 1003759).
390
+ //
391
+ // Two tests assert "this table has no such column" and must read the COLUMN LIST,
392
+ // not the file: a migration routinely EXPLAINS in a comment that some column is
393
+ // deliberately not modelled, and matching that prose as a violation of the rule it
394
+ // states is the over-broad-source-assertion reflex of learning 1000196.
395
+ //
396
+ // Both had the same strip, and the same bug: `l.replace(/--.*$/, '')`. In JS `.`
397
+ // does not match `\r`, and `$` without the `m` flag matches ONLY end-of-string —
398
+ // so on a CRLF checkout (every Windows clone) the trailing `\r` sits between `.*`
399
+ // and `$`, the match fails, and the comment is NOT stripped. The comment line then
400
+ // reaches the column scan and trips the very assertion it was written to explain.
401
+ //
402
+ // tests/task_recommendations.mjs failed exactly that way on Windows and nowhere
403
+ // else, so CI never saw it and it sat in the local failure baseline being read as
404
+ // environmental noise. tests/help_requests.mjs carried the identical strip and
405
+ // passed only by luck — no comment inside its CREATE TABLE body happened to
406
+ // contain its scanned word.
407
+ //
408
+ // Dropping the `$` is the whole fix: `.*` still stops at the `\r`, the match now
409
+ // succeeds, and the existing `.trim()` removes what is left.
410
+ export function sqlColumnLines(sql) {
411
+ const text = String(sql || '');
412
+ const start = text.indexOf('CREATE TABLE');
413
+ if (start === -1) return [];
414
+ // End on `\n);` rather than the first `);` — a bare `);` can appear inside a
415
+ // comment or an earlier statement, which would cut the body short.
416
+ const endAt = text.indexOf('\n);', start);
417
+ const body = text.slice(start, endAt === -1 ? undefined : endAt);
418
+ return body.split('\n')
419
+ .map((l) => l.replace(/--.*/, '').trim())
420
+ // CREATE TABLE is dropped too: the slice starts at the header line, so a table
421
+ // whose NAME contained a scanned word would otherwise read as a column.
422
+ .filter((l) => l && !/^(CREATE TABLE|CONSTRAINT|CHECK|\))/.test(l));
423
+ }
@@ -0,0 +1,100 @@
1
+ // tests/interaction_posture.mjs — the generated CLAUDE.md §2 posture block (task 1002515;
2
+ // clauses 2/3/7 of criterion `accessibility-fluency-profile`).
3
+ //
4
+ // THE BUG THIS PINS. The communication posture lived in two places at once: prose in the
5
+ // always-loaded root CLAUDE.md §2, and the injectable fragments in
6
+ // modules/builder-settings/interaction-prefs.js. Task 1002514 changed the knob and §2 did
7
+ // not move — a drift that no check could see, because nothing tied the two together.
8
+ //
9
+ // WHAT IS ACTUALLY ASSERTED, and why it is not just "the generator ran". A freshness gate
10
+ // alone is worthless if the renderer can quietly produce nothing: an empty block matches an
11
+ // empty block, `--check` passes, and the kernel silently loses the posture it exists to
12
+ // carry. So the shape of the block is pinned (a bullet per injecting default, none for the
13
+ // one that carries no text), the text is pinned as VERBATIM (a paraphrase is a second copy
14
+ // wearing a generator's clothes), and the committed CLAUDE.md is pinned against a freshly
15
+ // rendered block so a hand-edit fails here and not only in CI.
16
+ import { test } from 'node:test';
17
+ import assert from 'node:assert/strict';
18
+ import { readFileSync } from 'node:fs';
19
+ import { join, dirname } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { createRequire } from 'node:module';
22
+
23
+ const require = createRequire(import.meta.url);
24
+ const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
25
+ const gen = require('../scripts/gds/gen-interaction-posture.js');
26
+ const prefs = require('../modules/builder-settings/interaction-prefs.js');
27
+ // Never `===` on raw file bytes here: the generator renders LF, and a default Windows
28
+ // clone checks CLAUDE.md out as CRLF, so a byte comparison would fail on one OS and pass
29
+ // on the other. gen-freshness.js exists for exactly this (and its header says so).
30
+ const { sameContent } = require('../scripts/gds/gen-freshness.js');
31
+
32
+ test('the block carries one bullet per DEFAULT value that injects, and none for one that does not', () => {
33
+ const block = gen.renderBlock();
34
+ const bullets = block.split('\n').filter((l) => l.startsWith('> - '));
35
+ const eff = prefs.effectiveProfile({});
36
+ const injecting = prefs.FIELD_KEYS.filter((f) => prefs.FIELDS[f].values[eff[f]].contract);
37
+
38
+ assert.ok(injecting.length >= 3, `the accessible default must inject a posture; got ${injecting.length} field(s)`);
39
+ assert.equal(bullets.length, injecting.length, 'one bullet per injecting default — no more, no fewer');
40
+ // The silent default is deliberate (`machinery: show` means "behave as you otherwise
41
+ // would"), and the block must not invent text for it.
42
+ assert.ok(prefs.FIELD_KEYS.length > injecting.length, 'the silent default still exists — this test would pass vacuously if it did not');
43
+ });
44
+
45
+ test('every bullet is the contract fragment VERBATIM — the block paraphrases nothing', () => {
46
+ const block = gen.renderBlock();
47
+ const eff = prefs.effectiveProfile({});
48
+ for (const field of prefs.FIELD_KEYS) {
49
+ const fragment = prefs.FIELDS[field].values[eff[field]].contract;
50
+ if (!fragment) continue;
51
+ assert.ok(block.includes(fragment), `${field}: the block must carry the fragment a session is handed, not a restatement`);
52
+ }
53
+ });
54
+
55
+ // Clause 7: "bias toward the supported path over a bespoke technical fix" was encoded in no
56
+ // rule, hook, rubric line or check. It now rides the toolchain knob, so it is injected into
57
+ // every session AND scaled — a newcomer is told why (they would have to maintain it), an
58
+ // expert is told the exception (unless the supported path cannot do the job).
59
+ test('the supported-path bias reaches BOTH toolchain profiles, not just the newcomer', () => {
60
+ const beginner = prefs.contractFor({});
61
+ const expert = prefs.contractFor({ interaction: { toolchain_familiarity: 'comfortable' } });
62
+ assert.match(beginner, /prefer a supported path over a bespoke technical fix/);
63
+ assert.match(expert, /prefer a supported path over a bespoke technical fix/);
64
+ assert.ok(gen.renderBlock().includes('supported path over a bespoke technical fix'),
65
+ 'the fallback the kernel carries must state it too — a session with no profile is the reader this clause is about');
66
+ });
67
+
68
+ test('the committed CLAUDE.md §2 block matches a freshly rendered one', () => {
69
+ const claude = readFileSync(join(REPO_ROOT, 'CLAUDE.md'), 'utf8');
70
+ const b = claude.indexOf(gen.BEGIN);
71
+ const e = claude.indexOf(gen.END);
72
+ assert.ok(b !== -1 && e > b, 'CLAUDE.md §2 must carry the posture markers');
73
+ assert.ok(sameContent(claude.slice(b, e + gen.END.length), gen.renderBlock()),
74
+ 'CLAUDE.md §2 is STALE or hand-edited — run `node scripts/gds/gen-interaction-posture.js`');
75
+ });
76
+
77
+ test('CLAUDE.md expresses the posture through the knob instead of restating it', () => {
78
+ const claude = readFileSync(join(REPO_ROOT, 'CLAUDE.md'), 'utf8');
79
+ const b = claude.indexOf(gen.BEGIN);
80
+ const e = claude.indexOf(gen.END);
81
+ const handWritten = claude.slice(0, b) + claude.slice(e + gen.END.length);
82
+ // Clause 2: the kernel must NAME the knob (it named only the wandering one before)...
83
+ assert.match(handWritten, /interaction profile/i, 'the kernel must reference the knob');
84
+ assert.match(handWritten, /interaction-prefs\.js|\/settings#interaction/, 'and say where it lives');
85
+ // ...and must not carry a hand-written second copy of the posture beside the generated one.
86
+ assert.doesNotMatch(handWritten, /Don't assume software-engineering vocabulary/,
87
+ 'the old §2 prose copy is what drifted — it must not come back outside the generated block');
88
+ });
89
+
90
+ test('injectBlock replaces only between the markers, and leaves a marker-less file alone', () => {
91
+ const framed = `head\n${gen.BEGIN}\nold\n${gen.END}\ntail`;
92
+ const out = gen.injectBlock(framed, `${gen.BEGIN}\nnew\n${gen.END}`);
93
+ assert.equal(out, `head\n${gen.BEGIN}\nnew\n${gen.END}\ntail`);
94
+ assert.ok(!out.includes('old'));
95
+
96
+ // An instance that authored its own §2 must never have a block spliced in at an arbitrary
97
+ // offset — the generator declines rather than guessing where it belongs.
98
+ const bare = '# Some other instance CLAUDE.md\n\nno markers here\n';
99
+ assert.equal(gen.injectBlock(bare, 'anything'), bare);
100
+ });
@@ -0,0 +1,87 @@
1
+ // tests/sql_column_lines.mjs — the CRLF-safe SQL column extractor (task 1003759).
2
+ //
3
+ // THE BUG IT REPLACES. Two tests assert "this table has no such column", and both
4
+ // read the COLUMN LIST rather than the file, because a migration routinely EXPLAINS
5
+ // in a comment that some column is deliberately not modelled — matching that prose
6
+ // as a violation of the rule it states is the over-broad-source-assertion reflex of
7
+ // learning 1000196.
8
+ //
9
+ // Both stripped comments with `l.replace(/--.*$/, '')`, and that is wrong on a CRLF
10
+ // checkout. In JS `.` does not match `\r`, and `$` without the `m` flag matches ONLY
11
+ // end-of-string, so the trailing `\r` sits between `.*` and `$`, the match fails,
12
+ // and the comment survives into the column scan.
13
+ //
14
+ // tests/task_recommendations.mjs failed exactly that way — on Windows only, so CI
15
+ // never saw it and it sat in the local failure baseline being read as environmental
16
+ // noise for weeks. tests/help_requests.mjs had the identical strip and passed purely
17
+ // by luck: no comment inside its CREATE TABLE body happened to contain its scanned
18
+ // word. A guard that works by accident is the thing this file exists to stop.
19
+ //
20
+ // Run: node --test tests/sql_column_lines.mjs
21
+
22
+ import { test } from 'node:test';
23
+ import assert from 'node:assert/strict';
24
+ import { sqlColumnLines } from './helpers.mjs';
25
+
26
+ // The shape that broke: a comment INSIDE the table body naming the very word a
27
+ // caller scans for. Written with explicit \r\n — the whole point is the line ending.
28
+ const CRLF = [
29
+ 'CREATE TABLE lifecycle_task_recommendations (',
30
+ ' id BIGSERIAL PRIMARY KEY,',
31
+ " -- There is NO 'accepted'. Accepting a recommendation is CLAIMING THE TASK,",
32
+ ' -- so "accepted" would be a second, weaker record of holding work.',
33
+ ' status TEXT NOT NULL DEFAULT \'open\',',
34
+ ' CONSTRAINT one_open_pair UNIQUE (from_builder_id, to_builder_id)',
35
+ ');',
36
+ ].join('\r\n');
37
+
38
+ const LF = CRLF.replace(/\r\n/g, '\n');
39
+
40
+ test('CRLF: a comment naming the scanned word is stripped, not returned as a column', () => {
41
+ const cols = sqlColumnLines(CRLF);
42
+ assert.ok(!cols.some((l) => /accepted/i.test(l)),
43
+ `comments must not reach the column list, got: ${JSON.stringify(cols)}`);
44
+ });
45
+
46
+ test('CRLF and LF produce identical column lists — the platform must not matter', () => {
47
+ assert.deepEqual(sqlColumnLines(CRLF), sqlColumnLines(LF));
48
+ });
49
+
50
+ test('the real columns survive on both line endings', () => {
51
+ for (const [label, sql] of [['CRLF', CRLF], ['LF', LF]]) {
52
+ const cols = sqlColumnLines(sql);
53
+ assert.ok(cols.some((l) => l.startsWith('id ')), `${label}: id column kept`);
54
+ assert.ok(cols.some((l) => l.startsWith('status ')), `${label}: status column kept`);
55
+ }
56
+ });
57
+
58
+ test('CONSTRAINT / CHECK lines and the closing paren are filtered out', () => {
59
+ const cols = sqlColumnLines(CRLF);
60
+ assert.ok(!cols.some((l) => /^CONSTRAINT/.test(l)), 'constraints are not columns');
61
+ assert.ok(!cols.some((l) => /^\)/.test(l)), 'the closing paren is not a column');
62
+ });
63
+
64
+ test('a trailing comment on a real column line is stripped, the column kept', () => {
65
+ const sql = 'CREATE TABLE t (\r\n id BIGINT, -- accepted is not modelled here\r\n);';
66
+ const cols = sqlColumnLines(sql);
67
+ assert.deepEqual(cols, ['id BIGINT,']);
68
+ });
69
+
70
+ test('the body ends at the closing \\n); — a bare ); inside a comment does not cut it short', () => {
71
+ // `indexOf(');')` would stop at the comment and hide every column after it.
72
+ const sql = [
73
+ 'CREATE TABLE t (',
74
+ ' -- see fn(a); for why',
75
+ ' keeper TEXT,',
76
+ ');',
77
+ ].join('\r\n');
78
+ assert.ok(sqlColumnLines(sql).some((l) => l.startsWith('keeper')),
79
+ 'a column after a comment containing "};" must still be seen');
80
+ });
81
+
82
+ test('no CREATE TABLE, empty, and non-string inputs are safe', () => {
83
+ assert.deepEqual(sqlColumnLines('ALTER TABLE t ADD COLUMN x INT;'), []);
84
+ assert.deepEqual(sqlColumnLines(''), []);
85
+ assert.deepEqual(sqlColumnLines(null), []);
86
+ assert.deepEqual(sqlColumnLines(undefined), []);
87
+ });
@@ -18,6 +18,7 @@ import { test } from 'node:test';
18
18
  import { createRequire } from 'node:module';
19
19
  import fs from 'node:fs';
20
20
  import path from 'node:path';
21
+ import { sqlColumnLines } from './helpers.mjs';
21
22
  import { fileURLToPath } from 'node:url';
22
23
 
23
24
  const require = createRequire(import.meta.url);
@@ -79,10 +80,7 @@ test('the schema has NO accepted status — accepting is claiming, which lives e
79
80
  });
80
81
 
81
82
  test('the schema carries no transfer column', () => {
82
- const body = SQL.slice(SQL.indexOf('CREATE TABLE'), SQL.indexOf('\n);'));
83
- const columns = body.split('\n')
84
- .map((l) => l.replace(/--.*$/, '').trim())
85
- .filter((l) => l && !/^(CONSTRAINT|CHECK|\))/.test(l));
83
+ const columns = sqlColumnLines(SQL);
86
84
  const suspect = columns.filter((l) => /transferred|claimed_by|assigned_to|accepted/i.test(l));
87
85
  assert.deepEqual(suspect, [], `no transfer-shaped column: ${suspect}`);
88
86
  });