@bongos/core 1.17.3 → 1.18.3

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 (163) hide show
  1. package/.bongos-core.json +212 -332
  2. package/.devcontainer/healthcheck.sh +0 -0
  3. package/.devcontainer/init-firewall.sh +0 -0
  4. package/.devcontainer/post-create.sh +0 -0
  5. package/GOVERNANCE.md +1 -1
  6. package/README.md +26 -22
  7. package/bin/bongos.js +0 -0
  8. package/config/branding.neutral.json +5 -1
  9. package/docker-entrypoint.sh +0 -0
  10. package/docs/adr/0142-project-context-is-prose-only.md +41 -0
  11. package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
  12. package/docs/adr/README.md +6 -0
  13. package/docs/api/openapi.json +1630 -632
  14. package/docs/api-reference.md +50 -15
  15. package/docs/file-map.md +0 -18
  16. package/docs/onboarding/diagrams/05-versioning.mmd +4 -6
  17. package/docs/onboarding/diagrams/assertions.json +5 -9
  18. package/docs/recipes/packaging-the-core.md +13 -0
  19. package/docs/recipes/upgrading-the-core.md +5 -2
  20. package/migrations/039_newcomer_friendly_column.sql +5 -0
  21. package/migrations/102_remove_box_first_provision_approval.sql +4 -0
  22. package/migrations/core_192_github_oauth_tokens.sql +33 -0
  23. package/modules/hall-ui/public/builders.js +37 -0
  24. package/modules/hall-ui/public/goals-page.js +98 -176
  25. package/modules/hall-ui/public/goals.css +183 -0
  26. package/modules/hall-ui/public/goals.html +35 -261
  27. package/modules/hall-ui/public/goals.js +7 -2
  28. package/modules/hall-ui/public/hall-widgets.js +12 -4
  29. package/modules/hall-ui/public/my-projects.html +78 -0
  30. package/modules/hall-ui/public/my-projects.js +142 -0
  31. package/modules/hall-ui/public/scouting.html +45 -0
  32. package/modules/hall-ui/public/scouting.js +73 -0
  33. package/modules/hall-ui/public/settings.css +355 -0
  34. package/modules/hall-ui/public/settings.html +86 -414
  35. package/modules/hall-ui/public/settings.js +96 -35
  36. package/modules/hall-ui/public/style.css +31 -0
  37. package/modules/hall-ui/public/watch.css +184 -0
  38. package/modules/hall-ui/public/watch.html +111 -285
  39. package/modules/hall-ui/public/watch.js +59 -21
  40. package/modules/status-ui/public/explore.html +54 -0
  41. package/modules/status-ui/public/explore.js +67 -0
  42. package/package-lock.json +2 -2
  43. package/package.json +1 -1
  44. package/public-landing/create.html +292 -19
  45. package/public-landing/index.html +11 -76
  46. package/scripts/cron/style_guide_audit.sh +0 -0
  47. package/scripts/gds/api.js +0 -0
  48. package/scripts/gds/architect-audit.js +0 -0
  49. package/scripts/gds/audit-authorship.sh +0 -0
  50. package/scripts/gds/audit-replay.sh +0 -0
  51. package/scripts/gds/box.js +0 -0
  52. package/scripts/gds/capture.js +0 -0
  53. package/scripts/gds/cli-lib.js +11 -5
  54. package/scripts/gds/conductor-dispatch.js +0 -0
  55. package/scripts/gds/coreB-carve-proof.sh +0 -0
  56. package/scripts/gds/db-backup-nightly.sh +0 -0
  57. package/scripts/gds/db-backup-offsite.sh +0 -0
  58. package/scripts/gds/doctor.js +0 -0
  59. package/scripts/gds/grade-smoke.js +0 -0
  60. package/scripts/gds/hooks/pre-push.js +0 -0
  61. package/scripts/gds/install-git-hooks.js +0 -0
  62. package/scripts/gds/lint-migrations.js +102 -0
  63. package/scripts/gds/login.js +42 -10
  64. package/scripts/gds/main-audit.js +0 -0
  65. package/scripts/gds/mirror-redact.js +5 -0
  66. package/scripts/gds/oauth-secret.js +0 -0
  67. package/scripts/gds/onboarding-analyze.js +0 -0
  68. package/scripts/gds/onboarding-ship.js +0 -0
  69. package/scripts/gds/package-core.js +263 -4
  70. package/scripts/gds/paste-token.js +0 -0
  71. package/scripts/gds/portability-smoke-art.sh +0 -0
  72. package/scripts/gds/portability-smoke-memory.sh +0 -0
  73. package/scripts/gds/provision.js +270 -11
  74. package/scripts/gds/publish-manifest.js +15 -0
  75. package/scripts/gds/push-main.js +0 -0
  76. package/scripts/gds/render-ideas.js +0 -0
  77. package/scripts/gds/run-unit-tests.js +38 -14
  78. package/scripts/gds/security-baseline-check.sh +0 -0
  79. package/scripts/gds/session.js +0 -0
  80. package/scripts/gds/setup.js +41 -0
  81. package/scripts/gds/ship.js +126 -32
  82. package/scripts/gds/smoke-batch-claim.sh +0 -0
  83. package/scripts/gds/smoke-batch-release.sh +0 -0
  84. package/scripts/gds/smoke-box.sh +0 -0
  85. package/scripts/gds/smoke-claim-consistency.sh +0 -0
  86. package/scripts/gds/smoke-dependencies.sh +0 -0
  87. package/scripts/gds/smoke-write.sh +0 -0
  88. package/scripts/gds/upgrade.js +147 -5
  89. package/scripts/hall-preview/server.js +18 -1
  90. package/scripts/migrate.sh +10 -0
  91. package/scripts/migrate_cost_ledger.sh +0 -0
  92. package/scripts/render-diagrams.sh +0 -0
  93. package/scripts/security/dep-audit.js +0 -0
  94. package/scripts/setup-builder.js +0 -0
  95. package/scripts/setup-builder.sh +0 -0
  96. package/scripts/snapshots/maybe-render-on-deploy.sh +0 -0
  97. package/src/bongos/auth.js +362 -5
  98. package/src/bongos/db.js +66 -0
  99. package/src/bongos/github-repos.js +93 -0
  100. package/src/bongos/routes/auth.js +93 -11
  101. package/src/bongos/routes/github.js +90 -0
  102. package/src/bongos/routes/instance.js +15 -3
  103. package/src/bongos/routes.js +2 -0
  104. package/src/branding.js +17 -2
  105. package/src/instance-config.js +16 -0
  106. package/src/module-api.js +48 -1
  107. package/tests/branding.mjs +29 -23
  108. package/tests/github_repos_helpers.mjs +91 -0
  109. package/tests/github_token_store.mjs +121 -0
  110. package/tests/idp_device_exchange.mjs +105 -0
  111. package/tests/idp_federation.mjs +100 -0
  112. package/tests/migrations_forward_safe.mjs +86 -0
  113. package/tests/module_api.mjs +1 -1
  114. package/tests/package_core.mjs +246 -1
  115. package/tests/provision.mjs +226 -0
  116. package/tests/pruned_lockfile_integrity.mjs +79 -0
  117. package/tests/ship_ci_deploy.mjs +60 -0
  118. package/tests/upgrade.mjs +254 -5
  119. package/.gitignore +0 -91
  120. package/scripts/hall-preview/fixtures/access-requests.json +0 -1
  121. package/scripts/hall-preview/fixtures/achievements.json +0 -76
  122. package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
  123. package/scripts/hall-preview/fixtures/audit-log.json +0 -1
  124. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -1
  125. package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
  126. package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
  127. package/scripts/hall-preview/fixtures/builders__roster.json +0 -1
  128. package/scripts/hall-preview/fixtures/goals.json +0 -502
  129. package/scripts/hall-preview/fixtures/goals__25.json +0 -96
  130. package/scripts/hall-preview/fixtures/goals__26.json +0 -67
  131. package/scripts/hall-preview/fixtures/goals__3.json +0 -103
  132. package/scripts/hall-preview/fixtures/goals__35.json +0 -49
  133. package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
  134. package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
  135. package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
  136. package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
  137. package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -1
  138. package/scripts/hall-preview/fixtures/inbox.json +0 -330
  139. package/scripts/hall-preview/fixtures/instance.json +0 -124
  140. package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
  141. package/scripts/hall-preview/fixtures/me.json +0 -476
  142. package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
  143. package/scripts/hall-preview/fixtures/override-requests.json +0 -1
  144. package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
  145. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
  146. package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
  147. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
  148. package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
  149. package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
  150. package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
  151. package/scripts/hall-preview/fixtures/public__grades.json +0 -50
  152. package/scripts/hall-preview/fixtures/public__progress.json +0 -124
  153. package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
  154. package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
  155. package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
  156. package/scripts/hall-preview/fixtures/security__docs.json +0 -1
  157. package/scripts/hall-preview/fixtures/security__reports.json +0 -1
  158. package/scripts/hall-preview/fixtures/sessions__search.json +0 -1
  159. package/scripts/hall-preview/fixtures/tasks.json +0 -2420
  160. package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
  161. package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
  162. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
  163. package/scripts/hall-preview/fixtures/versions.json +0 -89
