@bongos/core 1.19.638 → 1.19.640
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 +53 -33
- package/docs/adr/0275-one-source-for-a-written-role-responsibility.md +50 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +24 -23
- package/docs/copy-registry.json +39 -30
- package/docs/file-map.md +3 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/packs/artist.md +6 -2
- package/docs/packs/engineer.md +10 -0
- package/docs/packs/ideator.md +10 -0
- package/modules/builder-settings/builder-needs.js +6 -3
- package/modules/discord/board-broadcast.js +10 -3
- package/modules/grading/grader-prompt.js +14 -0
- package/modules/hall-ui/public/profile.css +17 -0
- package/modules/hall-ui/public/profile.js +25 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +4 -0
- package/scripts/gds/gen-role-responsibilities.js +134 -0
- package/scripts/gds/role-pack-guard.js +21 -0
- package/scripts/gds/sequence.js +111 -3
- package/src/module-api.js +30 -1
- package/src/modules.js +12 -1
- package/src/role-responsibilities.js +80 -0
- package/tests/discord_board_broadcast.mjs +4 -2
- package/tests/government_board_hash_redirect.mjs +60 -0
- package/tests/government_board_summons.mjs +8 -4
- package/tests/module_api.mjs +1 -0
- package/tests/role_responsibilities.mjs +187 -0
- package/tests/sequence.mjs +157 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// tests/role_responsibilities.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1003732 (goal 1000095, criterion wa6-written-responsibilities) — the three
|
|
4
|
+
// written role responsibilities land as ONE source with three consumers.
|
|
5
|
+
//
|
|
6
|
+
// WHAT IS WORTH PINNING HERE. The criterion's whole content is "one source, shown
|
|
7
|
+
// on the profile, injected into the pack, referenced by grading". So the failure to
|
|
8
|
+
// guard against is not a typo — it is a SECOND COPY. If the packs, the hall and the
|
|
9
|
+
// grader can each hold their own wording, the project can show a builder one
|
|
10
|
+
// standard and judge them by another, which is the specific unfairness the single
|
|
11
|
+
// source exists to prevent. Every test below is a leg of that.
|
|
12
|
+
//
|
|
13
|
+
// The text itself is the owner's, verbatim (fixed 2026-09-08). It is asserted here
|
|
14
|
+
// character-for-character on purpose: a well-meaning tidy-up of someone else's
|
|
15
|
+
// authored sentence is exactly the drift this task forbids, and a test is the only
|
|
16
|
+
// thing that makes "do not paraphrase" survive contact with a future editor.
|
|
17
|
+
//
|
|
18
|
+
// Run: node tests/role_responsibilities.mjs
|
|
19
|
+
|
|
20
|
+
import { strict as assert } from 'node:assert';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
const src = require(path.join(ROOT, 'src', 'role-responsibilities.js'));
|
|
29
|
+
const modules = require(path.join(ROOT, 'src', 'modules.js'));
|
|
30
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
31
|
+
const gen = require(path.join(ROOT, 'scripts', 'gds', 'gen-role-responsibilities.js'));
|
|
32
|
+
|
|
33
|
+
let passed = 0;
|
|
34
|
+
let failed = 0;
|
|
35
|
+
function test(name, fn) {
|
|
36
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
37
|
+
catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// --- the source ---------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
// The owner's sentences, verbatim from criterion wa6-written-responsibilities.
|
|
43
|
+
// Duplicated here deliberately — a test that read the value it is checking would
|
|
44
|
+
// assert nothing. This copy is the fixture; the source file is the subject.
|
|
45
|
+
const OWNER_TEXT = {
|
|
46
|
+
engineer: 'Running and optimizing the running of Claude nonstop, and ensuring Ideators and Artists can continue to interface with that system effectively.',
|
|
47
|
+
artist: 'No slop in the appearance and text of the project; the story, emotion and ideology of the project are communicated effectively; project purpose and gravitas are upheld.',
|
|
48
|
+
ideator: 'Make good ideas; be a philosopher / thought leader for the project; enable Engineers to scope and Artists to create with maximum efficiency — a baseline for creating scopes of work.',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
test('the three statements are the owner\'s text, character for character', () => {
|
|
52
|
+
assert.deepEqual({ ...src.ROLE_RESPONSIBILITIES }, OWNER_TEXT);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('exactly the three CORE crafts carry a statement — no more, no fewer', () => {
|
|
56
|
+
assert.deepEqual(Object.keys(src.ROLE_RESPONSIBILITIES), ['engineer', 'artist', 'ideator']);
|
|
57
|
+
// Governor is deferred (ADR 0274) and `ui` is module-contributed (ADR 0272):
|
|
58
|
+
// inventing a sentence for either is the paraphrase this task forbids.
|
|
59
|
+
assert.equal(src.responsibilityFor('ui'), null);
|
|
60
|
+
assert.equal(src.responsibilityFor('governor'), null);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('the map is frozen — a consumer cannot edit the standard at runtime', () => {
|
|
64
|
+
assert.ok(Object.isFrozen(src.ROLE_RESPONSIBILITIES));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('responsibilityFor: case-insensitive, and absence is null not a placeholder', () => {
|
|
68
|
+
assert.equal(src.responsibilityFor('ENGINEER'), OWNER_TEXT.engineer);
|
|
69
|
+
for (const bad of [null, undefined, '', 'unclassified', 42, {}]) {
|
|
70
|
+
assert.equal(src.responsibilityFor(bad), null, `${JSON.stringify(bad)} must be null`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('responsibilitiesFor scopes to the disciplines an instance offers', () => {
|
|
75
|
+
assert.deepEqual(src.responsibilitiesFor(['artist', 'ui']), { artist: OWNER_TEXT.artist });
|
|
76
|
+
assert.deepEqual(src.responsibilitiesFor([]), {});
|
|
77
|
+
assert.deepEqual(src.responsibilitiesFor(null), {});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// --- consumer 1: the hall (via the injected client projection) ----------
|
|
81
|
+
|
|
82
|
+
test('clientModules carries the statements for the offered crafts only', () => {
|
|
83
|
+
const c = modules.clientModules();
|
|
84
|
+
for (const d of Object.keys(c.responsibilities)) {
|
|
85
|
+
assert.ok(c.disciplines.includes(d), `${d} is offered a statement but is not an offered discipline`);
|
|
86
|
+
assert.equal(c.responsibilities[d], OWNER_TEXT[d], `${d}: the projection must not reword the source`);
|
|
87
|
+
}
|
|
88
|
+
// `ui` is offered by the ui-design module but has no statement — the projection
|
|
89
|
+
// must carry the craft without inventing a standard for it.
|
|
90
|
+
if (c.disciplines.includes('ui')) assert.equal(c.responsibilities.ui, undefined);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// --- consumer 2: the grader (via the module doorway) --------------------
|
|
94
|
+
|
|
95
|
+
test('the doorway exposes it, so a module never deep-requires the source', () => {
|
|
96
|
+
assert.equal(api.responsibilityFor('artist'), OWNER_TEXT.artist);
|
|
97
|
+
assert.deepEqual({ ...api.ROLE_RESPONSIBILITIES }, OWNER_TEXT);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('the grader prompt states the standard for a craft, and stays silent without one', () => {
|
|
101
|
+
const { buildPrompt } = require(path.join(ROOT, 'modules', 'grading', 'grader-prompt.js'));
|
|
102
|
+
const build = (discipline) => buildPrompt({
|
|
103
|
+
task: { id: 1, title: 't', kind: 'feature', discipline },
|
|
104
|
+
valueSummary: 'v', changedFiles: ['a.js'], diff: 'x',
|
|
105
|
+
});
|
|
106
|
+
assert.ok(build('artist').includes(OWNER_TEXT.artist), 'the artist statement must reach the prompt verbatim');
|
|
107
|
+
// A craft with no written standard must not get an invented one — a grader told
|
|
108
|
+
// "this role has no standard" would supply its own.
|
|
109
|
+
for (const d of ['ui', 'unclassified', undefined]) {
|
|
110
|
+
assert.ok(!build(d).includes('What this craft is answerable for'), `${d} must add no responsibility line`);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// --- consumer 3: the packs (generated, so they cannot drift) ------------
|
|
115
|
+
|
|
116
|
+
test('every pack the registry names carries the block, matching the source', () => {
|
|
117
|
+
const { writes, warnings } = gen.plan();
|
|
118
|
+
assert.deepEqual(writes.map((w) => w.rel), [], `a pack block is stale: ${writes.map((w) => w.rel).join(', ')}`);
|
|
119
|
+
assert.deepEqual(warnings, [], `every registered pack must carry the marker pair: ${warnings.join(' | ')}`);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('the block carries the statement verbatim and says it is generated', () => {
|
|
123
|
+
for (const t of gen.packTargets()) {
|
|
124
|
+
const text = fs.readFileSync(t.abs, 'utf8');
|
|
125
|
+
const expected = OWNER_TEXT[t.discipline];
|
|
126
|
+
if (!expected) continue;
|
|
127
|
+
assert.ok(text.includes(expected), `${t.rel} must quote the ${t.discipline} statement verbatim`);
|
|
128
|
+
assert.ok(text.includes(gen.BEGIN) && text.includes(gen.END), `${t.rel} must keep the marker pair`);
|
|
129
|
+
assert.ok(/do not hand-edit/i.test(text), `${t.rel} must warn that the block is generated`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('the generator is idempotent and preserves a CRLF pack\'s line endings', () => {
|
|
134
|
+
// The packs are .md: CRLF on a Windows checkout, LF in CI. A generator that
|
|
135
|
+
// always wrote '\n' would make the committed bytes platform-dependent, so the
|
|
136
|
+
// freshness gate would be red on one checkout and green on the other.
|
|
137
|
+
const block = gen.blockFor('artist');
|
|
138
|
+
const crlf = `head\r\n${gen.BEGIN}\r\nstale\r\n${gen.END}\r\ntail\r\n`;
|
|
139
|
+
const out = gen.inject(crlf, block);
|
|
140
|
+
assert.ok(!/[^\r]\n/.test(out), 'a CRLF file must stay pure CRLF');
|
|
141
|
+
assert.equal(gen.inject(out, block), out, 'a second pass must change nothing');
|
|
142
|
+
|
|
143
|
+
const lf = `head\n${gen.BEGIN}\nstale\n${gen.END}\ntail\n`;
|
|
144
|
+
const outLf = gen.inject(lf, block);
|
|
145
|
+
assert.ok(!outLf.includes('\r'), 'an LF file must stay pure LF');
|
|
146
|
+
assert.equal(gen.inject(outLf, block), outLf, 'a second pass must change nothing');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('a pack with no markers is reported, never silently skipped', () => {
|
|
150
|
+
assert.equal(gen.inject('no markers here', gen.blockFor('artist')), null);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// --- the whole point: there is no second copy ---------------------------
|
|
154
|
+
|
|
155
|
+
test('no consumer hardcodes a statement — the packs\' generated blocks are the only copies', () => {
|
|
156
|
+
// Anything that repeats a statement outside the source, its generated blocks, the
|
|
157
|
+
// criterion record, or this fixture is a second copy that can drift.
|
|
158
|
+
const ALLOWED = new Set([
|
|
159
|
+
'src/role-responsibilities.js', // the source
|
|
160
|
+
'docs/packs/engineer.md', // generated blocks
|
|
161
|
+
'docs/packs/artist.md',
|
|
162
|
+
'docs/packs/ideator.md',
|
|
163
|
+
'tests/role_responsibilities.mjs', // this fixture
|
|
164
|
+
]);
|
|
165
|
+
const scan = ['src', 'scripts', 'modules', 'tests', 'docs/packs'];
|
|
166
|
+
const offenders = [];
|
|
167
|
+
const walk = (dir) => {
|
|
168
|
+
let entries = [];
|
|
169
|
+
try { entries = fs.readdirSync(path.join(ROOT, dir), { withFileTypes: true }); } catch { return; }
|
|
170
|
+
for (const e of entries) {
|
|
171
|
+
const rel = `${dir}/${e.name}`;
|
|
172
|
+
if (e.isDirectory()) { if (e.name !== 'node_modules') walk(rel); continue; }
|
|
173
|
+
if (!/\.(js|mjs|md|json|html)$/.test(e.name)) continue;
|
|
174
|
+
if (ALLOWED.has(rel)) continue;
|
|
175
|
+
let text;
|
|
176
|
+
try { text = fs.readFileSync(path.join(ROOT, rel), 'utf8'); } catch { continue; }
|
|
177
|
+
for (const [craft, sentence] of Object.entries(OWNER_TEXT)) {
|
|
178
|
+
if (text.includes(sentence)) offenders.push(`${rel} repeats the ${craft} statement`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
for (const d of scan) walk(d);
|
|
183
|
+
assert.deepEqual(offenders, [], `a second copy can drift from the source:\n ${offenders.join('\n ')}`);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
console.log(`\nrole_responsibilities: ${passed} passed, ${failed} failed`);
|
|
187
|
+
if (failed > 0) process.exit(1);
|
package/tests/sequence.mjs
CHANGED
|
@@ -16,10 +16,15 @@ const {
|
|
|
16
16
|
pickNext,
|
|
17
17
|
summarize,
|
|
18
18
|
MIN_SPEC_CHARS,
|
|
19
|
+
fetchGoalTasks,
|
|
20
|
+
fetchGoalTaskRows,
|
|
21
|
+
declaredSurfaceExcludesMigrations,
|
|
22
|
+
GOAL_PAGE,
|
|
19
23
|
} = require('../scripts/gds/sequence.js');
|
|
20
24
|
|
|
21
25
|
let passed = 0, failed = 0;
|
|
22
26
|
const t = (n, f) => { try { f(); passed++; console.log(` PASS ${n}`); } catch (e) { failed++; console.log(` FAIL ${n}\n ${e.message}`); } };
|
|
27
|
+
const ta = async (n, f) => { try { await f(); passed++; console.log(` PASS ${n}`); } catch (e) { failed++; console.log(` FAIL ${n}\n ${e.message}`); } };
|
|
23
28
|
|
|
24
29
|
// A description long enough to clear the thin-spec gate, so fixtures testing
|
|
25
30
|
// OTHER halt reasons don't trip it incidentally.
|
|
@@ -327,5 +332,157 @@ t('pickNext on an empty plan returns no pick and nothing skipped', () => {
|
|
|
327
332
|
assert.equal(r.head, null);
|
|
328
333
|
});
|
|
329
334
|
|
|
335
|
+
// ------------------------------------------------- goal paging (task 1003699)
|
|
336
|
+
|
|
337
|
+
// A fake board big enough to have the cliff the bug fell off. The UNFILTERED
|
|
338
|
+
// listing is capped at UNFILTERED_CAP rows exactly as the real route caps
|
|
339
|
+
// ?limit at 1000, so a row past it is genuinely unreachable that way — which is
|
|
340
|
+
// what makes the acceptance test meaningful rather than a small-instance smoke.
|
|
341
|
+
const UNFILTERED_CAP = 1000;
|
|
342
|
+
|
|
343
|
+
function fakeBoard({ goalId = 42, rowsInGoal = 3, targetIndex = 1100, total = 1200, failPages = [] } = {}) {
|
|
344
|
+
const table = [];
|
|
345
|
+
for (let i = 0; i < total; i += 1) {
|
|
346
|
+
table.push({ id: 10000 + i, title: `filler ${i}`, status: 'ready', goal_id: 999, touches: [] });
|
|
347
|
+
}
|
|
348
|
+
// The task under test sits PAST the unfiltered cap, and is in the goal.
|
|
349
|
+
table[targetIndex] = { id: 777, title: 'the task past the window', status: 'ready', goal_id: goalId, touches: [] };
|
|
350
|
+
for (let k = 1; k < rowsInGoal; k += 1) {
|
|
351
|
+
table[targetIndex - k] = { id: 800 + k, title: `sibling ${k}`, status: 'ready', goal_id: goalId, touches: [] };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const calls = [];
|
|
355
|
+
const api = {
|
|
356
|
+
tasks: {
|
|
357
|
+
async getTasks({ query } = {}) {
|
|
358
|
+
const q = query || {};
|
|
359
|
+
calls.push({ ...q });
|
|
360
|
+
if (q.offset === 0) return { ok: false, status: 400 }; // the real route's rule
|
|
361
|
+
const page = calls.length - 1;
|
|
362
|
+
if (failPages.includes(page)) return { ok: false, status: 500 };
|
|
363
|
+
const rows = q.goal_id === undefined
|
|
364
|
+
? table.slice(0, UNFILTERED_CAP)
|
|
365
|
+
: table.filter((r) => Number(r.goal_id) === Number(q.goal_id));
|
|
366
|
+
const off = Number(q.offset) || 0;
|
|
367
|
+
const lim = Number(q.limit) || rows.length;
|
|
368
|
+
return { ok: true, data: { tasks: rows.slice(off, off + lim) } };
|
|
369
|
+
},
|
|
370
|
+
async getTasksId({ id }) {
|
|
371
|
+
const row = table.find((r) => Number(r.id) === Number(id));
|
|
372
|
+
return row ? { ok: true, data: { task: { ...row, dependencies: [], dependents: [] } } } : { ok: false };
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
return { api, calls, table };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
await ta('the unfiltered listing genuinely cannot see the task — the premise of the bug', async () => {
|
|
380
|
+
const { api } = fakeBoard();
|
|
381
|
+
const r = await api.tasks.getTasks({ query: { limit: 1000 } });
|
|
382
|
+
const ids = (r.data.tasks || []).map((x) => Number(x.id));
|
|
383
|
+
assert.equal(ids.length, UNFILTERED_CAP);
|
|
384
|
+
assert.ok(!ids.includes(777), 'task 777 must sit outside the unfiltered window, or this suite proves nothing');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
await ta('--goal finds a task that is NOT in the first page of the unfiltered listing', async () => {
|
|
388
|
+
const { api } = fakeBoard();
|
|
389
|
+
const byId = await fetchGoalTasks(api, 42, 50);
|
|
390
|
+
assert.ok(byId.has(777), 'the goal-scoped read must return the task past the 1000-row window');
|
|
391
|
+
assert.equal(byId.size, 3, 'and its two siblings, and nothing from another goal');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
await ta('a goal larger than one page is paged until exhausted, and offset=0 is never sent', async () => {
|
|
395
|
+
const { api, calls } = fakeBoard({ rowsInGoal: GOAL_PAGE + 7, targetIndex: 1100, total: 2000 });
|
|
396
|
+
const rows = await fetchGoalTaskRows(api, 42);
|
|
397
|
+
assert.equal(rows.length, GOAL_PAGE + 7, 'every row in the goal, across pages');
|
|
398
|
+
assert.ok(calls.length >= 2, 'more than one page was requested');
|
|
399
|
+
assert.equal(calls[0].offset, undefined, 'page one sends no offset (the route rejects offset=0)');
|
|
400
|
+
assert.equal(calls[1].offset, GOAL_PAGE, 'page two continues from the page size');
|
|
401
|
+
assert.ok(calls.every((c) => c.goal_id === 42), 'every page is scoped server-side');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
await ta('an unreadable FIRST page throws — it must never read as an empty chain', async () => {
|
|
405
|
+
const { api } = fakeBoard({ failPages: [0] });
|
|
406
|
+
await assert.rejects(() => fetchGoalTaskRows(api, 42), (e) => {
|
|
407
|
+
assert.match(e.message, /refusing to report a partial goal/);
|
|
408
|
+
assert.match(e.message, /goal 42 at offset 0/, 'the message names which page failed');
|
|
409
|
+
return true;
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
await ta('a LATER page failing also throws — a partial goal must not read as a whole one', async () => {
|
|
414
|
+
const { api } = fakeBoard({ rowsInGoal: GOAL_PAGE + 7, targetIndex: 1100, total: 2000, failPages: [1] });
|
|
415
|
+
await assert.rejects(() => fetchGoalTaskRows(api, 42), /refusing to report a partial goal/);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
await ta('the goal_id match is kept as a guard against a core that ignores the param', async () => {
|
|
419
|
+
const { api } = fakeBoard();
|
|
420
|
+
// Simulate a pinned older core: the goal_id filter is silently ignored.
|
|
421
|
+
const inner = api.tasks.getTasks;
|
|
422
|
+
api.tasks.getTasks = ({ query } = {}) => inner({ query: { ...query, goal_id: undefined } });
|
|
423
|
+
const byId = await fetchGoalTasks(api, 42, 50);
|
|
424
|
+
assert.equal(byId.size, 0, 'a whole-table answer must not become someone else\'s plan');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// -------------------------------------------- needs_migration (task 1003699)
|
|
428
|
+
|
|
429
|
+
await ta('needs_migration no longer halts on the word alone (task 1003697\'s shape)', async () => {
|
|
430
|
+
// The real 1003697: the flag set from prose, four touched files, none of them a
|
|
431
|
+
// migration — and a description that DOES contain a literal migrations/ path,
|
|
432
|
+
// which is why keying on the prose would not have fixed anything.
|
|
433
|
+
const t1003697 = task({
|
|
434
|
+
id: 1003697,
|
|
435
|
+
needs_migration: true,
|
|
436
|
+
touches: ['config/rename-history-baseline.json', 'scripts/gds/gds-literal-scan.js',
|
|
437
|
+
'scripts/gds/rename-history-check.js', 'tests/rename_history_restraint.mjs'],
|
|
438
|
+
description: `${SPEC} see migrations/163_goal_dep_trigger.sql — DO NOT change any migration`,
|
|
439
|
+
});
|
|
440
|
+
assert.ok(declaredSurfaceExcludesMigrations(t1003697));
|
|
441
|
+
const reasons = haltReasons(t1003697, { rank: 'metic', claimableIds: new Set([1003697]) });
|
|
442
|
+
assert.ok(!reasons.some((r) => r.code === 'needs_migration'), reasons.map((r) => r.code).join(','));
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
await ta('a task that genuinely touches migrations/ still halts', async () => {
|
|
446
|
+
const real = task({ id: 5, needs_migration: true, touches: ['migrations/214_add_column.sql', 'src/x.js'] });
|
|
447
|
+
assert.equal(declaredSurfaceExcludesMigrations(real), false);
|
|
448
|
+
const reasons = haltReasons(real, { rank: 'metic', claimableIds: new Set([5]) });
|
|
449
|
+
assert.ok(reasons.some((r) => r.code === 'needs_migration'));
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
await ta('a flagged task with NO declared surface keeps halting — silence is not evidence', async () => {
|
|
453
|
+
for (const touches of [[], undefined]) {
|
|
454
|
+
const unknown = task({ id: 6, needs_migration: true, touches });
|
|
455
|
+
assert.equal(declaredSurfaceExcludesMigrations(unknown), false);
|
|
456
|
+
const reasons = haltReasons(unknown, { rank: 'metic', claimableIds: new Set([6]) });
|
|
457
|
+
assert.ok(reasons.some((r) => r.code === 'needs_migration'), `touches=${JSON.stringify(touches)}`);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
await ta('a MODULE-OWNED migration counts — modules/<key>/migrations/ is real schema work here', async () => {
|
|
462
|
+
// ADR 0083 / docs/modules-contract.md: ten modules ship their own migrations.
|
|
463
|
+
// An anchored ^migrations/ test called these "not a migration" and suppressed
|
|
464
|
+
// the halt — and nothing else would have caught it, because the protected-path
|
|
465
|
+
// registry's own migrations/ glob is root-anchored too.
|
|
466
|
+
for (const p of [
|
|
467
|
+
'modules/economy/migrations/012_add_col.sql',
|
|
468
|
+
'modules/lifecycle/migrations/163_goal_dep_trigger.sql',
|
|
469
|
+
'src/db/migrations/x.sql',
|
|
470
|
+
]) {
|
|
471
|
+
const real = task({ id: 7, needs_migration: true, touches: [p, 'src/x.js'] });
|
|
472
|
+
assert.equal(declaredSurfaceExcludesMigrations(real), false, p);
|
|
473
|
+
const reasons = haltReasons(real, { rank: 'metic', claimableIds: new Set([7]) });
|
|
474
|
+
assert.ok(reasons.some((r) => r.code === 'needs_migration'), `${p} must still halt`);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
await ta('the migrations match reads a path SEGMENT, so a lookalike filename does not count', async () => {
|
|
479
|
+
assert.equal(declaredSurfaceExcludesMigrations(task({ needs_migration: true, touches: ['./migrations/9.sql'] })), false);
|
|
480
|
+
assert.equal(declaredSurfaceExcludesMigrations(task({ needs_migration: true, touches: ['Migrations/9.sql'] })), false);
|
|
481
|
+
assert.equal(declaredSurfaceExcludesMigrations(task({ needs_migration: true, touches: ['modules\\economy\\migrations\\012.sql'] })), false);
|
|
482
|
+
// The word in a FILE name, with no migrations directory, is not schema work.
|
|
483
|
+
assert.equal(declaredSurfaceExcludesMigrations(task({ needs_migration: true, touches: ['docs/migrations-guide.md'] })), true);
|
|
484
|
+
assert.equal(declaredSurfaceExcludesMigrations(task({ needs_migration: true, touches: ['docs/recipes/migrations.md'] })), true);
|
|
485
|
+
});
|
|
486
|
+
|
|
330
487
|
console.log(`\nsequence: ${passed} passed, ${failed} failed`);
|
|
331
488
|
process.exit(failed ? 1 : 0);
|