@bongos/core 1.20.77 → 1.20.79

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 (56) hide show
  1. package/.bongos-core.json +83 -58
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +3 -0
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/adr/0136-update-channel-subscription-policy.md +1 -1
  8. package/docs/adr/0360-bongos-follows-every-candidate-every-other-project-follows-releases.md +66 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/api/openapi.json +70 -3
  11. package/docs/api-reference.md +3 -2
  12. package/docs/copy-inventory.md +22 -14
  13. package/docs/copy-registry.json +125 -41
  14. package/docs/module-api-changelog.md +4 -0
  15. package/docs/page-inventory.json +5 -1
  16. package/docs/page-readings.json +150 -137
  17. package/docs/recipes/upgrading-the-core.md +11 -2
  18. package/modules/copy-desk/artist-stats.js +15 -1
  19. package/modules/copy-desk/module.json +1 -1
  20. package/modules/copy-desk/page-status.js +41 -0
  21. package/modules/copy-desk/routes/copy-desk-artist.js +44 -0
  22. package/modules/copy-desk/tests/copy_no_cms.mjs +11 -2
  23. package/modules/hall-ui/public/deploy.states.json +1 -1
  24. package/modules/hall-ui/public/profile-crafts.js +231 -0
  25. package/modules/hall-ui/public/profile.css +60 -0
  26. package/modules/hall-ui/public/profile.html +31 -12
  27. package/modules/hall-ui/public/profile.js +180 -75
  28. package/modules/hall-ui/public/profile.states.json +34 -0
  29. package/modules/hall-ui/public/sky-draw.js +66 -39
  30. package/modules/hall-ui/public/sky-interaction.js +1 -1
  31. package/modules/hall-ui/public/sky-panel.js +1 -1
  32. package/modules/hall-ui/public/thinking.css +52 -24
  33. package/modules/hall-ui/records/profile-roles.md +18 -0
  34. package/modules/npm-release/public/work.js +72 -0
  35. package/modules/npm-release/work.js +67 -1
  36. package/modules/provisioning/render-standup.js +2 -1
  37. package/package-lock.json +2 -2
  38. package/package.json +1 -1
  39. package/release-notes.json +24 -0
  40. package/scripts/gds/init.js +16 -6
  41. package/scripts/gds/update-channel.js +68 -1
  42. package/scripts/gds/update-sweep.js +100 -4
  43. package/scripts/migrate.sh +6 -2
  44. package/src/module-api.js +1 -1
  45. package/tests/hall_sky.mjs +77 -0
  46. package/tests/helpers.mjs +32 -7
  47. package/tests/init.mjs +21 -5
  48. package/tests/migrate_applied_check.mjs +94 -0
  49. package/tests/npm_release_page.mjs +67 -0
  50. package/tests/npm_release_work.mjs +82 -0
  51. package/tests/profile_activity.mjs +23 -15
  52. package/tests/profile_role_headline.mjs +4 -4
  53. package/tests/profile_role_tabs.mjs +163 -0
  54. package/tests/update_channel.mjs +80 -1
  55. package/tests/update_subscription_engine.mjs +99 -0
  56. package/tests/upgrade_migrate.mjs +1 -1
@@ -766,3 +766,102 @@ test('the post-upgrade net re-probes, rather than reusing the set from before th
766
766
  assert.match(beforeSpawn, /const pre = pinState\(inst\.dir, spawnSync, generated\);/,
767
767
  'the heal runs before the bump, so it reads the core installed THEN');
768
768
  });
