@bongos/core 1.19.1066 → 1.19.1067
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 +28 -23
- package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +2 -0
- package/docs/copy-inventory.md +24 -24
- package/docs/copy-registry.json +28 -28
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +12 -12
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/studio.js +12 -9
- package/modules/lifecycle/cascade.js +48 -96
- package/modules/lifecycle/routes/lifecycle.js +9 -9
- package/modules/lifecycle/tests/cascade.mjs +106 -160
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/grader_reader_lens.mjs +16 -24
- package/tests/removal_verdict.mjs +5 -2
- package/tests/tweak_replaces_artist_review.mjs +170 -0
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
// 3. it says where it came from
|
|
12
12
|
// 4. it files at backlog, never ready
|
|
13
13
|
//
|
|
14
|
-
// The
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
14
|
+
// The mechanism is proved by the test's OWN declarations, not a production
|
|
15
|
+
// rule — which is also the point: a mechanism proved against the contract
|
|
16
|
+
// rather than one example survives its rules coming and going (the artist
|
|
17
|
+
// review, R04's, was removed by TW14).
|
|
18
18
|
//
|
|
19
19
|
// DB-free: the runner takes its three database calls as injected functions.
|
|
20
20
|
|
|
@@ -185,7 +185,13 @@ await test('the production table is valid, and a duplicate key is refused', () =
|
|
|
185
185
|
assert.ok(shape.some((p) => /body/.test(p)));
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
-
// --- the
|
|
188
|
+
// --- the artist review is GONE: Tweak Mode replaced it (TW14, ADR 0341 D9) ----
|
|
189
|
+
//
|
|
190
|
+
// R04 (task 1003115) wrote `artist-review-on-ship`, which filed an artist review
|
|
191
|
+
// for every copy/visual ship. Task 1004325 removed it: a ship that changes a
|
|
192
|
+
// page's words writes NOTHING, and the page reads "changed since last tweak"
|
|
193
|
+
// because drift is derived (D8). tests/tweak_replaces_artist_review.mjs proves
|
|
194
|
+
// that second half on the whole path; these pin the first.
|
|
189
195
|
|
|
190
196
|
const shippedCopy = (over = {}) => ({
|
|
191
197
|
id: '7001', version_id: 'BONGOS-V1', goal_id: '1000074',
|
|
@@ -194,66 +200,59 @@ const shippedCopy = (over = {}) => ({
|
|
|
194
200
|
...over,
|
|
195
201
|
});
|
|
196
202
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
203
|
+
// A test rule that records the ctx the runner handed it, so the runner's
|
|
204
|
+
// resolution of policy and changed files is still pinned with no production
|
|
205
|
+
// rule reading either.
|
|
206
|
+
const ctxProbe = () => {
|
|
207
|
+
const seen = [];
|
|
208
|
+
return {
|
|
209
|
+
seen,
|
|
210
|
+
rule: {
|
|
211
|
+
key: 'test-ctx-probe',
|
|
212
|
+
discipline: 'artist',
|
|
213
|
+
when: (t, ctx) => { seen.push(ctx); return false; },
|
|
214
|
+
title: () => 'never',
|
|
215
|
+
body: () => 'never',
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
await test('the artist review rule is gone from CASCADES, and the table stays well-formed', () => {
|
|
221
|
+
assert.equal(cascade.CASCADES.find((x) => x.key === 'artist-review-on-ship'), undefined,
|
|
222
|
+
'Tweak Mode replaced the per-ship review (ADR 0341 D9)');
|
|
223
|
+
assert.equal(cascade.artistReviewBody, undefined, 'its body went with it');
|
|
224
|
+
assert.deepEqual(cascade.validateCascades(), [], 'the shipped table is well-formed');
|
|
211
225
|
});
|
|
212
226
|
|
|
213
227
|
await test('the production rule set is exactly what is declared', () => {
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
// and silently suppress one another, which validateCascades
|
|
217
|
-
assert.deepEqual(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
228
|
+
// Adding a rule is a deliberate act, so it should have to be recorded here,
|
|
229
|
+
// and a rule vanishing fails here too. Duplicate keys would collide on
|
|
230
|
+
// source_ref and silently suppress one another, which validateCascades refuses.
|
|
231
|
+
assert.deepEqual(cascade.CASCADES.map((r) => r.key).sort(), ['removal-proposed-on-ship']);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await test('a copy/visual ship files NO artist review, at every artist-gate level', () => {
|
|
235
|
+
for (const ctx of [undefined, {}, { artistGate: 'strict' }, { artistGate: 'advisory' }, { artistGate: 'off' }, { artistGate: undefined }]) {
|
|
236
|
+
const { matched, errors } = cascade.cascadesFor(shippedCopy(), cascade.CASCADES, ctx);
|
|
237
|
+
assert.deepEqual(errors, []);
|
|
238
|
+
assert.equal(matched.length, 0, `nothing filed with ctx ${JSON.stringify(ctx)}`);
|
|
239
|
+
}
|
|
240
|
+
// Every shape that USED to file one: a copy proposal landing, an artist's own
|
|
241
|
+
// work, an unlabelled ship that changed a page file, and a truncated file list.
|
|
242
|
+
const shapes = [
|
|
243
|
+
[shippedCopy({ module_key: null, source: 'copy-proposal' }), {}],
|
|
244
|
+
[shippedCopy({ module_key: null, discipline: 'artist' }), {}],
|
|
245
|
+
[shippedCopy({ module_key: null }), { changedFiles: ['modules/hall-ui/public/studio.html'] }],
|
|
246
|
+
[shippedCopy({ module_key: null }), { changedFiles: [], changedFilesTruncated: true }],
|
|
247
|
+
];
|
|
248
|
+
for (const [task, ctx] of shapes) {
|
|
249
|
+
assert.equal(cascade.cascadesFor(task, cascade.CASCADES, ctx).matched.length, 0, JSON.stringify(ctx));
|
|
250
|
+
}
|
|
221
251
|
});
|
|
222
252
|
|
|
223
|
-
await test('
|
|
224
|
-
|
|
225
|
-
assert.equal(
|
|
226
|
-
const spec = matched[0].spec;
|
|
227
|
-
assert.equal(spec.discipline, 'artist');
|
|
228
|
-
assert.equal(spec.kind, 'unclassified');
|
|
229
|
-
assert.equal(spec.status, 'backlog', 'a request that triage may dismiss — not claimable, not nagging');
|
|
230
|
-
assert.match(spec.title, /7001/);
|
|
231
|
-
// Both steps, and both routes of step 2, are in the body.
|
|
232
|
-
//
|
|
233
|
-
// STEP 1's label tracks ADR 0298 (task 1003937), and it has now turned twice.
|
|
234
|
-
// It first read "REVIEW THE GRADER … coach it", which asked for something no
|
|
235
|
-
// artist could do: the panel's four lenses read completion, code quality,
|
|
236
|
-
// security and runtime cost, so there was no copy verdict to coach. It was
|
|
237
|
-
// rewritten to ask for the RECORD instead, as an explicitly INTERIM wording,
|
|
238
|
-
// pinned here so a well-meaning copy-edit could not restore the unfollowable
|
|
239
|
-
// version without reading why.
|
|
240
|
-
//
|
|
241
|
-
// Task 1003977 shipped the advisory reader lens and the voice guide it waited
|
|
242
|
-
// on (task 1003976), so the step is coaching again — and this pin moves with
|
|
243
|
-
// it. The two must turn TOGETHER: the wording is the signal for which world
|
|
244
|
-
// we are in, so a body that still said "no lens reads a sentence" after the
|
|
245
|
-
// lens landed would send every artist looking for a counterpart that is
|
|
246
|
-
// sitting right there in the grade record.
|
|
247
|
-
assert.match(spec.description, /STEP 1 — COACH THE READER/);
|
|
248
|
-
assert.match(spec.description, /docs\/voice\.md/, 'and names the written standard to coach against');
|
|
249
|
-
assert.doesNotMatch(spec.description, /none of them reads a sentence/,
|
|
250
|
-
'the interim claim is gone — it stopped being true when the reader shipped');
|
|
251
|
-
assert.match(spec.description, /ADR 0298/, 'the step names the decision that shaped it');
|
|
252
|
-
assert.match(spec.description, /STEP 2 — REMAKE OR TWEAK/);
|
|
253
|
-
assert.match(spec.description, /OUTSIDE the AI chat/);
|
|
254
|
-
assert.match(spec.description, /WITH it/);
|
|
255
|
-
assert.match(spec.description, /DISMISS this in triage/);
|
|
256
|
-
assert.match(spec.description, /blocks nothing that shipped/, 'it says out loud that it is not a gate');
|
|
253
|
+
await test('hasCopyOrVisualWork STAYS, because the grader\'s reader lens is scoped by it', () => {
|
|
254
|
+
assert.equal(typeof cascade.hasCopyOrVisualWork, 'function');
|
|
255
|
+
assert.equal(cascade.hasCopyOrVisualWork(shippedCopy()), true, 'a hall-ui ship is still copy/visual work');
|
|
257
256
|
});
|
|
258
257
|
|
|
259
258
|
await test('"copy or visuals" is a NARROW predicate — three ways in, and a default of no', () => {
|
|
@@ -262,49 +261,33 @@ await test('"copy or visuals" is a NARROW predicate — three ways in, and a def
|
|
|
262
261
|
assert.equal(H(shippedCopy({ module_key: 'art-pipeline' })), true);
|
|
263
262
|
assert.equal(H(shippedCopy({ module_key: null, source: 'copy-proposal' })), true, 'a landed copy proposal');
|
|
264
263
|
assert.equal(H(shippedCopy({ module_key: null, source: 'manual', discipline: 'artist' })), true, 'work an artist did');
|
|
265
|
-
// The default is NO — a backend/infra ship is not
|
|
264
|
+
// The default is NO — a backend/infra ship is not copy/visual work.
|
|
266
265
|
assert.equal(H(shippedCopy({ module_key: 'lifecycle', discipline: 'engineer', source: 'manual' })), false);
|
|
267
266
|
assert.equal(H(shippedCopy({ module_key: null, discipline: 'engineer', source: 'manual' })), false);
|
|
268
267
|
assert.equal(H(null), false);
|
|
269
268
|
});
|
|
270
269
|
|
|
271
|
-
await test('THE ANTI-RECURSION GUARD:
|
|
272
|
-
// The
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
270
|
+
await test('THE ANTI-RECURSION GUARD: a cascade-generated task shipping matches no production rule', () => {
|
|
271
|
+
// The removal rule's guard: even a cascade task whose ship deleted earlier
|
|
272
|
+
// work files nothing, so generation stays a single hop.
|
|
273
|
+
const cascaded = shippedCopy({ discipline: 'artist', source: cascade.CASCADE_SOURCE, module_key: null, id: '7100' });
|
|
274
|
+
const { matched } = cascade.cascadesFor(cascaded, cascade.CASCADES, {
|
|
275
|
+
removals: [{ taskId: '42', paths: ['a.js'] }],
|
|
277
276
|
});
|
|
278
|
-
|
|
279
|
-
assert.equal(matched.length, 0, 'a cascade-generated task never triggers another cascade');
|
|
277
|
+
assert.equal(matched.length, 0);
|
|
280
278
|
});
|
|
281
279
|
|
|
282
|
-
|
|
283
|
-
// A copy ship in some other goal gets its review filed next to that work.
|
|
284
|
-
const { matched } = cascade.cascadesFor(shippedCopy({ goal_id: '1000046', version_id: 'BONGOS-V1' }));
|
|
285
|
-
assert.equal(matched[0].spec.goalId, 1000046);
|
|
286
|
-
assert.equal(matched[0].spec.versionId, 'BONGOS-V1');
|
|
287
|
-
});
|
|
280
|
+
// --- the runner still resolves its policy and the changed files --------------
|
|
288
281
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
await
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
assert.equal(
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
await test('an UNREAD policy files — silence is never read as an opt-out', () => {
|
|
300
|
-
// No context at all (an old caller), and an explicitly-undefined level (a reader
|
|
301
|
-
// that threw) must both behave like the gate's default, which is 'strict'.
|
|
302
|
-
assert.equal(cascade.cascadesFor(shippedCopy()).matched.length, 1);
|
|
303
|
-
assert.equal(cascade.cascadesFor(shippedCopy(), cascade.CASCADES, {}).matched.length, 1);
|
|
304
|
-
assert.equal(cascade.cascadesFor(shippedCopy(), cascade.CASCADES, { artistGate: undefined }).matched.length, 1);
|
|
305
|
-
});
|
|
282
|
+
await test('the runner still hands the artist gate to the rules, and a reader that THROWS is reported', async () => {
|
|
283
|
+
const ok = ctxProbe();
|
|
284
|
+
await cascade.runCascadesForShip({ id: '7001' }, {
|
|
285
|
+
rules: [ok.rule], getTask: async () => shippedCopy(),
|
|
286
|
+
findTaskBySourceRef: async () => null, createTask: async () => ({ id: '1' }),
|
|
287
|
+
artistGateMode: () => 'advisory',
|
|
288
|
+
});
|
|
289
|
+
assert.equal(ok.seen[0].artistGate, 'advisory');
|
|
306
290
|
|
|
307
|
-
await test('a gate reader that THROWS costs the run nothing — it is reported, and the review is still filed', async () => {
|
|
308
291
|
const created = [];
|
|
309
292
|
const report = await cascade.runCascadesForShip({ id: '7001' }, {
|
|
310
293
|
getTask: async () => shippedCopy(),
|
|
@@ -312,24 +295,11 @@ await test('a gate reader that THROWS costs the run nothing — it is reported,
|
|
|
312
295
|
createTask: async (spec) => { created.push(spec); return { id: '7002' }; },
|
|
313
296
|
artistGateMode: () => { throw new Error('pack unreadable'); },
|
|
314
297
|
});
|
|
315
|
-
assert.equal(created.length,
|
|
316
|
-
assert.ok(report.errors.some((e) => /artistGateMode\(\) threw/.test(e)), '
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
await test("runCascadesForShip honours 'off' end to end — nothing is created, nothing errors", async () => {
|
|
320
|
-
let creates = 0;
|
|
321
|
-
const report = await cascade.runCascadesForShip({ id: '7001' }, {
|
|
322
|
-
getTask: async () => shippedCopy(),
|
|
323
|
-
findTaskBySourceRef: async () => null,
|
|
324
|
-
createTask: async () => { creates++; return { id: '7002' }; },
|
|
325
|
-
artistGateMode: () => 'off',
|
|
326
|
-
});
|
|
327
|
-
assert.equal(creates, 0);
|
|
328
|
-
assert.deepEqual(report.created, []);
|
|
329
|
-
assert.deepEqual(report.errors, [], 'a project that switched the gate off has no problem to report');
|
|
298
|
+
assert.equal(created.length, 0, 'and the production table files nothing either way');
|
|
299
|
+
assert.ok(report.errors.some((e) => /artistGateMode\(\) threw/.test(e)), 'the failed read says so');
|
|
330
300
|
});
|
|
331
301
|
|
|
332
|
-
// --- the
|
|
302
|
+
// --- the changed files (task 1004201) ----------------------------------------
|
|
333
303
|
|
|
334
304
|
await test('a page file changed under modules/*/public/ is visual work whatever the label says', () => {
|
|
335
305
|
const H = cascade.hasCopyOrVisualWork;
|
|
@@ -358,67 +328,43 @@ await test('a backend file in a UI module is NOT visual work — page files only
|
|
|
358
328
|
assert.equal(H(unlabelled, null), false);
|
|
359
329
|
});
|
|
360
330
|
|
|
361
|
-
await test('the ship runner resolves the changed files ONCE and
|
|
362
|
-
const
|
|
331
|
+
await test('the ship runner resolves the changed files ONCE into ctx, and a page change files nothing', async () => {
|
|
332
|
+
const probe = ctxProbe();
|
|
363
333
|
let asked = 0;
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
};
|
|
370
|
-
const report = await cascade.runCascadesForShip({ id: '7001' }, deps);
|
|
334
|
+
const changedFiles = async () => { asked++; return { paths: ['modules/hall-ui/public/goal-forest-render.js'], truncated: true }; };
|
|
335
|
+
await cascade.runCascadesForShip({ id: '7001' }, {
|
|
336
|
+
rules: [probe.rule], getTask: async () => shippedCopy({ module_key: null }),
|
|
337
|
+
findTaskBySourceRef: async () => null, createTask: async () => ({ id: '1' }), changedFiles,
|
|
338
|
+
});
|
|
371
339
|
assert.equal(asked, 1);
|
|
372
|
-
assert.
|
|
373
|
-
assert.
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
await test('a changed-files list cut off at the read cap FILES the review — a padded PR cannot skip it', async () => {
|
|
377
|
-
const run = async (changed) => {
|
|
378
|
-
const created = [];
|
|
379
|
-
await cascade.runCascadesForShip({ id: '7001' }, {
|
|
380
|
-
getTask: async () => shippedCopy({ module_key: null, discipline: 'engineer' }),
|
|
381
|
-
findTaskBySourceRef: async () => null,
|
|
382
|
-
createTask: async (spec) => { created.push(spec); return { id: '9500' }; },
|
|
383
|
-
changedFiles: async () => changed,
|
|
384
|
-
});
|
|
385
|
-
return created.filter((s) => s.discipline === 'artist').length;
|
|
386
|
-
};
|
|
387
|
-
const padding = Array.from({ length: 300 }, (_, i) => `docs/pad-${i}.md`);
|
|
388
|
-
assert.equal(await run({ paths: padding, truncated: true }), 1);
|
|
389
|
-
assert.equal(await run({ paths: padding, truncated: false }), 0, 'the same list, complete, is not UI work');
|
|
390
|
-
});
|
|
340
|
+
assert.deepEqual(probe.seen[0].changedFiles, ['modules/hall-ui/public/goal-forest-render.js']);
|
|
341
|
+
assert.equal(probe.seen[0].changedFilesTruncated, true);
|
|
391
342
|
|
|
392
|
-
|
|
343
|
+
// The production table, on the same ship: no artist review, nothing the
|
|
344
|
+
// deploy gate would hold, no error.
|
|
393
345
|
const gate = require_(path.join(HERE, '..', '..', '..', 'src', 'bongos', 'artist-gate.js'));
|
|
394
346
|
const created = [];
|
|
395
|
-
await cascade.runCascadesForShip({ id: '7001' }, {
|
|
396
|
-
getTask: async () => shippedCopy({ module_key: null
|
|
347
|
+
const report = await cascade.runCascadesForShip({ id: '7001' }, {
|
|
348
|
+
getTask: async () => shippedCopy({ module_key: null }),
|
|
397
349
|
findTaskBySourceRef: async () => null,
|
|
398
350
|
createTask: async (spec) => { created.push(spec); return { id: '9600' }; },
|
|
399
|
-
changedFiles:
|
|
351
|
+
changedFiles, artistGateMode: () => 'strict',
|
|
400
352
|
});
|
|
401
|
-
|
|
402
|
-
assert.
|
|
403
|
-
assert.
|
|
353
|
+
assert.deepEqual(created, []);
|
|
354
|
+
assert.equal(created.some((s) => gate.isArtistReview({ source: s.source, source_ref: s.sourceRef })), false);
|
|
355
|
+
assert.deepEqual(report.errors, []);
|
|
404
356
|
});
|
|
405
357
|
|
|
406
|
-
await test('a changed-files read that fails is REPORTED, never thrown
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
};
|
|
417
|
-
const unlabelled = await run(shippedCopy({ module_key: null, discipline: 'engineer' }));
|
|
418
|
-
assert.equal(unlabelled.created.length, 0);
|
|
419
|
-
assert.match(unlabelled.report.errors.join('\n'), /changedFiles\(\) threw \(github down\)/);
|
|
420
|
-
const labelled = await run(shippedCopy({ module_key: 'hall-ui' }));
|
|
421
|
-
assert.equal(labelled.created.filter((s) => s.discipline === 'artist').length, 1, 'an outage never costs a labelled review');
|
|
358
|
+
await test('a changed-files read that fails is REPORTED, never thrown', async () => {
|
|
359
|
+
const created = [];
|
|
360
|
+
const report = await cascade.runCascadesForShip({ id: '7001' }, {
|
|
361
|
+
getTask: async () => shippedCopy(),
|
|
362
|
+
findTaskBySourceRef: async () => null,
|
|
363
|
+
createTask: async (spec) => { created.push(spec); return { id: '9400' }; },
|
|
364
|
+
changedFiles: async () => { throw new Error('github down'); },
|
|
365
|
+
});
|
|
366
|
+
assert.equal(created.length, 0);
|
|
367
|
+
assert.match(report.errors.join('\n'), /changedFiles\(\) threw \(github down\)/);
|
|
422
368
|
});
|
|
423
369
|
|
|
424
370
|
const shipFiles = require_(path.join(HERE, '..', 'ship-files.js'));
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.1067",
|
|
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.1067",
|
|
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.1067",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -7697,5 +7697,11 @@
|
|
|
7697
7697
|
"id": "1004324",
|
|
7698
7698
|
"text": "When an artist's page rewrite is approved and goes live, the artist is now paid 30 credits for it, the same value an artist review used to carry. The payment lands at the moment the page ships, never at approval, and a page th"
|
|
7699
7699
|
}
|
|
7700
|
+
],
|
|
7701
|
+
"1.19.1067": [
|
|
7702
|
+
{
|
|
7703
|
+
"id": "1004325",
|
|
7704
|
+
"text": "A ship that changes a page's words no longer files an \"Artist review\" task. Those reviews came one per ship, and most were thrown away unread. Instead, the page itself shows \"changed since last tweak\" in Tweak Mode, worked out"
|
|
7705
|
+
}
|
|
7700
7706
|
]
|
|
7701
7707
|
}
|
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.1067'; // 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');
|
|
@@ -216,36 +216,28 @@ test('docs/voice.md exists — the lens has a standard to judge against', () =>
|
|
|
216
216
|
}
|
|
217
217
|
});
|
|
218
218
|
|
|
219
|
-
// ---- 7. THE
|
|
219
|
+
// ---- 7. THE ARTIST REVIEW THIS LENS WAS PAIRED WITH IS GONE -----------------
|
|
220
|
+
//
|
|
221
|
+
// Task 1003977 turned STEP 1 of the per-ship artist review into "coach the
|
|
222
|
+
// reader", because this lens was listening. Tweak Mode removed that review
|
|
223
|
+
// (task 1004325 / BV2.TW14, ADR 0341 D9): a ship now files no artist review at
|
|
224
|
+
// all. The lens stays, scoped by the same predicate, which cascade.js still
|
|
225
|
+
// exports for exactly this consumer.
|
|
220
226
|
|
|
221
|
-
test('the artist review
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const { artistReviewBody } = require('../modules/lifecycle/cascade.js');
|
|
228
|
-
const body = artistReviewBody({ id: '1234' });
|
|
229
|
-
assert.match(body, /COACH THE READER/, 'STEP 1 asks for coaching again');
|
|
230
|
-
assert.match(body, /docs\/voice\.md/, 'and names the standard to coach against');
|
|
231
|
-
assert.ok(!/none of them reads a sentence/.test(body),
|
|
232
|
-
'the interim claim that no lens reads a sentence is gone — it is no longer true');
|
|
233
|
-
// Still a request, never a gate (ADR 0241) — unchanged by this task.
|
|
234
|
-
assert.match(body, /REQUEST, not a gate/);
|
|
227
|
+
test('the per-ship artist review is gone, and the lens keeps its scope predicate', () => {
|
|
228
|
+
const cascade = require('../modules/lifecycle/cascade.js');
|
|
229
|
+
assert.equal(cascade.artistReviewBody, undefined, 'no review body is left to coach from');
|
|
230
|
+
assert.equal(cascade.CASCADES.some((r) => r.key === 'artist-review-on-ship'), false);
|
|
231
|
+
assert.equal(cascade.hasCopyOrVisualWork, hasCopyOrVisualWork,
|
|
232
|
+
'the lens is scoped by cascade.js\'s own export, not a copy of it');
|
|
235
233
|
});
|
|
236
234
|
|
|
237
|
-
test('the
|
|
238
|
-
// Both key off hasCopyOrVisualWork, so an artist review exists for exactly
|
|
239
|
-
// the ships a reader ran on. If these ever diverge, an artist is asked to
|
|
240
|
-
// coach a lens that never looked.
|
|
241
|
-
const cascadeSrc = fs.readFileSync(path.join(ROOT, 'modules', 'lifecycle', 'cascade.js'), 'utf8');
|
|
235
|
+
test('the copy lens is scoped by the files the work changed', () => {
|
|
242
236
|
const shipSrc = fs.readFileSync(path.join(ROOT, 'scripts', 'gds', 'ship-grade.js'), 'utf8');
|
|
243
|
-
assert.match(cascadeSrc, /hasCopyOrVisualWork/);
|
|
244
237
|
assert.match(shipSrc, /hasCopyOrVisualWork/);
|
|
245
|
-
// Task 1004201:
|
|
246
|
-
//
|
|
238
|
+
// Task 1004201: the grader hands the predicate the files the work changed, so
|
|
239
|
+
// an unlabelled UI ship gets the lens at grade time.
|
|
247
240
|
assert.match(shipSrc, /hasCopyOrVisualWork\(task, changedFiles\)/, 'the grader passes its own diff');
|
|
248
|
-
assert.match(cascadeSrc, /hasCopyOrVisualWork\(t, \(ctx \|\| \{\}\)\.changedFiles\)/, 'the cascade passes the merged PR');
|
|
249
241
|
const unlabelled = { module_key: null, discipline: 'engineer' };
|
|
250
242
|
assert.equal(hasCopyOrVisualWork(unlabelled, ['modules/hall-ui/public/goal-forest-lib.js']), true);
|
|
251
243
|
assert.equal(hasCopyOrVisualWork(unlabelled, ['modules/hall-ui/module.json', 'scripts/gds/claim.js']), false);
|
|
@@ -62,8 +62,11 @@ await test('the prefix does NOT match the other cascade rule', () => {
|
|
|
62
62
|
// separating a removal question from an artist review. If it ever widened, a
|
|
63
63
|
// Confirm button would appear on a copy review and answering it would record a
|
|
64
64
|
// removal verdict about work nobody proposed removing.
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
//
|
|
66
|
+
// The artist rule itself was removed by Tweak Mode (TW14, ADR 0341 D9), but the
|
|
67
|
+
// reviews it filed are still rows on the board under its key, so the key is
|
|
68
|
+
// spelled here rather than read from CASCADES.
|
|
69
|
+
const artistRef = cascade.cascadeSourceRef('artist-review-on-ship', SHIP);
|
|
67
70
|
assert.ok(!artistRef.startsWith(proposals.PROPOSAL_SOURCE_REF_PREFIX));
|
|
68
71
|
assert.equal(proposals.shipTaskIdFromSourceRef(artistRef), null);
|
|
69
72
|
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// tests/tweak_replaces_artist_review.mjs — Tweak Mode replaces the per-ship
|
|
2
|
+
// artist review, the WHOLE path (task 1004325 / BV2.TW14, ADR 0341 D8 and D9).
|
|
3
|
+
//
|
|
4
|
+
// The done-when: "a ship changing a page's text files no artist review and the
|
|
5
|
+
// page reads changed-since". Two halves, and they meet at one ship:
|
|
6
|
+
//
|
|
7
|
+
// 1. THE SHIP FILES NOTHING. The real cascade runner, with the real
|
|
8
|
+
// production table, the ship's real changed files and the artist gate at
|
|
9
|
+
// 'strict' (the level that used to file AND hold), creates no task.
|
|
10
|
+
// 2. THE PAGE READS CHANGED-SINCE ANYWAY, with no write. The real copy-desk
|
|
11
|
+
// route over the real lifecycle port's SQL (a fake pool), reading the page
|
|
12
|
+
// artifacts from disk: before the ship the page matches its last round;
|
|
13
|
+
// after it, the page's reading has moved, drift names the changed line, and
|
|
14
|
+
// the ship itself is listed as what changed it, found by its touches.
|
|
15
|
+
//
|
|
16
|
+
// "No write" is checked twice: the cascade's createTask is never called, and
|
|
17
|
+
// the fake pool refuses any SQL that is not one of the two reads, so an INSERT
|
|
18
|
+
// or UPDATE anywhere on the read path reds here.
|
|
19
|
+
|
|
20
|
+
import { strict as assert } from 'node:assert';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import os from 'node:os';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { makeRunner } from './helpers.mjs';
|
|
26
|
+
|
|
27
|
+
process.env.NODE_ENV = 'test';
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
|
|
30
|
+
const F = require('../modules/copy-desk/tests/fixtures/page-tweak.cjs');
|
|
31
|
+
const cascade = require('../modules/lifecycle/cascade.js');
|
|
32
|
+
const gate = require('../src/bongos/artist-gate.js');
|
|
33
|
+
const api = require('../src/module-api.js');
|
|
34
|
+
|
|
35
|
+
// THE SHIP. An engineer rewords the studio greeting ("Welcome home" becomes
|
|
36
|
+
// "Welcome back") after the page's first round shipped. Its touches meet the
|
|
37
|
+
// studio page's files, and it is copy/visual work by every measure the old rule
|
|
38
|
+
// used: a hall-ui module ship that changed a page file.
|
|
39
|
+
const SHIP = {
|
|
40
|
+
id: '555', title: 'Reword the studio greeting', status: 'shipped',
|
|
41
|
+
discipline: 'engineer', kind: 'feature', source: 'manual', module_key: 'hall-ui',
|
|
42
|
+
version_id: 'BONGOS-V2', goal_id: '1000095',
|
|
43
|
+
touches: ['modules/hall-ui/public/studio.html'],
|
|
44
|
+
shipped_at: new Date('2026-09-22T09:00:00Z'),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Every SQL the page read sends, so the test can say none of it wrote.
|
|
48
|
+
const seen = [];
|
|
49
|
+
const flat = (sql) => String(sql).replace(/\s+/g, ' ').trim();
|
|
50
|
+
api.pool = {
|
|
51
|
+
async query(sql, params = []) {
|
|
52
|
+
const q = flat(sql);
|
|
53
|
+
seen.push({ q, params });
|
|
54
|
+
if (/FROM tasks t LEFT JOIN task_grades g/.test(q)) return { rows: F.TASKS };
|
|
55
|
+
if (/t\.touches && \$1::text\[\]/.test(q)) {
|
|
56
|
+
// The real query's predicate, applied to the one ship: shipped after the
|
|
57
|
+
// page's last round, touching one of the page's files.
|
|
58
|
+
const files = new Set(params[0]);
|
|
59
|
+
const hit = SHIP.shipped_at > new Date(params[1]) && SHIP.touches.some((t) => files.has(t));
|
|
60
|
+
return { rows: hit ? [{ id: SHIP.id, title: SHIP.title, shipped_at: SHIP.shipped_at }] : [] };
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`unexpected SQL on the page read: ${q.slice(0, 120)}`);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
api.requireBuilder = (req, res, next) => { req.builder = { id: F.ALICE.id, rank: 'thetes' }; next(); };
|
|
66
|
+
|
|
67
|
+
const seams = require('../src/module-seams.js');
|
|
68
|
+
if (!seams.hasProvider('lifecycle')) seams.registerProvider('lifecycle', require('../modules/lifecycle/lifecycle.js'));
|
|
69
|
+
if (!seams.hasProvider('reward')) seams.registerProvider('reward', require('../modules/economy/reward.js'));
|
|
70
|
+
|
|
71
|
+
const pageData = require('../modules/copy-desk/page-data.js');
|
|
72
|
+
const express = require('express');
|
|
73
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
74
|
+
const buildRouter = require('../modules/copy-desk/routes/copy-desk.js');
|
|
75
|
+
|
|
76
|
+
const app = express();
|
|
77
|
+
app.use(attachFail);
|
|
78
|
+
app.use(buildRouter());
|
|
79
|
+
const server = await new Promise((resolve) => { const s = app.listen(0, '127.0.0.1', () => resolve(s)); });
|
|
80
|
+
const base = `http://127.0.0.1:${server.address().port}`;
|
|
81
|
+
async function get(p) {
|
|
82
|
+
const r = await fetch(base + p);
|
|
83
|
+
return { status: r.status, body: await r.json() };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The studio's reading BEFORE the ship says exactly what its first round left
|
|
87
|
+
// ("Welcome home", "Resume"); AFTER it is the fixture's (the greeting moved).
|
|
88
|
+
// The shared shell line differs on both sides, and drift must ignore it.
|
|
89
|
+
const studioAfter = F.READINGS.pages.find((p) => p.id === 'builders:studio');
|
|
90
|
+
const studioBefore = {
|
|
91
|
+
...studioAfter,
|
|
92
|
+
reading_hash: 'studio-before',
|
|
93
|
+
lines: studioAfter.lines.map((l) => (l.key === 'L0002' ? { ...l, text: 'Welcome home' } : l)),
|
|
94
|
+
};
|
|
95
|
+
const readingsWith = (studio) => ({ ...F.READINGS, pages: F.READINGS.pages.map((p) => (p.id === studio.id ? studio : p)) });
|
|
96
|
+
|
|
97
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'tweak-replaces-review-'));
|
|
98
|
+
const root = (name, readings) => {
|
|
99
|
+
const dir = path.join(tmp, name);
|
|
100
|
+
fs.mkdirSync(path.join(dir, 'docs'), { recursive: true });
|
|
101
|
+
fs.writeFileSync(path.join(dir, 'docs', 'page-inventory.json'), JSON.stringify(F.INVENTORY));
|
|
102
|
+
fs.writeFileSync(path.join(dir, 'docs', 'page-readings.json'), JSON.stringify(readings));
|
|
103
|
+
return dir;
|
|
104
|
+
};
|
|
105
|
+
const BEFORE = root('before', readingsWith(studioBefore));
|
|
106
|
+
const AFTER = root('after', readingsWith(studioAfter));
|
|
107
|
+
|
|
108
|
+
const { test, summary } = makeRunner();
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await test('before the ship, the studio matches its last round: tweaked, no drift', async () => {
|
|
112
|
+
pageData._setRoot(BEFORE);
|
|
113
|
+
const { status, body } = await get('/copy-desk/pages/builders:studio');
|
|
114
|
+
assert.equal(status, 200);
|
|
115
|
+
assert.equal(body.status_label, 'Text tweaked 1x');
|
|
116
|
+
assert.equal(body.drift.changed, false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await test('the ship that rewords the page files NO artist review, at the strictest gate', async () => {
|
|
120
|
+
const created = [];
|
|
121
|
+
const report = await cascade.runCascadesForShip({ id: SHIP.id }, {
|
|
122
|
+
getTask: async () => SHIP,
|
|
123
|
+
findTaskBySourceRef: async () => null,
|
|
124
|
+
createTask: async (spec) => { created.push(spec); return { id: '9999' }; },
|
|
125
|
+
artistGateMode: () => 'strict',
|
|
126
|
+
changedFiles: async () => ({ paths: SHIP.touches, truncated: false }),
|
|
127
|
+
removalCandidates: async () => ({ removals: [] }),
|
|
128
|
+
});
|
|
129
|
+
assert.deepEqual(report.errors, []);
|
|
130
|
+
assert.deepEqual(created, [], 'nothing is filed: no review task, no marker, nothing');
|
|
131
|
+
assert.equal(created.some((s) => gate.isArtistReview({ source: s.source, source_ref: s.sourceRef })), false);
|
|
132
|
+
// It is still copy/visual work: the grader's reader lens still runs on it.
|
|
133
|
+
assert.equal(cascade.hasCopyOrVisualWork(SHIP, SHIP.touches), true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await test('after the ship, the page reads changed-since, and names the ship, with no write', async () => {
|
|
137
|
+
pageData._setRoot(AFTER);
|
|
138
|
+
seen.length = 0;
|
|
139
|
+
const { status, body } = await get('/copy-desk/pages/builders:studio');
|
|
140
|
+
assert.equal(status, 200);
|
|
141
|
+
assert.equal(body.drift.known, true);
|
|
142
|
+
assert.equal(body.drift.changed, true, 'the page reads "changed since last tweak"');
|
|
143
|
+
assert.deepEqual(body.drift.changed_lines, [{ key: 'L0002', section: 'greeting', text: 'Welcome back' }]);
|
|
144
|
+
assert.deepEqual(body.drift.removed_lines, ['Welcome home']);
|
|
145
|
+
assert.deepEqual(body.drift.ships.map((s) => s.task_id), [SHIP.id], 'the ship is what changed it');
|
|
146
|
+
assert.equal(body.status_label, 'Text tweaked 1x', 'drift never lowers the count');
|
|
147
|
+
// Derived, never stored: only the two reads ran.
|
|
148
|
+
assert.ok(seen.length > 0);
|
|
149
|
+
for (const s of seen) assert.match(s.q, /^(WITH .*)?SELECT /, `a read, never a write: ${s.q.slice(0, 80)}`);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await test('the page list carries the same drift, which is what the recommendation ranks on', async () => {
|
|
153
|
+
pageData._setRoot(AFTER);
|
|
154
|
+
const list = await get('/copy-desk/pages');
|
|
155
|
+
const studio = list.body.pages.find((p) => p.id === 'builders:studio');
|
|
156
|
+
assert.equal(studio.drift.changed, true);
|
|
157
|
+
assert.equal(studio.drift.changed_count, 1);
|
|
158
|
+
});
|
|
159
|
+
} finally {
|
|
160
|
+
pageData._setRoot(null);
|
|
161
|
+
server.closeAllConnections();
|
|
162
|
+
await new Promise((resolve) => server.close(resolve));
|
|
163
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Let the closed sockets finish closing before summary() calls process.exit: on
|
|
167
|
+
// Windows an exit while a handle is mid-close trips a libuv assertion (exit 127)
|
|
168
|
+
// after every case has passed.
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
170
|
+
summary();
|