@@ -71,6 +71,66 @@ test('parseDeployMode: SAFE DEFAULT laptop on no input at all', () => {
71
71
  assert.equal(parseDeployMode('', ''), 'laptop');
72
72
  });
73
73
 
74
+ // ---- 1b. resolveDeployTarget precedence + safe "no target" default (task 1002202) ----
75
+ // Every case passes env explicitly so the machine's own OTB_/GDS_ env can't leak in.
76
+
77
+ const { resolveDeployTarget } = ship;
78
+
79
+ test('resolveDeployTarget: SAFE DEFAULT — no env, no config, no branding → no target', () => {
80
+ const r = resolveDeployTarget({ env: {}, configRaw: null, branding: null });
81
+ assert.equal(r.sshTarget, null);
82
+ assert.equal(r.healthzUrl, null);
83
+ });
84
+
85
+ test('resolveDeployTarget: config sshTarget + healthzUrl are honored', () => {
86
+ const r = resolveDeployTarget({
87
+ env: {},
88
+ configRaw: '{"sshTarget":"deployer@host.example","healthzUrl":"https://inst.example/healthz"}',
89
+ branding: null,
90
+ });
91
+ assert.equal(r.sshTarget, 'deployer@host.example');
92
+ assert.equal(r.healthzUrl, 'https://inst.example/healthz');
93
+ });
94
+
95
+ test('resolveDeployTarget: env wins over config for the ssh target', () => {
96
+ const r = resolveDeployTarget({
97
+ env: { GDS_DEPLOY_SSH_TARGET: 'env@host.example' },
98
+ configRaw: '{"sshTarget":"config@host.example"}',
99
+ branding: null,
100
+ });
101
+ assert.equal(r.sshTarget, 'env@host.example');
102
+ });
103
+
104
+ test('resolveDeployTarget: healthzUrl falls back to branding publicOrigin + /healthz', () => {
105
+ const r = resolveDeployTarget({
106
+ env: {},
107
+ configRaw: '{"sshTarget":"deployer@host.example"}',
108
+ branding: { domains: { publicOrigin: 'https://cloudbongos.com' } },
109
+ });
110
+ assert.equal(r.healthzUrl, 'https://cloudbongos.com/healthz');
111
+ });
112
+
113
+ test('resolveDeployTarget: env healthz URL wins over the branding fallback', () => {
114
+ const r = resolveDeployTarget({
115
+ env: { GDS_DEPLOY_HEALTHZ_URL: 'https://explicit.example/healthz' },
116
+ configRaw: null,
117
+ branding: { domains: { publicOrigin: 'https://cloudbongos.com' } },
118
+ });
119
+ assert.equal(r.healthzUrl, 'https://explicit.example/healthz');
120
+ });
121
+
122
+ test('resolveDeployTarget: malformed config JSON → safe no-target (never throws)', () => {
123
+ const r = resolveDeployTarget({ env: {}, configRaw: '{not json', branding: null });
124
+ assert.equal(r.sshTarget, null);
125
+ assert.equal(r.healthzUrl, null);
126
+ });
127
+
128
+ test('resolveDeployTarget: blank/whitespace config values are treated as unset', () => {
129
+ const r = resolveDeployTarget({ env: {}, configRaw: '{"sshTarget":" ","healthzUrl":""}', branding: null });
130
+ assert.equal(r.sshTarget, null);
131
+ assert.equal(r.healthzUrl, null);
132
+ });
133
+
74
134
  test('parseDeployMode: SAFE DEFAULT laptop on malformed config JSON', () => {
75
135
  assert.equal(parseDeployMode(undefined, '{not json'), 'laptop');
76
136
  });