769
+
770
+ // ── the follow (task 1004297, ADR 0360) ─────────────────────────────────────────────
771
+ //
772
+ // followTarget is the sweep's whole decision about WHICH version an instance moves to. It
773
+ // runs the REAL update-channel.js math; only the registry read is stood in for, the way
774
+ // every other case here injects its externals.
775
+
776
+ const UC = require('../scripts/gds/update-channel.js');
777
+ const PUBLISHED = ['1.19.1039', '1.19.1040', '1.19.1041', '1.20.0'];
778
+ // The real module, with its dist-tags read answering from `tags` (or failing).
779
+ function channelWith(tags) {
780
+ return {
781
+ ...UC,
782
+ readReleaseTags: () => (tags ? UC.readReleaseTags({ run: () => ({ status: 0, stdout: JSON.stringify(tags) }) })
783
+ : UC.readReleaseTags({ run: () => ({ status: 1, stderr: 'npm ERR! network timeout' }) })),
784
+ };
785
+ }
786
+ const aim = (inst, channel) => S.followTarget({ inst: entry(inst), installed: '1.19.1039', channel, versions: PUBLISHED, env: {} });
787
+
788
+ test('follow candidates: the platform takes the newest version its channel allows, whatever is released', () => {
789
+ assert.deepEqual(aim({ follow: 'candidates' }, channelWith({ latest: '1.19.1039' })), { follow: 'candidates', target: '1.19.1041', held: null });
790
+ });
791
+
792
+ test('follow released: every other project stops at the released label', () => {
793
+ assert.deepEqual(aim({ follow: 'released' }, channelWith({ latest: '1.19.1040', next: '1.19.1041' })), { follow: 'released', target: '1.19.1040', held: null });
794
+ assert.deepEqual(aim({ follow: 'released' }, channelWith({ latest: '1.19.1039' })), { follow: 'released', target: null, held: null }, 'up to date on the release');
795
+ });
796
+
797
+ test('follow released with an unreadable label is SKIPPED, never upgraded on a guess', () => {
798
+ const r = aim({ follow: 'released' }, channelWith(null));
799
+ assert.equal(r.target, undefined);
800
+ assert.match(r.skip, /could not read which version is released \(npm ERR! network timeout\)/);
801
+ });
802
+
803
+ test('a core that predates the follow keeps the old reading: every published version', () => {
804
+ const { readReleaseTags, versionsForFollow, ...oldCore } = UC;
805
+ assert.equal(aim({}, oldCore).target, '1.19.1041', 'no follow on the entry — the roster came from an older core');
806
+ assert.equal(aim({ follow: 'released' }, oldCore).target, '1.19.1041', 'no dist-tags reader: before the split every version was a release');
807
+ });
808
+
809
+ test('the sweep asks followTarget, and says the follow on every line it prints', () => {
810
+ const src = readFileSync(path.join(ROOT, 'scripts', 'gds', 'update-sweep.js'), 'utf8');
811
+ const main = src.slice(src.indexOf('function main('));
812
+ assert.match(main, /followTarget\(\{ inst, installed, channel, versions: listed\.versions/);
813
+ assert.doesNotMatch(main, /resolveChannelTarget\(\{ installed, channel: inst\.channel, available: listed\.versions/, 'the unfiltered list never reaches the target');
814
+ assert.match(main, /follows: \$\{aim\.follow\}/);
815
+ });
816
+
817
+ // ── a version that failed here is held, not retried every sweep (task 1004297) ──────
818
+ //
819
+ // At a 15-minute cadence an unguarded lane would install a bad version, fail its health
820
+ // check, restart the service and roll back four times an hour until a fix was published.
821
+
822
+ const NOW = Date.parse('2026-09-26T18:00:00Z');
823
+ const hoursAgo = (h) => new Date(NOW - h * 3600000).toISOString();
824
+
825
+ test('a held version is skipped and named; a NEWER version is still taken at once', () => {
826
+ const held = S.heldVersions({ demo: [{ version: '1.19.1041', at: hoursAgo(1), reason: 'exit 1' }] }, 'demo', NOW);
827
+ const r = S.followTarget({ inst: entry({ follow: 'candidates' }), installed: '1.19.1039', channel: UC, versions: ['1.19.1040', '1.19.1041'], env: {}, held });
828
+ assert.equal(r.target, '1.19.1040', 'the held version is not retried');
829
+ assert.deepEqual(r.held, { version: '1.19.1041', at: hoursAgo(1), reason: 'exit 1' });
830
+
831
+ const fixed = S.followTarget({ inst: entry({ follow: 'candidates' }), installed: '1.19.1039', channel: UC, versions: ['1.19.1040', '1.19.1041', '1.19.1042'], env: {}, held });
832
+ assert.equal(fixed.target, '1.19.1042', 'the fix is taken the moment it is published');
833
+ assert.equal(fixed.held, null, 'and no hold is reported, because none changed the answer');
834
+ });
835
+
836
+ test('a hold is per instance and expires after a day', () => {
837
+ const refusals = { demo: [{ version: '1.19.1041', at: hoursAgo(25) }, { version: '1.19.1042', at: hoursAgo(2) }], other: [{ version: '1.19.1043', at: hoursAgo(1) }] };
838
+ assert.deepEqual([...S.heldVersions(refusals, 'demo', NOW).keys()], ['1.19.1042'], 'a day-old failure is retried');
839
+ assert.deepEqual([...S.heldVersions(refusals, 'nobody', NOW).keys()], []);
840
+ assert.equal(S.REFUSAL_HOLD_MS, 24 * 3600000);
841
+ for (const bad of [null, 'x', { demo: 'x' }, { demo: [null, { version: 7 }, { version: '1.0.0', at: 'not a date' }] }]) {
842
+ assert.deepEqual([...S.heldVersions(bad, 'demo', NOW).keys()], [], JSON.stringify(bad));
843
+ }
844
+ });
845
+
846
+ test('recordRefusal writes outside the instance, keeps only live holds, and never throws', () => {
847
+ const dir = mkdtempSync(path.join(tmpdir(), 'refusals-'));
848
+ const file = path.join(dir, 'nested', 'core-update-refused.json');
849
+ assert.equal(S.recordRefusal({ file, slug: 'demo', version: '1.19.1041', reason: 'exit 1', now: NOW - 30 * 3600000 }), true);
850
+ assert.equal(S.recordRefusal({ file, slug: 'demo', version: '1.19.1042', reason: 'exit 1', now: NOW }), true);
851
+ const all = JSON.parse(readFileSync(file, 'utf8'));
852
+ assert.deepEqual(all.demo.map((r) => r.version), ['1.19.1042'], 'the expired hold was dropped on write');
853
+ assert.equal(S.recordRefusal({ file, slug: 'demo', version: '1.19.1042', now: NOW + 1000 }), true);
854
+ assert.equal(JSON.parse(readFileSync(file, 'utf8')).demo.length, 1, 'the same version is one row, re-dated');
855
+ assert.equal(S.recordRefusal({ file: null, slug: 'demo', version: '1' }), false);
856
+ assert.equal(S.recordRefusal({ file, slug: 'demo', version: '1', writeFile: () => { throw new Error('EROFS'); } }), false, 'a failed write costs a retry, not the sweep');
857
+ assert.deepEqual(S.readRefusals(path.join(dir, 'missing.json')), {});
858
+ });
859
+
860
+ test('the sweep holds a version only when it did not go in', () => {
861
+ const src = readFileSync(path.join(ROOT, 'scripts', 'gds', 'update-sweep.js'), 'utf8');
862
+ const at = src.indexOf('if (res.status !== 0) {');
863
+ const failBranch = src.slice(at, src.indexOf('failed++; continue;', at));
864
+ assert.match(failBranch, /if \(readInstalledCoreVersion\(inst\.dir\) !== target\) \{\s*const held = recordRefusal\(/);
865
+ assert.match(src, /held: heldVersions\(refusals, label, Date\.now\(\)\)/, 'every target is chosen with the holds applied');
866
+ assert.match(src, /return path\.join\(require\('\.\.\/\.\.\/src\/instance-config\.js'\)\.configHome\(\), REFUSALS_FILE\)/, 'the record lives in the config home, never the instance tree');
867
+ });
@@ -47,7 +47,7 @@ function adoptedProject({ installed = '1.19.312' } = {}) {
47
47
  return dir;
48
48
  }
49
49
 
50
- function scaffoldedProject(scripts = { migrate: init.INSTANCE_MIGRATE_SCRIPT, start: init.INSTANCE_START_SCRIPT }) {
50
+ function scaffoldedProject(scripts = { migrate: init.INSTANCE_MIGRATE_SCRIPT, hall: init.INSTANCE_HALL_SCRIPT }) {
51
51
  const dir = fixture('upgrade-migrate-scaffold-');
52
52
  writeFileSync(path.join(dir, 'package.json'), `${JSON.stringify({ name: 'charter', private: true, scripts, dependencies: { '@bongos/core': '1.19.871' } }, null, 2)}\n`);
53
53
  return dir;