@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 +40 -20
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/builder-settings/interaction-prefs.js +2 -2
- package/modules/grading/generated-provenance.js +9 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/exec-path-guard.js +3 -3
- package/scripts/gds/fitness.js +11 -1
- package/scripts/gds/gen-interaction-posture.js +122 -0
- package/scripts/gds/stray-unit-sweep.sh +56 -0
- package/src/module-api.js +1 -1
- package/tests/grader_generated_provenance.mjs +63 -17
- package/tests/help_requests.mjs +2 -4
- package/tests/helpers.mjs +36 -0
- package/tests/interaction_posture.mjs +100 -0
- package/tests/sql_column_lines.mjs +87 -0
- package/tests/task_recommendations.mjs +2 -4
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
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-
|
|
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":
|
|
14
|
+
"functional_verbatim": 2129,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
7580
|
+
"sha256": "12fe4a7e4e34e534096d540489201021b891700e349dfd55553951e242852665"
|
|
7581
7581
|
},
|
|
7582
7582
|
{
|
|
7583
7583
|
"path": "package.json",
|
|
7584
7584
|
"mode": "0000644",
|
|
7585
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
11180
|
+
"sha256": "e1e1075e2f8ba4b52acba29804e5fd62443b823cfd15d4c50f0a93fcea0d13bd"
|
|
11171
11181
|
},
|
|
11172
11182
|
{
|
|
11173
11183
|
"path": "tests/helpers.mjs",
|
|
11174
11184
|
"mode": "0000644",
|
|
11175
|
-
"sha256": "
|
|
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": "
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
88
|
-
why: 'The async twin of the call at :
|
|
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.',
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
96
|
-
assert.
|
|
97
|
-
assert.equal(
|
|
98
|
-
assert.deepEqual(
|
|
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
|
-
|
|
129
|
-
assert.deepEqual(
|
|
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
|
|
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.
|
|
141
|
-
assert.
|
|
142
|
-
assert.
|
|
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-
|
|
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.
|
|
229
|
-
assert.
|
|
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.
|
|
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', () => {
|
package/tests/help_requests.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
});
|