package/tests/upgrade.mjs CHANGED
@@ -16,10 +16,11 @@ import { strict as assert } from 'node:assert';
16
16
  import { createRequire } from 'node:module';
17
17
  import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
18
18
  import { tmpdir } from 'node:os';
19
- import { join } from 'node:path';
19
+ import { join, dirname } from 'node:path';
20
20
 
21
21
  const require = createRequire(import.meta.url);
22
22
  const u = require('../scripts/gds/upgrade.js');
23
+ const pc = require('../scripts/gds/package-core.js'); // the packager — reuse its EXACT hash fns to build reference pins
23
24
 
24
25
  let passed = 0, failed = 0;
25
26
  const pending = [];
@@ -159,7 +160,9 @@ t('full: pre-flight → pin → install → migrate → materialize → restart
159
160
  let materialized = null;
160
161
  let regenArgs = null;
161
162
  const res = await u.runUpgrade(
162
- { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'emersonian.service' },
163
+ // healthCheck:false — this test exercises the full pipeline, not the health poll (which would need
164
+ // a fetch mock); a managed-restart bump now requires a --health-url unless health is opted out (H9).
165
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'emersonian.service', healthCheck: false },
163
166
  {
164
167
  log: () => {}, err: () => {},
165
168
  run: fakeRun(calls, { dirty: '', installTo: '1.15.0', dir }),
@@ -398,7 +401,7 @@ t('runUpgrade --registry (full): pins the exact version + writes the env-fed .np
398
401
  const dir = scratchConsumer({ pinned: '1.16.0', installed: '1.16.0' });
399
402
  const calls = [];
400
403
  const res = await u.runUpgrade(
401
- { to: '1.17.0', registry: true, instance: dir, service: 'demo.service' },
404
+ { to: '1.17.0', registry: true, instance: dir, service: 'demo.service', healthCheck: false }, // H9: opt out (no health poll under test)
402
405
  {
403
406
  log: () => {}, err: () => {},
404
407
  run: fakeRun(calls, { dirty: '', installTo: '1.17.0', dir }),
@@ -479,7 +482,7 @@ t('rollback on migrate failure: reverts pin + reinstalls previous core + rolled_
479
482
  const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
480
483
  const calls = [], queries = [];
481
484
  const res = await u.runUpgrade(
482
- { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service' },
485
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service', healthCheck: false }, // H9: opt out — this test is about migrate-failure rollback, not health
483
486
  {
484
487
  log: () => {}, err: () => {},
485
488
  run: pinAwareRun(calls, { dir, failMigrate: true }),
@@ -502,7 +505,7 @@ t('rollback on npm-install failure: reverts pin + reinstalls previous core', asy
502
505
  const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
503
506
  const calls = [], queries = [];
504
507
  const res = await u.runUpgrade(
505
- { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service' },
508
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service', healthCheck: false }, // H9: opt out — this test is about install-failure rollback, not health
506
509
  {
507
510
  log: () => {}, err: () => {},
508
511
  run: pinAwareRun(calls, { dir, failInstallForVersion: '1.15.0' }),
@@ -547,6 +550,252 @@ t('restorePin: writes a previous dep string back verbatim (file: or registry)',
547
550
  assert.equal(JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).dependencies['@bongos/core'], '^1.16.0');
548
551
  });
549
552
 
553
+ // ---- 7. integrity-pin verification (task 1002216 / audit H8, finding F1) ----
554
+ // Build a reference pin exactly as the packager does — canonicalTreeHash over the pinned
555
+ // files' sha256, so a matching installed tree recomputes byte-identically.
556
+ function pinManifestFor(files, { tarballSha = null, tarballBytes = 0 } = {}) {
557
+ const meta = files
558
+ .map((f) => ({ path: f.path, mode: f.mode || '0000644', sha256: pc.sha256hex(Buffer.from(f.content)) }))
559
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
560
+ const m = { artifact: 'bongos-core', tree_sha256: pc.canonicalTreeHash(meta), files: meta };
561
+ if (tarballSha) m.tarball = { name: 'bongos-core.tgz', sha256: tarballSha, bytes: tarballBytes };
562
+ return m;
563
+ }
564
+ function writeCoreTree(coreDir, files) {
565
+ for (const f of files) {
566
+ const abs = join(coreDir, ...f.path.split('/'));
567
+ mkdirSync(dirname(abs), { recursive: true });
568
+ writeFileSync(abs, f.content);
569
+ }
570
+ }
571
+ const PIN_FILES = [{ path: 'a.js', content: 'console.log(1)\n' }, { path: 'sub/b.json', content: '{"x":1}\n' }];
572
+
573
+ t('verifyInstalledPin: matching installed tree + tarball bytes → ok (both checks)', () => {
574
+ const dir = mkdtempSync(join(tmpdir(), 'otb-pin-'));
575
+ const coreDir = join(dir, 'node_modules', '@bongos', 'core');
576
+ writeCoreTree(coreDir, PIN_FILES);
577
+ const tgzBytes = Buffer.from('deterministic-fake-tgz');
578
+ const tgz = join(dir, 'vendor', 'bongos-core-1.15.0.tgz');
579
+ mkdirSync(dirname(tgz), { recursive: true });
580
+ writeFileSync(tgz, tgzBytes);
581
+ const m = pinManifestFor(PIN_FILES, { tarballSha: pc.sha256hex(tgzBytes), tarballBytes: tgzBytes.length });
582
+ const res = u.verifyInstalledPin({ coreDir, referenceManifest: m, tgzPath: tgz });
583
+ assert.equal(res.ran, true);
584
+ assert.equal(res.ok, true, res.problems.join('; '));
585
+ assert.deepEqual(res.checks.map((c) => c.name).sort(), ['tarball.sha256', 'tree_sha256']);
586
+ });
587
+
588
+ t('verifyInstalledPin: a corrupted installed file → mismatch (ran, not ok)', () => {
589
+ const dir = mkdtempSync(join(tmpdir(), 'otb-pin-'));
590
+ const coreDir = join(dir, 'node_modules', '@bongos', 'core');
591
+ writeCoreTree(coreDir, PIN_FILES);
592
+ const m = pinManifestFor(PIN_FILES);
593
+ writeFileSync(join(coreDir, 'a.js'), 'console.log(999) // tampered\n'); // corrupt AFTER pinning
594
+ const res = u.verifyInstalledPin({ coreDir, referenceManifest: m, tgzPath: null });
595
+ assert.equal(res.ran, true);
596
+ assert.equal(res.ok, false);
597
+ assert.ok(res.problems.some((p) => /content mismatch: a\.js/.test(p)), res.problems.join('; '));
598
+ });
599
+
600
+ t('verifyInstalledPin: a missing installed file → mismatch', () => {
601
+ const dir = mkdtempSync(join(tmpdir(), 'otb-pin-'));
602
+ const coreDir = join(dir, 'node_modules', '@bongos', 'core');
603
+ writeCoreTree(coreDir, [PIN_FILES[0]]); // omit sub/b.json
604
+ const res = u.verifyInstalledPin({ coreDir, referenceManifest: pinManifestFor(PIN_FILES), tgzPath: null });
605
+ assert.equal(res.ok, false);
606
+ assert.ok(res.problems.some((p) => /missing installed file: sub\/b\.json/.test(p)), res.problems.join('; '));
607
+ });
608
+
609
+ t('verifyInstalledPin: tarball bytes differ from the pin → mismatch', () => {
610
+ const dir = mkdtempSync(join(tmpdir(), 'otb-pin-'));
611
+ const coreDir = join(dir, 'node_modules', '@bongos', 'core');
612
+ writeCoreTree(coreDir, PIN_FILES);
613
+ const tgz = join(dir, 'vendor', 'bongos-core-1.15.0.tgz');
614
+ mkdirSync(dirname(tgz), { recursive: true });
615
+ writeFileSync(tgz, Buffer.from('the-actual-bytes'));
616
+ const m = pinManifestFor(PIN_FILES, { tarballSha: pc.sha256hex(Buffer.from('DIFFERENT-bytes')), tarballBytes: 9 });
617
+ const res = u.verifyInstalledPin({ coreDir, referenceManifest: m, tgzPath: tgz });
618
+ assert.equal(res.ok, false);
619
+ assert.ok(res.problems.some((p) => /tarball bytes/.test(p)), res.problems.join('; '));
620
+ });
621
+
622
+ t('verifyInstalledPin: no pin in the reference manifest → ran:false (nothing to check)', () => {
623
+ const res = u.verifyInstalledPin({ coreDir: '/nonexistent', referenceManifest: {}, tgzPath: null });
624
+ assert.equal(res.ran, false);
625
+ assert.equal(res.ok, false);
626
+ });
627
+
628
+ t('resolveReferenceManifest: prefers the committed vendored side-car (strong)', () => {
629
+ const dir = mkdtempSync(join(tmpdir(), 'otb-pin-'));
630
+ const coreDir = join(dir, 'node_modules', '@bongos', 'core');
631
+ mkdirSync(join(dir, 'vendor'), { recursive: true });
632
+ writeFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.manifest.json'), JSON.stringify(pinManifestFor(PIN_FILES)));
633
+ writeFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.tgz'), 'x');
634
+ const ref = u.resolveReferenceManifest({ instanceDir: dir, coreDir, targetVersion: '1.15.0' });
635
+ assert.equal(ref.source, 'vendored side-car manifest');
636
+ assert.equal(ref.strong, true);
637
+ assert.ok(ref.tgzPath && existsSync(ref.tgzPath));
638
+ });
639
+
640
+ t('resolveReferenceManifest: falls back to the installed inner manifest (not strong)', () => {
641
+ const dir = mkdtempSync(join(tmpdir(), 'otb-pin-'));
642
+ const coreDir = join(dir, 'node_modules', '@bongos', 'core');
643
+ mkdirSync(coreDir, { recursive: true });
644
+ writeFileSync(join(coreDir, '.bongos-core.json'), JSON.stringify(pinManifestFor(PIN_FILES)));
645
+ const ref = u.resolveReferenceManifest({ instanceDir: dir, coreDir, targetVersion: '1.15.0' });
646
+ assert.equal(ref.source, 'installed inner manifest');
647
+ assert.equal(ref.strong, false);
648
+ assert.equal(ref.tgzPath, null);
649
+ });
650
+
651
+ // A spawn shim whose `npm install` writes an installed core tree (+ version) so the
652
+ // real pin-verify step can run against it. `files` is what actually lands on disk.
653
+ function installWritingTree(calls, { dir, version, files }) {
654
+ return (cmd, args) => {
655
+ calls.push(`${cmd} ${(args || []).join(' ')}`.trim());
656
+ if (cmd === 'git' && args[0] === 'status') return { status: 0, stdout: '' };
657
+ if (cmd === 'npm' && args[0] === 'install') {
658
+ const coreDir = join(dir, 'node_modules', '@bongos', 'core');
659
+ writeCoreTree(coreDir, files);
660
+ writeFileSync(join(coreDir, 'package.json'), JSON.stringify({ name: '@bongos/core', version }) + '\n');
661
+ }
662
+ return { status: 0 };
663
+ };
664
+ }
665
+
666
+ t('runUpgrade: a matching pin passes verification (upgrade proceeds)', async () => {
667
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
668
+ mkdirSync(join(dir, 'vendor'), { recursive: true });
669
+ writeFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.tgz'), 'fake-tgz');
670
+ writeFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.manifest.json'),
671
+ JSON.stringify(pinManifestFor(PIN_FILES, { tarballSha: pc.sha256hex(Buffer.from('fake-tgz')), tarballBytes: 8 })));
672
+ const logs = [];
673
+ const res = await u.runUpgrade(
674
+ { to: '1.15.0', instance: dir },
675
+ {
676
+ log: (m) => logs.push(m), err: () => {},
677
+ run: installWritingTree([], { dir, version: '1.15.0', files: PIN_FILES }),
678
+ materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
679
+ }
680
+ );
681
+ assert.equal(res.ok, true, res.error);
682
+ assert.ok(logs.some((m) => /integrity pin verified.*tarball\.sha256.*tree_sha256/.test(m)), logs.join('\n'));
683
+ });
684
+
685
+ t('runUpgrade: a pin MISMATCH after install fails and auto-rolls-back', async () => {
686
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
687
+ mkdirSync(join(dir, 'vendor'), { recursive: true });
688
+ writeFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.tgz'), 'fake-tgz');
689
+ // The reference pin is for PIN_FILES, but install writes DIFFERENT content → tree mismatch.
690
+ writeFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.manifest.json'), JSON.stringify(pinManifestFor(PIN_FILES)));
691
+ const corrupted = [{ path: 'a.js', content: 'console.log(666)\n' }, { path: 'sub/b.json', content: '{"x":1}\n' }];
692
+ const res = await u.runUpgrade(
693
+ { to: '1.15.0', instance: dir },
694
+ {
695
+ log: () => {}, err: () => {},
696
+ run: installWritingTree([], { dir, version: '1.15.0', files: corrupted }),
697
+ materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
698
+ }
699
+ );
700
+ assert.equal(res.ok, false);
701
+ assert.ok(/integrity pin MISMATCH/.test(res.error || ''), res.error);
702
+ assert.equal(res.rolledBack, true);
703
+ assert.equal(u.readPinnedCoreVersion(dir), '1.14.1', 'pin reverted to the previous version on rollback');
704
+ });
705
+
706
+ t('runUpgrade: --require-pin makes a MISSING reference manifest fatal', async () => {
707
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
708
+ mkdirSync(join(dir, 'vendor'), { recursive: true });
709
+ writeFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.tgz'), 'fake-tgz'); // tgz present, but NO manifest
710
+ const res = await u.runUpgrade(
711
+ { to: '1.15.0', instance: dir, requirePin: true },
712
+ {
713
+ log: () => {}, err: () => {},
714
+ run: installWritingTree([], { dir, version: '1.15.0', files: PIN_FILES }),
715
+ materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
716
+ }
717
+ );
718
+ assert.equal(res.ok, false);
719
+ assert.ok(/no reference manifest/.test(res.error || ''), res.error);
720
+ assert.equal(res.rolledBack, true, 'unverifiable pin rolls back under --require-pin');
721
+ });
722
+
723
+ t('runUpgrade: --skip-pin-verify bypasses the check (no manifest, still succeeds)', async () => {
724
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
725
+ mkdirSync(join(dir, 'vendor'), { recursive: true });
726
+ writeFileSync(join(dir, 'vendor', 'bongos-core-1.15.0.tgz'), 'fake-tgz');
727
+ const res = await u.runUpgrade(
728
+ { to: '1.15.0', instance: dir, skipPinVerify: true },
729
+ {
730
+ log: () => {}, err: () => {},
731
+ run: installWritingTree([], { dir, version: '1.15.0', files: PIN_FILES }),
732
+ materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
733
+ }
734
+ );
735
+ assert.equal(res.ok, true, res.error);
736
+ });
737
+
738
+ // ---- 8. mandatory post-restart health check (task 1002222 / audit H9, finding F5) ----
739
+ t('runUpgrade: a managed-restart bump WITHOUT --health-url is refused up front (H9 mandatory)', async () => {
740
+ const src = mkdtempSync(join(tmpdir(), 'src-'));
741
+ writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
742
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
743
+ const calls = [];
744
+ const res = await u.runUpgrade(
745
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service' }, // restarts, no --health-url, not opted out
746
+ { log: () => {}, err: () => {}, run: fakeRun(calls, { dirty: '', installTo: '1.15.0', dir }), materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }) }
747
+ );
748
+ assert.equal(res.ok, false);
749
+ assert.match(res.error, /health check is mandatory/);
750
+ assert.equal(u.readPinnedCoreVersion(dir), '1.14.1', 'refused in pre-flight → pin untouched');
751
+ assert.ok(!calls.some((c) => /npm install/.test(c)), 'refused before any install');
752
+ });
753
+
754
+ t('runUpgrade: --no-health-check lets a managed-restart bump proceed without a URL', async () => {
755
+ const src = mkdtempSync(join(tmpdir(), 'src-'));
756
+ writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
757
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
758
+ const logs = [];
759
+ const res = await u.runUpgrade(
760
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service', healthCheck: false },
761
+ { log: (m) => logs.push(m), err: () => {}, run: fakeRun([], { dirty: '', installTo: '1.15.0', dir }), materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }) }
762
+ );
763
+ assert.equal(res.ok, true, res.error);
764
+ assert.ok(logs.some((m) => /health check skipped \(--no-health-check\)/.test(m)), logs.join('\n'));
765
+ });
766
+
767
+ t('runUpgrade: a managed-restart bump WITH a healthy --health-url confirms + succeeds', async () => {
768
+ const src = mkdtempSync(join(tmpdir(), 'src-'));
769
+ writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
770
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
771
+ const logs = [];
772
+ const res = await u.runUpgrade(
773
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir, service: 'demo.service', healthUrl: 'http://x/healthz' },
774
+ {
775
+ log: (m) => logs.push(m), err: () => {}, run: fakeRun([], { dirty: '', installTo: '1.15.0', dir }),
776
+ materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }),
777
+ fetch: async () => ({ ok: true, status: 200 }), sleep: async () => {}, now: () => 0,
778
+ }
779
+ );
780
+ assert.equal(res.ok, true, res.error);
781
+ assert.equal(res.healthOk, true);
782
+ assert.ok(logs.some((m) => /health http:\/\/x\/healthz → 200/.test(m)), logs.join('\n'));
783
+ });
784
+
785
+ t('runUpgrade: no managed restart + no --health-url proceeds but flags the bump as unconfirmed', async () => {
786
+ const src = mkdtempSync(join(tmpdir(), 'src-'));
787
+ writeFileSync(join(src, 'bongos-core-1.15.0.tgz'), 'TGZ');
788
+ const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
789
+ const logs = [];
790
+ const res = await u.runUpgrade(
791
+ { to: '1.15.0', from: join(src, 'bongos-core-1.15.0.tgz'), instance: dir }, // no service/restart-cmd, no --health-url
792
+ { log: (m) => logs.push(m), err: () => {}, run: fakeRun([], { dirty: '', installTo: '1.15.0', dir }), materialize: () => ({ skills: 1 }), regenerateApiArtifacts: () => ({ ok: true, ran: [] }) }
793
+ );
794
+ assert.equal(res.ok, true, res.error);
795
+ assert.equal(res.healthOk, null);
796
+ assert.ok(logs.some((m) => /not confirmed serving/.test(m)), logs.join('\n'));
797
+ });
798
+
550
799
  await Promise.all(pending);
551
800
  console.log(`\nupgrade: ${passed} passed, ${failed} failed`);
552
801
  process.exit(failed ? 1 : 0);
package/.gitignore DELETED
@@ -1,91 +0,0 @@
1
- # Dependencies
2
- node_modules/
3
-
4
- # Per-session git worktrees live under here; each is itself a git checkout
5
- # and must NEVER be committed (would create embedded-repo references).
6
- .claude/worktrees/
7
-
8
- # Environment / secrets
9
- .env
10
- .env.local
11
- .env.*.local
12
-
13
- # Google Chat OAuth credentials — must NEVER be committed.
14
- # Live on the droplet at /etc/amazonprimea/ instead.
15
- google-chat-credentials.json
16
- google-chat-refresh-token.json
17
- *.credentials.json
18
-
19
- # Build output
20
- dist/
21
- build/
22
-
23
- # Logs
24
- *.log
25
- npm-debug.log*
26
-
27
- # OS files
28
- .DS_Store
29
-
30
- # Pixel art generation artifacts (iterations are huge; keep references + finals only)
31
- modules/art-pipeline/iterations/*
32
- # ...but keep the anti-grid FFT baselines tracked (idea 27): they're tiny + the smoke suite
33
- # (tests/anti_grid.mjs) compares against them. `modules/art-pipeline/iterations/*` (not the bare
34
- # dir) lets this negation re-include the file — git can't re-include under a fully-excluded dir.
35
- !modules/art-pipeline/iterations/anti_grid_baselines.json
36
- modules/art-pipeline/generated/raw/
37
- modules/art-pipeline/generated/*/raw/
38
- # Derived from references/ via slice_references.py — easy to regenerate, no need in git
39
- modules/art-pipeline/template/ref_tiles/
40
- modules/art-pipeline/template/ref_clusters/
41
- # Autotile cross-verify snapshot — regenerated on every smoke_test run.
42
- modules/art-pipeline/pipeline/_autotile_tables.json
43
- __pycache__/
44
- *.pyc
45
-
46
-
47
- # World snapshots — generated on the droplet by scripts/snapshots/render-world.mjs.
48
- # Untracked so git reset --hard on deploy doesn't wipe them.
49
- public-status/snapshots/*.png
50
- public-status/snapshots/manifest.json
51
-
52
- # Repo Atlas data — generated on the droplet by scripts/gds/gen-atlas.js on every
53
- # deploy (ADR 0094). Untracked so git reset --hard doesn't wipe it; the deploy
54
- # step rewrites it fresh each time, so a committed copy would only go stale.
55
- # (The hall surface carved to modules/hall-ui/public/ in BV1.R85.)
56
- modules/hall-ui/public/atlas.json
57
-
58
- # Whole-file generated navigation docs — untracked since ADR 0110 (task 1926), the
59
- # same reasoning as the atlas above. docs/repo-map.md (symbol skeleton, ADR 0063) and
60
- # docs/session-log-index.md (session index, ADR 0062 §8) are 100%-generated and were
61
- # regenerated + committed on EVERY ship, so two parallel builder branches collided on
62
- # them constantly. Untracked + regenerated at deploy (scripts/deploy/deploy-prod.sh) and
63
- # at box-source-fetch, they can never appear in a branch diff → the conflict class is
64
- # gone at the source. Their generators are dual-duty: the nested CLAUDE.md symbol blocks
65
- # and the CLAUDE.md §13 snippet stay TRACKED and CI-gated (gen-*.js --check-block).
66
- docs/repo-map.md
67
- docs/session-log-index.md
68
-
69
- # Onboarding diagram RENDERS — untracked since ADR 0110 pt.3 / ADR 0122 (task 2027),
70
- # the deferred diagram half of ADR 0110. The .svg/.png are regenerated + committed on
71
- # every render-capable ship, so parallel builder branches collided on them (same class
72
- # as the nav docs above). Untracked + re-rendered at deploy (scripts/deploy/deploy-prod.sh
73
- # runs scripts/render-diagrams.sh against the tracked .mmd, using the host's installed
74
- # Chrome — never puppeteer's own droplet-stalling download) and best-effort at
75
- # box-source-fetch. The .mmd SOURCES + assertions.json stay TRACKED (git reset --hard
76
- # restores them; the render regenerates the images from them), so the nightly
77
- # diagram-drift cron (reads assertions.json) is unaffected.
78
- docs/onboarding/diagrams/*.svg
79
- docs/onboarding/diagrams/*.png
80
-
81
- # Re-hosted #bugs screenshots (task #936). Downloaded from Discord and served
82
- # by routes/bug-attachments.js. Outside git so a deploy never wipes them.
83
- var/bug-attachments/
84
-
85
- # Custom merge-driver scratch files. The regen merge driver
86
- # (scripts/gds/git-merge-regen.js, installed by scripts/gds/install-git-hooks.js)
87
- # writes `.merge_file_*` temp files while resolving conflicts. They are pure
88
- # scratch — never source — but a `git add -A` once staged one onto main
89
- # (.merge_file_IKo9KW, merge 3e9f1f81). Ignore the pattern so it can't recur.
90
- .merge_file_*
91
- scratchpad/
@@ -1 +0,0 @@
1
- { "requests": [] }
@@ -1,76 +0,0 @@
1
- {
2
- "achievements": [
3
- {
4
- "id": "first-ship",
5
- "name": "First Ship",
6
- "description": "Ship your first task.",
7
- "icon": "📦",
8
- "credit_bonus": 1,
9
- "sort_order": 10
10
- },
11
- {
12
- "id": "first-claim",
13
- "name": "First Claim",
14
- "description": "Claim your first task.",
15
- "icon": "🪙",
16
- "credit_bonus": 1,
17
- "sort_order": 20
18
- },
19
- {
20
- "id": "first-parallel-claim",
21
- "name": "First Parallel Claim",
22
- "description": "Hold two simultaneous claims; ship both successfully.",
23
- "icon": "⚓",
24
- "credit_bonus": 2,
25
- "sort_order": 30
26
- },
27
- {
28
- "id": "streak-3",
29
- "name": "Streak: 3 Nights",
30
- "description": "Ship a task in 3 consecutive local-time nights (>=15min; cleanup/refactor now counts).",
31
- "icon": "🔥",
32
- "credit_bonus": 2,
33
- "sort_order": 40
34
- },
35
- {
36
- "id": "streak-7",
37
- "name": "Streak: 7 Nights",
38
- "description": "Ship a task in 7 consecutive local-time nights (>=15min; cleanup/refactor now counts).",
39
- "icon": "🌋",
40
- "credit_bonus": 5,
41
- "sort_order": 50
42
- },
43
- {
44
- "id": "art-pipeline-shipper",
45
- "name": "Art Pipeline Shipper",
46
- "description": "Ship a task whose touches[] include the art/ pipeline.",
47
- "icon": "🎨",
48
- "credit_bonus": 2,
49
- "sort_order": 60
50
- },
51
- {
52
- "id": "subtractor",
53
- "name": "Subtractor",
54
- "description": "Earn the net-negative ship bonus on 3 ships — across your work you have deleted more than you have added.",
55
- "icon": "🪓",
56
- "credit_bonus": 2,
57
- "sort_order": 65
58
- },
59
- {
60
- "id": "gds-shipper",
61
- "name": "GDS Shipper",
62
- "description": "Ship a task whose touches[] include src/gds/ or scripts/gds/.",
63
- "icon": "⚙️",
64
- "credit_bonus": 2,
65
- "sort_order": 70
66
- },
67
- {
68
- "id": "breaking-the-100c-bar",
69
- "name": "Breaking the 100c Bar",
70
- "description": "Cross 100 total credits.",
71
- "icon": "💯",
72
- "credit_bonus": 5,
73
- "sort_order": 80
74
- }
75
- ]
76
- }