@bongos/core 1.19.315 → 1.19.317

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.
Files changed (38) hide show
  1. package/.bongos-core.json +104 -29
  2. package/.claude/skills/merge-mode/SKILL.md +16 -0
  3. package/.claude/skills/planning-session/SKILL.md +10 -0
  4. package/modules/lifecycle/db-analytics.js +719 -0
  5. package/modules/lifecycle/db-claim-reads.js +165 -0
  6. package/modules/lifecycle/db-claims.js +934 -0
  7. package/modules/lifecycle/db-deps-criteria.js +441 -0
  8. package/modules/lifecycle/db-goals.js +763 -0
  9. package/modules/lifecycle/db-grade.js +233 -0
  10. package/modules/lifecycle/db-overrides.js +181 -0
  11. package/modules/lifecycle/db-rank-authz.js +446 -0
  12. package/modules/lifecycle/db-shared.js +115 -0
  13. package/modules/lifecycle/db-ship.js +994 -0
  14. package/modules/lifecycle/db-tasks.js +810 -0
  15. package/modules/lifecycle/db-versions.js +118 -0
  16. package/modules/lifecycle/db.js +167 -5564
  17. package/package-lock.json +2 -2
  18. package/package.json +1 -1
  19. package/scripts/gds/coreB-carve-proof.sh +18 -9
  20. package/scripts/gds/fitness.js +35 -27
  21. package/scripts/gds/identity-strip.js +75 -0
  22. package/scripts/gds/preflight.js +25 -2
  23. package/scripts/gds/provision.js +7 -3
  24. package/scripts/gds/skill-preflight.js +336 -0
  25. package/src/module-api.js +5 -1
  26. package/tests/auto_satisfy_criteria.mjs +3 -2
  27. package/tests/dependencies_migration.mjs +3 -2
  28. package/tests/fitness.mjs +43 -0
  29. package/tests/goal_abuse_matrix.mjs +3 -2
  30. package/tests/goal_advisory.mjs +3 -2
  31. package/tests/government_claim_eligibility.mjs +3 -1
  32. package/tests/government_rank_sync.mjs +9 -2
  33. package/tests/helpers.mjs +19 -0
  34. package/tests/migration_allocation.mjs +3 -1
  35. package/tests/publish_reconciler.mjs +3 -1
  36. package/tests/rank_gate.mjs +3 -1
  37. package/tests/reward_gate.mjs +3 -1
  38. package/tests/skill_preflight.mjs +251 -0
