@bongos/core 1.19.932 → 1.19.934
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 +44 -34
- package/docs/copy-inventory.md +14 -14
- package/docs/copy-registry.json +14 -14
- package/docs/module-api-changelog.md +4 -0
- package/modules/government/board.js +32 -7
- package/modules/government/migrations/government_017_board_section_keys_v2.sql +59 -0
- package/modules/hall-ui/public/board-room.js +72 -15
- package/modules/ideas/migrations/ideas_008_template_default_v2.sql +37 -0
- package/modules/ideas/projection.js +16 -3
- package/modules/ideas/templates.js +80 -11
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/claude-materialize.js +48 -5
- package/scripts/gds/upgrade.js +9 -4
- package/src/module-api.js +1 -1
- package/tests/government_board_room_copy.mjs +12 -3
- package/tests/government_board_vote.mjs +53 -5
- package/tests/government_board_vote_ui.mjs +26 -6
- package/tests/government_seed.mjs +1 -0
- package/tests/helpers.mjs +23 -0
- package/tests/idea_completeness.mjs +30 -13
- package/tests/idea_grades.mjs +7 -2
- package/tests/idea_hall_composer.mjs +45 -5
- package/tests/idea_ideate_full.mjs +13 -4
- package/tests/idea_resubmit.mjs +49 -10
- package/tests/idea_spark_hall.mjs +3 -1
- package/tests/idea_templates.mjs +95 -16
- package/tests/update_subscription_engine.mjs +67 -5
- package/tests/upgrade.mjs +52 -0
- package/tests/upgrade_persist_pin.mjs +97 -3
|
@@ -356,12 +356,74 @@ test('every commitPin call site in the lane decides about push', () => {
|
|
|
356
356
|
}
|
|
357
357
|
});
|
|
358
358
|
|
|
359
|
-
test('the direct-upgrade fallback
|
|
359
|
+
test('the direct-upgrade fallback asks pinFlagsFor rather than deciding inline', () => {
|
|
360
360
|
const src = readFileSync(path.join(ROOT, '.claude', 'scheduled-tasks', 'core-update-subscription', 'subscribe.js'), 'utf8');
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
361
|
+
// The fourth version of this assertion. It used to pin the inline `if` verbatim, which froze
|
|
362
|
+
// the bug of task 1004122 in place: the line was correct about NOT pushing a co-tenant pin and
|
|
363
|
+
// wrong about how, and a test that quotes an expression can only ever confirm it is still there.
|
|
364
|
+
assert.match(src, /upArgs\.push\(\.\.\.pinFlagsFor\(\{ pinMode, canCommitPin, canPinNoPush \}\)\)/,
|
|
365
|
+
'the fallback decides its pin flags inline again, where no test can reach them');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
// 8b. pinFlagsFor — the fallback's pin policy, and the cross-file contract (task 1004122)
|
|
370
|
+
//
|
|
371
|
+
// go-live.js is the PREFERRED engine and the fallback must not apply a weaker policy just
|
|
372
|
+
// because an instance's core could not support it. It did: the fallback withheld --commit-pin
|
|
373
|
+
// on `local-commit`, so upgrade.js left the `.claude/` it had just rewritten uncommitted, the
|
|
374
|
+
// lane's own net can stage nothing but PIN_FILES, and the next sweep halted on those files
|
|
375
|
+
// forever. The instance took exactly one upgrade and then needed a human.
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
test('pinFlagsFor: local-commit commits the bump WITHOUT pushing — the fix for the one-upgrade wedge', () => {
|
|
379
|
+
assert.deepEqual(
|
|
380
|
+
S.pinFlagsFor({ pinMode: 'local-commit', canCommitPin: true, canPinNoPush: true }),
|
|
381
|
+
['--commit-pin', '--pin-no-push'],
|
|
382
|
+
'upgrade.js commits the pin AND the .claude/ it materialized, and pushes nothing');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('pinFlagsFor: push keeps --commit-pin alone, leave-dirty still commits nothing', () => {
|
|
386
|
+
assert.deepEqual(S.pinFlagsFor({ pinMode: 'push', canCommitPin: true, canPinNoPush: true }), ['--commit-pin']);
|
|
387
|
+
// --pin-no-push WITHOUT --commit-pin: nothing is committed, and upgrade.js's pull-deploy alarm
|
|
388
|
+
// drops to a note, because on this target no reset is coming to revert the bump.
|
|
389
|
+
assert.deepEqual(S.pinFlagsFor({ pinMode: 'leave-dirty', canCommitPin: true, canPinNoPush: true }), ['--pin-no-push']);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('pinFlagsFor: an old core that lacks --pin-no-push gets NOTHING, never the lone --commit-pin', () => {
|
|
393
|
+
// parseArgs silently ignores an unknown flag, so the pair would degrade to a bare --commit-pin
|
|
394
|
+
// and push the bump into the CUSTOMER's repo. Fail closed: keep today's behaviour instead.
|
|
395
|
+
assert.deepEqual(S.pinFlagsFor({ pinMode: 'local-commit', canCommitPin: true, canPinNoPush: false }), []);
|
|
396
|
+
assert.deepEqual(S.pinFlagsFor({ pinMode: 'leave-dirty', canCommitPin: true, canPinNoPush: false }), []);
|
|
397
|
+
// …and a core with no --commit-pin at all is never asked to commit.
|
|
398
|
+
for (const mode of ['push', 'local-commit', 'leave-dirty']) {
|
|
399
|
+
assert.ok(!S.pinFlagsFor({ pinMode: mode, canCommitPin: false, canPinNoPush: true }).includes('--commit-pin'), `mode ${mode}`);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('pinFlagsFor mirrors go-live.js upgradeCommand() mode for mode', () => {
|
|
404
|
+
// The cross-file contract, pinned at the source in the same way GO_LIVE_REQUIRED is: go-live
|
|
405
|
+
// builds a command STRING on a box, so the two can only be compared through its source.
|
|
406
|
+
const src = readFileSync(path.join(ROOT, 'scripts', 'gds', 'go-live.js'), 'utf8');
|
|
407
|
+
const body = src.slice(src.indexOf('function upgradeCommand('));
|
|
408
|
+
const commitPinModes = /\.\.\.\(mode === 'leave-dirty' \? \[\] : \['--commit-pin'\]\)/.test(body);
|
|
409
|
+
const noPushModes = /\.\.\.\(mode === 'push' \? \[\] : \['--pin-no-push'\]\)/.test(body);
|
|
410
|
+
assert.ok(commitPinModes && noPushModes, 'go-live.js upgradeCommand() moved — re-derive pinFlagsFor from it');
|
|
411
|
+
for (const mode of ['push', 'local-commit', 'leave-dirty']) {
|
|
412
|
+
const goLive = [
|
|
413
|
+
...(mode === 'leave-dirty' ? [] : ['--commit-pin']),
|
|
414
|
+
...(mode === 'push' ? [] : ['--pin-no-push']),
|
|
415
|
+
];
|
|
416
|
+
assert.deepEqual(
|
|
417
|
+
S.pinFlagsFor({ pinMode: mode, canCommitPin: true, canPinNoPush: true }), goLive,
|
|
418
|
+
`mode ${mode}: the fallback applies a different pin policy from the engine it stands in for`);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test('supportsPinNoPush probes the file, and is fail-closed on anything it cannot read', () => {
|
|
423
|
+
assert.equal(S.supportsPinNoPush('/x/upgrade.js', () => 'usage: --commit-pin --pin-no-push'), true);
|
|
424
|
+
assert.equal(S.supportsPinNoPush('/x/upgrade.js', () => 'usage: --commit-pin'), false, 'a core that predates the flag');
|
|
425
|
+
assert.equal(S.supportsPinNoPush(null, () => '--pin-no-push'), false);
|
|
426
|
+
assert.equal(S.supportsPinNoPush('/x/upgrade.js', () => { throw new Error('ENOENT'); }), false);
|
|
365
427
|
});
|
|
366
428
|
|
|
367
429
|
// ---------------------------------------------------------------------------
|
package/tests/upgrade.mjs
CHANGED
|
@@ -171,6 +171,58 @@ t('guard: dirty tree refuses (unless --force)', async () => {
|
|
|
171
171
|
assert.match(res.error, /not clean/);
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
+
// task 1004122 — the pre-flight must not refuse on what step 5 is about to overwrite anyway.
|
|
175
|
+
// An instance upgraded before task 1003957 carries ~40 modified `.claude/skills/*` nobody
|
|
176
|
+
// edited, and provision-core-upgrade.js (the Deploy page) passes --commit-pin but NOT --force,
|
|
177
|
+
// so the button stayed dead forever. The waiver is scoped to what a dry-run materialize on THIS
|
|
178
|
+
// instance says it rewrites; anything else still halts.
|
|
179
|
+
t('guard: a dirty tree that is ONLY this upgrade\'s own .claude/ materialization proceeds', async () => {
|
|
180
|
+
const src = mkdtempSync(join(tmpdir(), 'src-'));
|
|
181
|
+
writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
|
|
182
|
+
const dir = scratchConsumer();
|
|
183
|
+
const dryRuns = [];
|
|
184
|
+
const res = await u.runUpgrade(
|
|
185
|
+
{ to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, healthCheck: false, skipMigrate: true },
|
|
186
|
+
{
|
|
187
|
+
log: () => {}, err: () => {},
|
|
188
|
+
run: fakeRun([], { dirty: ' M .claude/skills/builder-end/SKILL.md\n M .claude/settings.json', installTo: '1.15.0', dir }),
|
|
189
|
+
materialize: (a) => { if (a.dryRun) dryRuns.push(a); return { skills: 2, writtenPaths: ['.claude/skills/builder-end/SKILL.md', '.claude/settings.json'] }; },
|
|
190
|
+
regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
|
|
191
|
+
databaseUrl: '', pgDatabase: '',
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
assert.equal(res.ok, true, 'the bump runs, and step 5 rewrites those same files');
|
|
195
|
+
assert.equal(dryRuns.length, 1, 'the pre-flight previewed the materialization exactly once');
|
|
196
|
+
assert.equal(dryRuns[0].instanceDir, dir);
|
|
197
|
+
assert.ok(dryRuns[0].coreRoot.endsWith(join('node_modules', '@bongos', 'core')), 'against the core the instance has NOW — the target is not installed yet');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
t('guard: a dirty tree with anything ELSE in it still refuses', async () => {
|
|
201
|
+
const dir = scratchConsumer();
|
|
202
|
+
const res = await u.runUpgrade({ to: '1.15.0', instance: dir }, {
|
|
203
|
+
log: () => {}, err: () => {},
|
|
204
|
+
run: fakeRun([], { dirty: ' M .claude/settings.json\n M migrations/core_253_wip.sql' }),
|
|
205
|
+
materialize: () => ({ skills: 1, writtenPaths: ['.claude/settings.json'] }),
|
|
206
|
+
});
|
|
207
|
+
assert.equal(res.ok, false);
|
|
208
|
+
assert.match(res.error, /not clean/);
|
|
209
|
+
assert.match(res.error, /migrations\/core_253_wip\.sql/, 'and it names the path that is genuinely a human\'s');
|
|
210
|
+
assert.ok(!res.error.includes('.claude/settings.json'), 'without re-listing the half it waived');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
t('guard: --skip-materialize waives nothing — step 5 rewrites nothing, so every dirty path is real', async () => {
|
|
214
|
+
const dir = scratchConsumer();
|
|
215
|
+
let asked = 0;
|
|
216
|
+
const res = await u.runUpgrade({ to: '1.15.0', instance: dir, skipMaterialize: true }, {
|
|
217
|
+
log: () => {}, err: () => {},
|
|
218
|
+
run: fakeRun([], { dirty: ' M .claude/settings.json' }),
|
|
219
|
+
materialize: () => { asked += 1; return { skills: 1, writtenPaths: ['.claude/settings.json'] }; },
|
|
220
|
+
});
|
|
221
|
+
assert.equal(res.ok, false);
|
|
222
|
+
assert.match(res.error, /not clean/);
|
|
223
|
+
assert.equal(asked, 0, 'and materialize was never even asked');
|
|
224
|
+
});
|
|
225
|
+
|
|
174
226
|
t('guard: already on the target refuses (unless --force)', async () => {
|
|
175
227
|
const dir = scratchConsumer({ installed: '1.15.0' });
|
|
176
228
|
const res = await u.runUpgrade({ to: '1.15.0', instance: dir }, { log: () => {}, err: () => {}, run: fakeRun([], { dirty: '' }) });
|
|
@@ -365,11 +365,10 @@ test('real git: an owner edit outside the generated files is never swept into th
|
|
|
365
365
|
// when anything but the pin is dirty, which is simultaneously the `failed=1` on a successful
|
|
366
366
|
// upgrade and the next sweep's permanent halt.
|
|
367
367
|
|
|
368
|
-
const { materializeClaude } = require('../scripts/gds/claude-materialize.js');
|
|
368
|
+
const { materializeClaude, materializeWillRewrite } = require('../scripts/gds/claude-materialize.js');
|
|
369
369
|
|
|
370
370
|
// A minimal core package: just the .claude/ tree materialize copies out of it.
|
|
371
|
-
function
|
|
372
|
-
const dir = mkdtempSync(join(tmpdir(), 'upgrade-core-'));
|
|
371
|
+
function coreAt(dir, version) {
|
|
373
372
|
put(dir, 'package.json', JSON.stringify({ name: '@bongos/core', version }));
|
|
374
373
|
put(dir, 'config/branding.json', JSON.stringify({ identity: {} }));
|
|
375
374
|
put(dir, '.claude/skills/builder-end/SKILL.md', `# builder-end (core ${version})\n`);
|
|
@@ -378,6 +377,13 @@ function scratchCore(version) {
|
|
|
378
377
|
put(dir, '.claude/hooks/stop.js', `// hook for ${version}\n`);
|
|
379
378
|
return dir;
|
|
380
379
|
}
|
|
380
|
+
function scratchCore(version) {
|
|
381
|
+
return coreAt(mkdtempSync(join(tmpdir(), 'upgrade-core-')), version);
|
|
382
|
+
}
|
|
383
|
+
// The same tree where an instance really carries it — what materializeWillRewrite probes.
|
|
384
|
+
function installCore(instanceDir, version) {
|
|
385
|
+
return coreAt(join(instanceDir, 'node_modules', '@bongos', 'core'), version);
|
|
386
|
+
}
|
|
381
387
|
|
|
382
388
|
// A scaffolded co-tenant that already TRACKS the .claude/ a previous materialize landed.
|
|
383
389
|
function scratchCoTenant(version = '1.19.737') {
|
|
@@ -390,6 +396,7 @@ function scratchCoTenant(version = '1.19.737') {
|
|
|
390
396
|
put(dir, 'package-lock.json', `lock ${version}\n`);
|
|
391
397
|
put(dir, 'config/branding.json', JSON.stringify({ identity: {} }));
|
|
392
398
|
put(dir, 'CLAUDE.md', 'the instance owns this\n');
|
|
399
|
+
put(dir, '.gitignore', 'node_modules/\n');
|
|
393
400
|
materializeClaude({ coreRoot: scratchCore(version), instanceDir: dir, log: quiet });
|
|
394
401
|
git(dir, 'add', '-A');
|
|
395
402
|
git(dir, 'commit', '-q', '-m', 'scaffold');
|
|
@@ -477,3 +484,90 @@ test('adopt mode records nothing, so `init --adopt` never commits over what it r
|
|
|
477
484
|
assert.ok(!sum.writtenPaths.includes('.claude/settings.json'));
|
|
478
485
|
assert.equal(readFileSync(join(dir, '.claude/skills/builder-end/SKILL.md'), 'utf8'), 'the adopting project already had this\n');
|
|
479
486
|
});
|
|
487
|
+
|
|
488
|
+
// ── the fleet that was ALREADY wedged (task 1004122) ────────────────────────────
|
|
489
|
+
//
|
|
490
|
+
// Task 1003957 above stops an upgrade CREATING the dirt. It does nothing for the instances
|
|
491
|
+
// that were upgraded before it shipped: every one of them still carries ~40 modified
|
|
492
|
+
// `.claude/skills/*` files nobody edited, and the clean-tree pre-flight refuses on them. So
|
|
493
|
+
// `test4` (instance 11) failed its Deploy-page upgrade on 2026-09-20 — provision-core-upgrade.js
|
|
494
|
+
// passes --commit-pin but NOT --force, so the button stays dead until someone commits by hand.
|
|
495
|
+
//
|
|
496
|
+
// The fix is that the pre-flight stops counting a path THIS upgrade is about to overwrite as
|
|
497
|
+
// evidence of anything. The set is not an allow-list: it is what a dry-run materialize against
|
|
498
|
+
// the core actually installed here says it would write.
|
|
499
|
+
|
|
500
|
+
test('gitTreeClean: an ignored path stops counting, and is reported rather than hidden', () => {
|
|
501
|
+
const dir = scratchCoTenant();
|
|
502
|
+
writeFileSync(join(dir, '.claude/skills/builder-end/SKILL.md'), 'rewritten by an earlier upgrade\n');
|
|
503
|
+
assert.equal(gitTreeClean(dir).ok, false, 'by default it is dirt like any other');
|
|
504
|
+
const t = gitTreeClean(dir, spawnSync, ['.claude/skills/builder-end/SKILL.md']);
|
|
505
|
+
assert.equal(t.ok, true);
|
|
506
|
+
assert.equal(t.ignored, 1, 'and the caller can say how many it waived');
|
|
507
|
+
assert.equal(t.dirty, '');
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
test('gitTreeClean: ignoring the materialized paths never waives anything else', () => {
|
|
511
|
+
const dir = scratchCoTenant();
|
|
512
|
+
writeFileSync(join(dir, '.claude/skills/builder-end/SKILL.md'), 'rewritten by an earlier upgrade\n');
|
|
513
|
+
put(dir, 'config/branding.json', 'the owner was here\n');
|
|
514
|
+
const t = gitTreeClean(dir, spawnSync, ['.claude/skills/builder-end/SKILL.md']);
|
|
515
|
+
assert.equal(t.ok, false, 'the owner edit still halts the bump');
|
|
516
|
+
assert.match(t.dirty, /config\/branding\.json/);
|
|
517
|
+
assert.ok(!t.dirty.includes('builder-end'), 'and only the materialized half was waived');
|
|
518
|
+
assert.equal(t.ignored, 1);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test('materializeWillRewrite: the set comes from the core the instance actually has', () => {
|
|
522
|
+
const dir = scratchCoTenant();
|
|
523
|
+
installCore(dir, '1.19.785');
|
|
524
|
+
const paths = materializeWillRewrite({ instanceDir: dir, materialize: materializeClaude }, quiet);
|
|
525
|
+
assert.ok(paths.includes('.claude/skills/builder-end/SKILL.md'));
|
|
526
|
+
assert.ok(paths.includes('.claude/settings.json'));
|
|
527
|
+
assert.ok(paths.includes('.claude/hooks/stop.js'));
|
|
528
|
+
assert.ok(paths.includes('.claude/skills/.instance-skills.json'), 'the provenance manifest is rewritten every run too');
|
|
529
|
+
assert.ok(!paths.includes('CLAUDE.md'), 'a file materialize writes only when ABSENT is not rewritten — the instance owns it');
|
|
530
|
+
assert.equal(git(dir, 'status', '--porcelain').stdout, '', 'and previewing it changed nothing on disk');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test('materializeWillRewrite: no core installed → no skill is waived, and a throw waives nothing at all', () => {
|
|
534
|
+
const dir = scratchCoTenant();
|
|
535
|
+
// Nothing to copy out of: materialize reports only the provenance record it rewrites anyway.
|
|
536
|
+
const paths = materializeWillRewrite({ instanceDir: dir, materialize: materializeClaude }, quiet);
|
|
537
|
+
assert.ok(!paths.some((p) => p.startsWith('.claude/skills/builder-')), 'no skill file is waived on the strength of a core that is not there');
|
|
538
|
+
|
|
539
|
+
// And an outright failure is fail-SOFT in the strict direction — an empty set plus a note.
|
|
540
|
+
const notes = [];
|
|
541
|
+
const boom = () => { throw new Error('unreadable core tree'); };
|
|
542
|
+
assert.deepEqual(materializeWillRewrite({ instanceDir: dir, materialize: boom }, (m) => notes.push(m)), []);
|
|
543
|
+
assert.equal(notes.length, 1, 'it says so rather than failing silently');
|
|
544
|
+
assert.match(notes[0], /unreadable core tree/);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test('real git: an instance wedged by a pre-1003957 upgrade unwedges itself on the next bump', () => {
|
|
548
|
+
const dir = scratchCoTenant();
|
|
549
|
+
sweep(dir, '1.19.737', '1.19.785', { artifactPaths: [] }); // the upgrade that left the mess
|
|
550
|
+
installCore(dir, '1.19.785'); // …and the core it installed
|
|
551
|
+
assert.equal(gitTreeClean(dir).ok, false, 'today: the next `bongos upgrade` refuses here');
|
|
552
|
+
|
|
553
|
+
// What runUpgrade's pre-flight now does.
|
|
554
|
+
const rewritable = materializeWillRewrite({ instanceDir: dir, materialize: materializeClaude }, quiet);
|
|
555
|
+
assert.equal(gitTreeClean(dir, spawnSync, rewritable).ok, true, 'the bump is allowed to proceed');
|
|
556
|
+
|
|
557
|
+
// …and proceeding is what cleans it up: step 5 rewrites those same paths and persistPin commits them.
|
|
558
|
+
const { pin } = sweep(dir, '1.19.785', '1.19.786');
|
|
559
|
+
assert.equal(pin.committed, true);
|
|
560
|
+
assert.equal(git(dir, 'status', '--porcelain').stdout, '', 'one bump, no human hands, tree clean');
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test('real git: a wedged instance with an owner edit beside the mess still refuses', () => {
|
|
564
|
+
const dir = scratchCoTenant();
|
|
565
|
+
sweep(dir, '1.19.737', '1.19.785', { artifactPaths: [] });
|
|
566
|
+
installCore(dir, '1.19.785');
|
|
567
|
+
put(dir, 'core_999_half_written.sql', '-- not finished\n');
|
|
568
|
+
const rewritable = materializeWillRewrite({ instanceDir: dir, materialize: materializeClaude }, quiet);
|
|
569
|
+
const t = gitTreeClean(dir, spawnSync, rewritable);
|
|
570
|
+
assert.equal(t.ok, false, 'a human really does need to look');
|
|
571
|
+
assert.match(t.dirty, /core_999_half_written\.sql/);
|
|
572
|
+
assert.ok(!t.dirty.includes('.claude/skills/builder-end'), 'the materialization half is still waived');
|
|
573
|
+
});
|