@@ -0,0 +1,336 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/skill-preflight.js — an ops skill declares the machine it needs,
3
+ // and refuses early when this machine is not it (task 1003167 / G03, goal
4
+ // 1000077; criterion guards-skill-declares-machine).
5
+ //
6
+ // THE INCIDENT. /merge-mode states in its own prose that it assumes the Archon's
7
+ // Mac or the droplet. It was run on a dev box anyway. Worse, ship.js ALREADY knew:
8
+ // strandNextStep() tells a box "/merge-mode is NOT available here — it needs gh +
9
+ // a main checkout this machine doesn't have." So the knowledge existed in two
10
+ // places and neither stopped the run, because prose cannot refuse and the one
11
+ // executable copy was on a different code path. A capability a procedure needs
12
+ // must be a declaration the machine can CHECK, not a sentence a reader can skim.
13
+ //
14
+ // THREE PARTS, matching that diagnosis:
15
+ // 1. `requires:` in the skill's own YAML frontmatter — the declaration.
16
+ // 2. this preflight — the check, naming the MISSING capability and nothing else.
17
+ // 3. checkOpsSkillsDeclareMachine() in fitness.js — the assertion that a skill
18
+ // whose fenced commands touch main/deploy actually carries a declaration.
19
+ //
20
+ // TRI-STATE, deliberately. A probe returns true / false / null, and null means
21
+ // "could not tell" (the context-pack isMainCheckout precedent). We REFUSE on
22
+ // false and WARN on null: refusing on unknown would block legitimate machines,
23
+ // and passing on unknown silently would rebuild the hole this closes.
24
+ //
25
+ // The preflight NEVER connects to anything. Proving an SSH key reaches a host
26
+ // means using it, and a preflight with side effects is not a preflight.
27
+
28
+ 'use strict';
29
+
30
+ const fs = require('fs');
31
+ const path = require('path');
32
+ const { spawnSync } = require('node:child_process');
33
+ const { resolveWorktreeRoot } = require('./context-pack.js');
34
+ const { depsFinding } = require('./preflight.js');
35
+
36
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
37
+ const runGit = (args) => spawnSync('git', args, { cwd: process.cwd(), encoding: 'utf8' }).stdout || '';
38
+
39
+ // Memoized for the life of the process. merge-mode declares BOTH `gh` and
40
+ // `push-credential`, and push-credential's probe also consults gh — so without
41
+ // this, one preflight spawned `gh auth status` twice, each with a 15s timeout.
42
+ // A preflight slow enough to be worth skipping is a preflight nobody runs, which
43
+ // would defeat the entire point of this task.
44
+ let _ghAuthed = null;
45
+ function ghAuthed({ fresh = false } = {}) {
46
+ if (!fresh && _ghAuthed !== null) return _ghAuthed;
47
+ _ghAuthed = ghAuthedUncached();
48
+ return _ghAuthed;
49
+ }
50
+
51
+ function ghAuthedUncached() {
52
+ try {
53
+ const a = spawnSync('gh', ['auth', 'status'], {
54
+ stdio: 'ignore', shell: process.platform === 'win32', windowsHide: true, timeout: 15000,
55
+ });
56
+ return !!a && !a.error && a.status === 0;
57
+ } catch { return false; }
58
+ }
59
+
60
+ function binaryPresent(name) {
61
+ try {
62
+ const probe = process.platform === 'win32' ? ['where', [name]] : ['which', [name]];
63
+ const r = spawnSync(probe[0], probe[1], { stdio: 'ignore', shell: process.platform === 'win32', timeout: 10000 });
64
+ return !!r && !r.error && r.status === 0;
65
+ } catch { return false; }
66
+ }
67
+
68
+ // The registry. `probe` returns true | false | null. `missing` is the sentence a
69
+ // builder reads when it comes back false — it must name the remedy, because the
70
+ // whole failure mode here was a reader who knew the rule and still had nowhere
71
+ // to go with it.
72
+ const CAPABILITIES = {
73
+ gh: {
74
+ what: 'an authenticated GitHub CLI',
75
+ probe: () => ghAuthed(),
76
+ missing: 'no authenticated `gh` on this machine. Run `gh auth login`, or use a machine that has it — '
77
+ + 'a dev box normally does not, by design (it publishes through the server instead, ADR 0055).',
78
+ },
79
+ 'main-checkout': {
80
+ what: 'the main checkout, not a linked worktree',
81
+ // Tri-state on purpose: resolveWorktreeRoot cannot always tell.
82
+ probe: () => resolveWorktreeRoot({ runGit, cwd: process.cwd() }).isMainCheckout,
83
+ missing: 'this is a linked worktree, not the main checkout. Procedures that mutate `main` must run in the '
84
+ + 'main checkout — a worktree holding the branch has nowhere to merge INTO (task 1002572).',
85
+ },
86
+ 'push-credential': {
87
+ // Mirrors ship.js pushVia(): the project's operative definition of "can push"
88
+ // is an authed gh (or an explicit token), and everything else routes through
89
+ // the server. Following that rather than inventing a stricter rule keeps one
90
+ // answer to the question instead of two that can disagree.
91
+ what: 'a credential that can push to origin',
92
+ probe: () => ghAuthed() || !!(process.env.GITHUB_TOKEN || process.env.GH_TOKEN),
93
+ missing: 'no push credential here. This machine lands work through the server-mediated publish '
94
+ + '(`node scripts/gds/ship.js <id>` resumes it) — it cannot push `main` directly.',
95
+ },
96
+ 'droplet-ssh': {
97
+ what: 'SSH access to the deploy host',
98
+ // false when there is no ssh at all; null when there is, because proving the
99
+ // key and host actually work would mean connecting.
100
+ probe: () => (binaryPresent('ssh') ? null : false),
101
+ missing: 'no `ssh` binary on this machine, so a step that reaches the deploy host cannot run. '
102
+ + 'In `ci` deploy mode nothing should need it — check config/deploy.json before proceeding.',
103
+ },
104
+ tty: {
105
+ what: 'an interactive terminal',
106
+ probe: () => !!(process.stdin.isTTY && process.stdout.isTTY),
107
+ missing: 'no TTY — this procedure prompts, and a prompt with nobody to answer it hangs. '
108
+ + 'Use the non-interactive form (a --*-file flag or an explicit flag) instead.',
109
+ },
110
+ 'node-modules': {
111
+ what: 'resolvable dependencies',
112
+ // DELEGATES to preflight.js rather than testing repoRoot/node_modules itself.
113
+ // Writing that test here is exactly the bug idea 1000788 reported against
114
+ // depsFinding: a worktree has no node_modules of its own, yet every require
115
+ // resolves fine because node walks up. Two copies of a probe are two chances
116
+ // to disagree, and this task exists because knowledge lived in two places.
117
+ probe: () => depsFinding().level === 'ok',
118
+ missing: 'dependencies are not resolvable from this tree — a step needing pg/express/pino will fail. '
119
+ + 'Run `npm ci` in the repo root.',
120
+ },
121
+ };
122
+
123
+ // ---- the declaration ------------------------------------------------------
124
+ // Read `requires:` out of a skill's YAML frontmatter. Accepts the inline form
125
+ // (`requires: [gh, main-checkout]`) and the dash-list form. Returns
126
+ // { requires, declared, error }: `declared` distinguishes "declared nothing"
127
+ // from "declared an empty list", which the fitness check needs to tell apart.
128
+ function parseRequires(md) {
129
+ const m = /^---\n([\s\S]*?)\n---/.exec(String(md || ''));
130
+ if (!m) return { requires: [], declared: false, error: null };
131
+ const fm = m[1];
132
+ const inline = /^requires:[ \t]*\[(.*?)\][ \t]*$/m.exec(fm);
133
+ if (inline) {
134
+ const items = inline[1].split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean);
135
+ return { requires: items, declared: true, error: null };
136
+ }
137
+ const block = /^requires:[ \t]*\n((?:[ \t]*-[ \t]*\S.*\n?)+)/m.exec(fm);
138
+ if (block) {
139
+ const items = block[1].split('\n')
140
+ .map((l) => /^[ \t]*-[ \t]*(.+?)[ \t]*$/.exec(l))
141
+ .filter(Boolean).map((x) => x[1].replace(/^['"]|['"]$/g, ''));
142
+ return { requires: items, declared: true, error: null };
143
+ }
144
+ if (/^requires:/m.test(fm)) {
145
+ // Present but unparseable — say so rather than reporting zero requirements,
146
+ // which would read as "declared, needs nothing" (the docs-entropy lesson: a
147
+ // scan that finds nothing must not be indistinguishable from a clean pass).
148
+ return { requires: [], declared: true, error: 'requires: is present but not a list ([a, b] or a dash list)' };
149
+ }
150
+ return { requires: [], declared: false, error: null };
151
+ }
152
+
153
+ // ---- what a skill's fenced commands imply it needs -------------------------
154
+ // FENCED BLOCKS ONLY, and comments stripped. Prose that NAMES a command is
155
+ // discussion — dev/SKILL.md forbids `git push origin main`, and an earlier draft
156
+ // of this scan flagged it for saying so. A fenced line is an instruction to run.
157
+ const SIGNALS = [
158
+ [/(?:^|[\s|(])node\s+scripts\/gds\/push-main\.js\b/m, 'push-credential'],
159
+ [/(?:^|[\s|(])git\s+push\s+origin\s+main\b/m, 'push-credential'],
160
+ [/(?:^|[\s|(])node\s+scripts\/gds\/go-live\.js\b/m, 'push-credential'],
161
+ [/(?:^|[\s|(])ssh\s+(?:-o\s+\S+\s+)?\S+@/m, 'droplet-ssh'],
162
+ [/~\/deploy\.sh/m, 'droplet-ssh'],
163
+ [/(?:^|[\s|(])node\s+scripts\/gds\/merge-lock\.js\b/m, 'main-checkout'],
164
+ [/(?:^|[\s|(])bash\s+scripts\/gds\/smoke-write\.sh\b/m, 'main-checkout'],
165
+ [/(?:^|[\s|(])gh\s+(?:auth|pr|secret|api|run|repo|workflow)\b/m, 'gh'],
166
+ ];
167
+
168
+ function fencedCode(md) {
169
+ const fences = String(md || '').match(/```[a-z]*\n[\s\S]*?```/g) || [];
170
+ return fences
171
+ .map((f) => f.replace(/^```[a-z]*\n/, '').replace(/```$/, ''))
172
+ .join('\n')
173
+ .split('\n')
174
+ .map((l) => l.replace(/(^|\s)#.*$/, '$1')) // a shell comment is discussion
175
+ .join('\n');
176
+ }
177
+
178
+ function inferRequired(md) {
179
+ const code = fencedCode(md);
180
+ const out = new Set();
181
+ for (const [rx, cap] of SIGNALS) if (rx.test(code)) out.add(cap);
182
+ return [...out].sort();
183
+ }
184
+
185
+ // ---- the check ------------------------------------------------------------
186
+ // Returns { ok, missing, unknown, checked }. `missing` is what came back FALSE —
187
+ // the only thing the CLI prints, because the task's own wording is that the
188
+ // preflight must name the MISSING capability, not recite the list.
189
+ function preflight(requires, caps = CAPABILITIES) {
190
+ const missing = [];
191
+ const unknown = [];
192
+ const checked = [];
193
+ for (const name of requires) {
194
+ const cap = caps[name];
195
+ if (!cap) {
196
+ missing.push({ name, why: `unknown capability "${name}" — not in the registry (typo in the skill's requires:?)` });
197
+ continue;
198
+ }
199
+ let verdict;
200
+ try { verdict = cap.probe(); } catch (err) { verdict = null; }
201
+ checked.push(name);
202
+ if (verdict === false) missing.push({ name, why: cap.missing });
203
+ else if (verdict === null) unknown.push({ name, why: `could not confirm ${cap.what} — proceeding, but verify before a destructive step.` });
204
+ }
205
+ return { ok: missing.length === 0, missing, unknown, checked };
206
+ }
207
+
208
+ // A skill name is one path SEGMENT. Containment is checked rather than assumed:
209
+ // `skill-preflight.js ../../../etc` would otherwise read outside .claude/skills.
210
+ // Harmless today (a local, read-only CLI the caller could equally well `cat`), but
211
+ // this helper is exactly the kind that gets reused somewhere less trusting.
212
+ // Returns null for anything that is not a plain segment.
213
+ function skillPath(name) {
214
+ const n = String(name || '');
215
+ if (!n || n !== path.basename(n) || n === '.' || n === '..') return null;
216
+ const full = path.join(REPO_ROOT, '.claude', 'skills', n, 'SKILL.md');
217
+ const base = path.join(REPO_ROOT, '.claude', 'skills') + path.sep;
218
+ return full.startsWith(base) ? full : null;
219
+ }
220
+
221
+ function preflightSkill(name, caps = CAPABILITIES) {
222
+ const p = skillPath(name);
223
+ if (!p) {
224
+ return { ok: false, requires: [], missing: [{ name: 'skill', why: `"${name}" is not a skill name — it must be a single directory name under .claude/skills/.` }], unknown: [], checked: [] };
225
+ }
226
+ let md;
227
+ try { md = fs.readFileSync(p, 'utf8'); } catch {
228
+ return { ok: false, missing: [{ name: 'skill', why: `no skill named "${name}" at .claude/skills/${name}/SKILL.md` }], unknown: [], checked: [] };
229
+ }
230
+ const { requires, error } = parseRequires(md);
231
+ if (error) return { ok: false, missing: [{ name: 'requires', why: `${name}: ${error}` }], unknown: [], checked: [] };
232
+ return { ...preflight(requires, caps), requires };
233
+ }
234
+
235
+ // ---- the assertion (fitness check 23) --------------------------------------
236
+ // A skill whose FENCED commands need a capability must DECLARE it. This is the
237
+ // part that stops the contract rotting: without it, a new ops step gets added to
238
+ // a skill years from now and the declaration silently falls behind the commands.
239
+ //
240
+ // It compares inferRequired() (what the fenced commands imply) against
241
+ // parseRequires() (what the frontmatter claims) and reds the build on a gap. It
242
+ // does NOT complain about a skill declaring MORE than its fences imply — a human
243
+ // knows things the scan cannot see, and punishing an over-declaration would train
244
+ // people to declare less.
245
+ const SKILLS_DIR = path.join(REPO_ROOT, '.claude', 'skills');
246
+
247
+ function checkOpsSkillsDeclareMachine({ dir = SKILLS_DIR, read = null, names: injected = null } = {}) {
248
+ const violations = [];
249
+ const warnings = [];
250
+ let names = injected;
251
+ if (!names) {
252
+ try {
253
+ names = fs.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
254
+ } catch { names = []; }
255
+ }
256
+ // A scan that enumerates nothing must fail, not pass (the docs-entropy lesson).
257
+ if (!names.length) {
258
+ return {
259
+ name: 'ops skills declare the machine they need (G03)',
260
+ ok: false, hardFail: true,
261
+ violations: ['scan defect — enumerated 0 skills under .claude/skills/.'],
262
+ warnings: [], note: 'static scan of skill frontmatter vs the commands each skill actually fences.',
263
+ };
264
+ }
265
+ const readOne = read || ((n) => {
266
+ try { return fs.readFileSync(path.join(dir, n, 'SKILL.md'), 'utf8'); } catch { return null; }
267
+ });
268
+
269
+ let declaring = 0;
270
+ for (const n of names) {
271
+ const md = readOne(n);
272
+ if (md === null) continue;
273
+ const { requires, error } = parseRequires(md);
274
+ if (error) { violations.push(`${n}: ${error}`); continue; }
275
+ if (requires.length) declaring += 1;
276
+ for (const r of requires) {
277
+ if (!CAPABILITIES[r]) {
278
+ violations.push(`${n} declares an unknown capability "${r}" — it can never be probed, so the declaration is decorative. Registry: ${Object.keys(CAPABILITIES).join(', ')}.`);
279
+ }
280
+ }
281
+ // DECLARING is not ENFORCING. planning-session shipped a `requires:` with no
282
+ // preflight call, which is the same shape as the original bug: a contract
283
+ // stated where nothing checks it. If a skill names a machine, it must also
284
+ // run the check (the grader caught this one on the first pass).
285
+ if (requires.length && !/skill-preflight\.js\s+\S/.test(md)) {
286
+ violations.push(
287
+ `${n} declares requires: [${requires.join(', ')}] but never runs \`node scripts/gds/skill-preflight.js ${n}\`. `
288
+ + 'A declaration nothing enforces is prose, and prose cannot refuse — the exact failure this check exists for.'
289
+ );
290
+ }
291
+ const inferred = inferRequired(md);
292
+ const undeclared = inferred.filter((c) => !requires.includes(c));
293
+ if (undeclared.length) {
294
+ violations.push(
295
+ `${n} fences commands needing [${undeclared.join(', ')}] but does not declare them. `
296
+ + 'Add them to `requires:` in the frontmatter so the preflight can refuse early — '
297
+ + 'prose cannot refuse, which is how /merge-mode came to be run on a dev box (task 1003167).'
298
+ );
299
+ }
300
+ }
301
+ return {
302
+ name: 'ops skills declare the machine they need (G03)',
303
+ ok: violations.length === 0,
304
+ hardFail: violations.length > 0,
305
+ violations,
306
+ warnings,
307
+ note: `${names.length} skill(s) scanned; ${declaring} declare a machine contract. A skill whose FENCED commands need gh / a main checkout / a push credential / the deploy host must name it in \`requires:\`, so \`node scripts/gds/skill-preflight.js <skill>\` can refuse before the procedure starts. Prose in a skill body cannot refuse.`,
308
+ };
309
+ }
310
+
311
+ function main() {
312
+ const name = process.argv[2];
313
+ if (!name || name.startsWith('--')) {
314
+ console.error('usage: node scripts/gds/skill-preflight.js <skill-name>');
315
+ console.error(' exits 1 naming the capability this machine is MISSING for that skill.');
316
+ process.exit(2);
317
+ }
318
+ const r = preflightSkill(name);
319
+ for (const u of r.unknown) console.error(`? ${u.name}: ${u.why}`);
320
+ if (r.ok) {
321
+ console.log(`preflight OK — ${name}: ${r.checked.length ? r.checked.join(', ') : 'no machine requirements declared'}.`);
322
+ process.exit(0);
323
+ }
324
+ console.error(`STOP — /${name} cannot run on this machine.`);
325
+ for (const m of r.missing) console.error(`✗ ${m.name}: ${m.why}`);
326
+ process.exit(1);
327
+ }
328
+
329
+ if (require.main === module) main();
330
+
331
+ module.exports = {
332
+ CAPABILITIES, SIGNALS,
333
+ parseRequires, inferRequired, fencedCode,
334
+ preflight, preflightSkill, skillPath, checkOpsSkillsDeclareMachine,
335
+ ghAuthed, binaryPresent,
336
+ };
package/src/module-api.js CHANGED
@@ -1057,8 +1057,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1057
1057
  // landed since 1.19.313 with no explicit bump. run 32668599111. (task 1002620)
1058
1058
  // 1.19.315 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1059
1059
  // landed since 1.19.314 with no explicit bump. run 32668969037. (task 1002620)
1060
+ // 1.19.316 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1061
+ // landed since 1.19.315 with no explicit bump. run 32669347711. (task 1002620)
1062
+ // 1.19.317 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1063
+ // landed since 1.19.316 with no explicit bump. run 32669571610. (task 1002620)
1060
1064
  // ---------------------------------------------------------------------------
1061
- const CORE_VERSION = '1.19.315'; // CI auto-patch carrier (ADR 0161); see changelog above
1065
+ const CORE_VERSION = '1.19.317'; // CI auto-patch carrier (ADR 0161); see changelog above
1062
1066
 
1063
1067
  // A namespaced logger so a module's log lines are attributable + consistent.
1064
1068
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -34,7 +34,7 @@
34
34
  import { strict as assert } from 'node:assert';
35
35
  import { createRequire } from 'node:module';
36
36
  import { readFileSync } from 'node:fs';
37
- import { makeRunner } from './helpers.mjs';
37
+ import { makeRunner, lifecycleDbSource } from './helpers.mjs';
38
38
 
39
39
  process.env.NODE_ENV = 'test';
40
40
  const require = createRequire(import.meta.url);
@@ -113,7 +113,8 @@ await test('EXEC: every write goes through the caller-supplied exec (so a ship t
113
113
  });
114
114
 
115
115
  await test('EXEC: shipTask passes its transaction CLIENT, and the reconciler passes the pool', () => {
116
- const db = readFileSync(new URL('../modules/lifecycle/db.js', import.meta.url), 'utf8');
116
+ // task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
117
+ const db = lifecycleDbSource();
117
118
  // db.js is over the fitness oversize threshold, so the hook is ONE line there
118
119
  // and its body lives in done-when.closeCompletedWorkForShip. What must hold is
119
120
  // that the thing handed in is the txn client, not a pool.
@@ -13,7 +13,7 @@ import { strict as assert } from 'node:assert';
13
13
  import { readFileSync } from 'node:fs';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { dirname, join } from 'node:path';
16
- import { makeRunner } from './helpers.mjs';
16
+ import { makeRunner, lifecycleDbSource } from './helpers.mjs';
17
17
 
18
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
19
  const repoRoot = join(__dirname, '..');
@@ -22,7 +22,8 @@ const read = (...p) => readFileSync(join(repoRoot, ...p), 'utf8');
22
22
  const mig = read('migrations', '163_polymorphic_dependencies.sql');
23
23
  const migN = mig.replace(/\s+/g, ' ');
24
24
  // the task-dependency CRUD + the claim-gate slice carved to the lifecycle module at BV1.R86.
25
- const dbSrc = read('modules', 'lifecycle', 'db.js');
25
+ // task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
26
+ const dbSrc = lifecycleDbSource();
26
27
 
27
28
  const { test, summary } = makeRunner();
28
29
 
package/tests/fitness.mjs CHANGED
@@ -542,6 +542,49 @@ test('identity gate skips comments + instance-injected tokens (no false positive
542
542
  assert.equal(scanPromptIdentity('x.html', '<title>{{worldName}} — the gate</title>').length, 0, 'a {{worldName}} token carries no literal');
543
543
  });
544
544
 
545
+ // task 1002756 — the two holes that let migrate.sh's founder DB default survive:
546
+ // (1) the stripper could be OPENED by a `/*` inside a `//` line comment (a real
547
+ // `*/` hundreds of lines later then swallowed everything between — provision.js
548
+ // lost ~330 lines from the scanned text), and (2) whole file classes were never
549
+ // scanned. These lock the stripper; the root widening is pinned by the note test.
550
+ test('identity gate: a /* inside a line comment cannot open a phantom block (task 1002756)', () => {
551
+ const { scanPromptIdentity } = fitness;
552
+ const src = [
553
+ '// glob note: import <dir>/*.caddy', // the phantom opener shape
554
+ 'const leak = "amazonprimea.example";', // must STILL be seen
555
+ 'doThing();',
556
+ '/* a real block */ const ok = 1;',
557
+ ].join('\n');
558
+ assert.equal(scanPromptIdentity('x.js', src).length, 1, 'the literal after a /*-bearing line comment must be caught');
559
+ });
560
+
561
+ test('identity gate: a // inside a block comment does not truncate the block', () => {
562
+ const { scanPromptIdentity } = fitness;
563
+ const src = [
564
+ '/* rationale: see https://amazonprimea.com — historical // note',
565
+ 'still inside the block, amazonprimea again',
566
+ '*/',
567
+ 'const clean = 1;',
568
+ ].join('\n');
569
+ assert.equal(scanPromptIdentity('x.js', src).length, 0, 'comment prose naming the brand must not trip the scan');
570
+ });
571
+
572
+ test('identity gate: comment markers inside strings and regex literals are inert', () => {
573
+ const { scanPromptIdentity } = fitness;
574
+ // A string containing /* must not open a comment that hides a later literal…
575
+ assert.equal(scanPromptIdentity('x.js', 'const g = "src/*.js";\nconst leak = "amazonprimea";').length, 1);
576
+ // …a regex containing the brand is code and IS caught (the detectors carve out their own hits)…
577
+ assert.equal(scanPromptIdentity('x.js', 'const re = /amazonprimea/i;').length, 1);
578
+ // …and a division sign is not mistaken for a regex opener that swallows code.
579
+ assert.equal(scanPromptIdentity('x.js', 'const x = a / b / c;\nconst leak = "amazonprimea";').length, 1);
580
+ });
581
+
582
+ test('identity gate scans the WHOLE scripts/ root, not just scripts/gds (task 1002756)', () => {
583
+ const r = fitness.checkKernelPromptsNoHardcodedIdentity();
584
+ assert.match(r.note, /scripts\/\*\* \(whole root/, 'note must document the widened root');
585
+ assert.equal(r.hardFail, false, 'live tree must be clean under the widened scan:\n' + r.violations.join('\n'));
586
+ });
587
+
545
588
  // --- write-route validation build check (BV1.R12 / task 1999, ADR 0118) ---
546
589
 
547
590
  test('write-route validation check: the live tree is clean (R11 sweep reached 100%)', () => {
@@ -46,7 +46,7 @@
46
46
  import { strict as assert } from 'node:assert';
47
47
  import { createRequire } from 'node:module';
48
48
  import { readFileSync } from 'node:fs';
49
- import { makeRunner } from './helpers.mjs';
49
+ import { makeRunner, lifecycleDbSource } from './helpers.mjs';
50
50
 
51
51
  process.env.NODE_ENV = 'test';
52
52
 
@@ -201,7 +201,8 @@ await test('[Q6] non-member on a public PROTECTED-scope goal → action=apply (s
201
201
  assert.equal(r.action, 'apply', 'a protected-scope goal is never offered as open-join');
202
202
  });
203
203
  await test('[Q7] source contract: BOTH INSERT INTO claims sites run the extracted gate (batch cannot bypass)', () => {
204
- const dbSrc = readFileSync(new URL('../modules/lifecycle/db.js', import.meta.url), 'utf8');
204
+ // task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
205
+ const dbSrc = lifecycleDbSource();
205
206
  const insertSites = dbSrc.match(/INSERT INTO claims \(/g) || [];
206
207
  assert.equal(insertSites.length, 2, 'exactly two claim-insert sites (claimTask + claimTasksBatch) — a third needs the gate too');
207
208
  const gateCalls = dbSrc.match(/await goalMembershipFailures\(/g) || [];
@@ -17,7 +17,7 @@ import { strict as assert } from 'node:assert';
17
17
  import { createRequire } from 'node:module';
18
18
  import { readFileSync } from 'node:fs';
19
19
  import http from 'node:http';
20
- import { makeRunner } from './helpers.mjs';
20
+ import { makeRunner, lifecycleDbSource } from './helpers.mjs';
21
21
 
22
22
  process.env.NODE_ENV = 'test';
23
23
 
@@ -25,7 +25,8 @@ const require = createRequire(import.meta.url);
25
25
  const { adviseGoal, candidateGoals, generalGoalTitle, buildGoalAdvisory } = require('../modules/lifecycle/goal-advisory.js');
26
26
 
27
27
  const tasksSrc = readFileSync(new URL('../modules/lifecycle/routes/tasks.js', import.meta.url), 'utf8');
28
- const dbSrc = readFileSync(new URL('../modules/lifecycle/db.js', import.meta.url), 'utf8');
28
+ // task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
29
+ const dbSrc = lifecycleDbSource();
29
30
  const { test, summary } = makeRunner();
30
31
 
31
32
  const V = 'BONGOS-V1';
@@ -37,6 +37,7 @@ const LIFECYCLE = path.join(ROOT, 'modules', 'lifecycle');
37
37
  const eligibility = require(path.join(LIFECYCLE, 'claim-eligibility.js'));
38
38
  const db = require(path.join(LIFECYCLE, 'db.js'));
39
39
  const catalog = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
40
+ import { lifecycleDbSource } from './helpers.mjs';
40
41
 
41
42
  const { CLAIM_ANY, CLAIM_NEWCOMER, permissionAllowsClaim, claimEligibilityAllows } = eligibility;
42
43
 
@@ -147,7 +148,8 @@ test('the permission predicate cannot see requires_rank at all (structural)', ()
147
148
  });
148
149
 
149
150
  test('every claim path in db.js still gates on requires_rank as its own check', () => {
150
- const src = fs.readFileSync(path.join(LIFECYCLE, 'db.js'), 'utf8');
151
+ // task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
152
+ const src = lifecycleDbSource();
151
153
  // claimTask, claimTasksBatch, validateClaimBatch — three call sites, three
152
154
  // separate rankAllowsTask conjuncts. If a refactor ever folds the floor into the
153
155
  // permission predicate, this count drops and the test reds.
@@ -18,6 +18,7 @@ import { createRequire } from 'node:module';
18
18
  import fs from 'node:fs';
19
19
  import path from 'node:path';
20
20
  import { fileURLToPath } from 'node:url';
21
+ import { lifecycleDbSource } from './helpers.mjs';
21
22
 
22
23
  const require = createRequire(import.meta.url);
23
24
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -189,8 +190,14 @@ test('the writer runs on the CALLER\'s transaction client when given one', () =>
189
190
  });
190
191
 
191
192
  test('both rank-change twins rotate the role inside their transaction', () => {
192
- for (const rel of ['src/bongos/db.js', 'modules/lifecycle/db.js']) {
193
- const src = fs.readFileSync(path.join(ROOT, rel), 'utf8');
193
+ // task 1003204: the lifecycle twin moved out of db.js into db-rank-authz.js
194
+ // when the monolith was carved up. Read the whole lifecycle family so the
195
+ // assertion follows the code instead of a filename.
196
+ const twins = [
197
+ ['src/bongos/db.js', fs.readFileSync(path.join(ROOT, 'src/bongos/db.js'), 'utf8')],
198
+ ['modules/lifecycle/db-*.js', lifecycleDbSource()],
199
+ ];
200
+ for (const [rel, src] of twins) {
194
201
  const body = src.slice(src.indexOf('async function _applyRankChangeTx'));
195
202
  const upd = body.indexOf('UPDATE builders SET rank');
196
203
  const sync = body.indexOf('await syncGovernanceRankRole(client');
package/tests/helpers.mjs CHANGED
@@ -123,3 +123,22 @@ export function requireHostModule(require, spec, note) {
123
123
  throw err;
124
124
  }
125
125
  }
126
+
127
+ // lifecycleDbSource — the LIFECYCLE data layer as one source string.
128
+ //
129
+ // task 1003204 (goal 1000079) split the 5,748-line modules/lifecycle/db.js into
130
+ // db.js (a pure re-export facade) + twelve cohesive db-*.js units. A pile of
131
+ // tests assert things about "the data layer" by regexing its source — that the
132
+ // claim paths gate on requires_rank, that a literal appears in exactly one
133
+ // place, that dependency CRUD points at the right table. Those assertions were
134
+ // always about the LAYER, not about one filename, so they read the whole family
135
+ // here. Reading the family (rather than repointing each test at whichever db-*
136
+ // unit happens to hold the code today) also means a future re-carve moves code
137
+ // between units without silently disarming the assertion.
138
+ export function lifecycleDbSource(modulesDir = path.join(ROOT, 'modules')) {
139
+ const dir = path.join(modulesDir, 'lifecycle');
140
+ const files = fs.readdirSync(dir)
141
+ .filter((f) => f === 'db.js' || (f.startsWith('db-') && f.endsWith('.js')))
142
+ .sort();
143
+ return files.map((f) => fs.readFileSync(path.join(dir, f), 'utf8')).join('\n');
144
+ }
@@ -28,6 +28,7 @@ import { readFileSync } from 'node:fs';
28
28
  import { fileURLToPath } from 'node:url';
29
29
  import { dirname, join } from 'node:path';
30
30
  import { createRequire } from 'node:module';
31
+ import { lifecycleDbSource } from './helpers.mjs';
31
32
 
32
33
  const __dirname = dirname(fileURLToPath(import.meta.url));
33
34
  const repoRoot = join(__dirname, '..');
@@ -49,7 +50,8 @@ async function test(name, fn) {
49
50
  const read = (...p) => readFileSync(join(repoRoot, ...p), 'utf8');
50
51
  const migrationSql = read('migrations', '081_tasks_needs_migration.sql');
51
52
  // the tasks db slice + the tasks route carved to modules/lifecycle/ at BV1.R86.
52
- const dbSrc = read('modules', 'lifecycle', 'db.js');
53
+ // task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
54
+ const dbSrc = lifecycleDbSource();
53
55
  const routesSrc = read('modules', 'lifecycle', 'routes', 'tasks.js');
54
56
  const captureSrc = read('scripts', 'gds', 'capture.js');
55
57
 
@@ -16,6 +16,7 @@ import { createRequire } from 'node:module';
16
16
 
17
17
  const require = createRequire(import.meta.url);
18
18
  const reconciler = require('../modules/lifecycle/publish-reconciler.js');
19
+ import { lifecycleDbSource } from './helpers.mjs';
19
20
 
20
21
  let passed = 0;
21
22
  let failed = 0;
@@ -1647,7 +1648,8 @@ test('vector 2: shipped_at is stamped ONLY by shipTask, and shipTask requires co
1647
1648
  const url = await import('node:url');
1648
1649
  const path = await import('node:path');
1649
1650
  const ROOT = path.dirname(path.dirname(url.fileURLToPath(import.meta.url)));
1650
- const dbSrc = fs.readFileSync(path.join(ROOT, 'modules/lifecycle/db.js'), 'utf8');
1651
+ // task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
1652
+ const dbSrc = lifecycleDbSource();
1651
1653
 
1652
1654
  // Every statement that writes shipped_at, ignoring SELECT projections and comments.
1653
1655
  const writes = [...dbSrc.matchAll(/shipped_at\s*=\s*now\(\)/g)];
@@ -26,6 +26,7 @@ const require = createRequire(import.meta.url);
26
26
  const auth = require('../src/bongos/auth.js');
27
27
  const db = require('../modules/lifecycle/db.js');
28
28
  const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
29
+ import { lifecycleDbSource } from './helpers.mjs';
29
30
 
30
31
  let passed = 0;
31
32
  let failed = 0;
@@ -319,7 +320,8 @@ console.log('\nlistClaimableTasks SQL rank-tier CASE (regression: thetes drift):
319
320
 
320
321
  const __dir = dirname(fileURLToPath(import.meta.url));
321
322
  // listClaimableTasks carved to the lifecycle module at BV1.R86 — read the SQL there.
322
- const dbSrc = readFileSync(join(__dir, '..', 'modules', 'lifecycle', 'db.js'), 'utf8');
323
+ // task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
324
+ const dbSrc = lifecycleDbSource();
323
325
 
324
326
  t('every live rank appears in the claimable CASE at its REQUIRES_RANK_TIER value', () => {
325
327
  // Isolate the listClaimableTasks function body so we don't match CASE
@@ -25,6 +25,7 @@ const db = require('../modules/lifecycle/db.js');
25
25
  // the SAME singleton object db.js captured, so mutating its .query stubs the INSERT
26
26
  // createTask runs — no database needed.
27
27
  const sharedPool = require('../src/module-api.js').pool;
28
+ import { lifecycleDbSource } from './helpers.mjs';
28
29
 
29
30
  const __dir = dirname(fileURLToPath(import.meta.url));
30
31
  const root = join(__dir, '..');
@@ -132,7 +133,8 @@ t('null task is a no-op (no throw, no assignment)', () => {
132
133
  console.log('\nenforcement points (source-wiring — AUTO-ASSIGN):');
133
134
 
134
135
  const tasksRouteSrc = readFileSync(join(root, 'modules/lifecycle/routes/tasks.js'), 'utf8');
135
- const dbSrc = readFileSync(join(root, 'modules/lifecycle/db.js'), 'utf8');
136
+ // task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
137
+ const dbSrc = lifecycleDbSource();
136
138
  const migration178 = readFileSync(join(root, 'migrations/178_reward_gate_auto_assign_revert_autopromote.sql'), 'utf8');
137
139
 
138
140
  t('promote route AUTO-ASSIGNS then proceeds (no 409 refusal for missing reward)', () => {