@bongos/core 1.21.176 → 1.21.178

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 (214) hide show
  1. package/.bongos-core.json +186 -496
  2. package/.claude/skills/planning-session/SKILL.md +2 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +0 -6
  5. package/clients/bongos-client/index.cjs +0 -6
  6. package/clients/bongos-client/index.d.ts +0 -6
  7. package/clients/bongos-client/index.mjs +0 -6
  8. package/config/scheduled-routines.json +3 -8
  9. package/docs/adr/0042-builder-self-deploy-ci-auto-merge.md +3 -2
  10. package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +1 -1
  11. package/docs/adr/0093-tranche-2-core-carve-sequence.md +1 -1
  12. package/docs/api/openapi.json +1 -87
  13. package/docs/api-reference.md +1 -8
  14. package/docs/canonical-permissions.md +4 -4
  15. package/docs/copy-inventory.md +25 -39
  16. package/docs/copy-registry.json +73 -200
  17. package/docs/file-map.md +6 -11
  18. package/docs/handoff-template.md +1 -1
  19. package/docs/module-api-changelog.md +4 -0
  20. package/docs/modules-contract.md +2 -2
  21. package/docs/onboarding/diagrams/README.md +3 -7
  22. package/docs/onboarding/diagrams/assertions.json +3 -3
  23. package/docs/page-inventory.json +4 -7
  24. package/docs/page-readings.json +1520 -1569
  25. package/docs/recipes/ops-gotchas.md +3 -1
  26. package/modules/autonomy/db.js +9 -0
  27. package/modules/autonomy/module.json +2 -2
  28. package/modules/autonomy/pollers/daily-checks.js +262 -0
  29. package/modules/autonomy/pollers/routine-timers.js +7 -5
  30. package/modules/autonomy/rank-roles.js +64 -0
  31. package/modules/autonomy/server-schedule.js +37 -0
  32. package/modules/government/catalog.js +10 -7
  33. package/modules/government/decision-board.js +0 -1
  34. package/modules/government/decision-rules.js +0 -1
  35. package/modules/government/docket.js +0 -1
  36. package/modules/government/protected-surfaces.js +1 -1
  37. package/modules/government/protected-surfaces.json +2 -3
  38. package/modules/hall-ui/public/atlas.html +1 -1
  39. package/modules/hall-ui/public/blockers.html +1 -1
  40. package/modules/hall-ui/public/board-room.html +1 -1
  41. package/modules/hall-ui/public/city.html +1 -1
  42. package/modules/hall-ui/public/collab.html +1 -1
  43. package/modules/hall-ui/public/commands.html +1 -1
  44. package/modules/hall-ui/public/copy-desk.html +1 -1
  45. package/modules/hall-ui/public/deploy.html +1 -1
  46. package/modules/hall-ui/public/diagrams.html +1 -1
  47. package/modules/hall-ui/public/drachmae.html +1 -1
  48. package/modules/hall-ui/public/fleet.html +1 -1
  49. package/modules/hall-ui/public/gate.css +4 -26
  50. package/modules/hall-ui/public/gate.html +12 -27
  51. package/modules/hall-ui/public/gate.js +26 -176
  52. package/modules/hall-ui/public/gate.states.json +4 -22
  53. package/modules/hall-ui/public/goal-map.html +1 -1
  54. package/modules/hall-ui/public/goals.html +1 -1
  55. package/modules/hall-ui/public/government.html +1 -1
  56. package/modules/hall-ui/public/idea.html +1 -1
  57. package/modules/hall-ui/public/ideas.html +1 -1
  58. package/modules/hall-ui/public/index.html +1 -1
  59. package/modules/hall-ui/public/modules.html +1 -1
  60. package/modules/hall-ui/public/my-agents.html +1 -1
  61. package/modules/hall-ui/public/primer.html +1 -1
  62. package/modules/hall-ui/public/profile.html +1 -1
  63. package/modules/hall-ui/public/project-settings.html +1 -1
  64. package/modules/hall-ui/public/ranks.html +1 -1
  65. package/modules/hall-ui/public/roadmap.html +1 -1
  66. package/modules/hall-ui/public/roster.html +1 -1
  67. package/modules/hall-ui/public/sessions.html +1 -1
  68. package/modules/hall-ui/public/settings.html +1 -1
  69. package/modules/hall-ui/public/shell.js +1 -1
  70. package/modules/hall-ui/public/street-card.js +5 -9
  71. package/modules/hall-ui/public/street-desk.js +2 -2
  72. package/modules/hall-ui/public/street-inside.js +1 -1
  73. package/modules/hall-ui/public/street.html +1 -1
  74. package/modules/hall-ui/public/studio.html +1 -1
  75. package/modules/hall-ui/public/task.html +1 -1
  76. package/modules/hall-ui/public/thinking.html +1 -1
  77. package/modules/hall-ui/public/tweak-editor.html +1 -1
  78. package/modules/hall-ui/public/watch.html +1 -1
  79. package/modules/hall-ui/public/work.html +1 -1
  80. package/modules/lifecycle/matter-acts.js +0 -6
  81. package/modules/lifecycle/module.json +0 -1
  82. package/package-lock.json +2 -2
  83. package/package.json +1 -1
  84. package/release-notes.json +36 -0
  85. package/scripts/bongos/adopt-preflight.js +3 -3
  86. package/scripts/bongos/api-alias-caller-scan.js +2 -2
  87. package/scripts/bongos/audit-authorship.sh +1 -8
  88. package/scripts/bongos/audit-rank-roles.js +15 -56
  89. package/scripts/bongos/bump-version.js +46 -9
  90. package/scripts/bongos/codemod-rename-scripts-gds.js +0 -1
  91. package/scripts/bongos/diagram-facts.js +1 -1
  92. package/scripts/bongos/do-api.js +1 -1
  93. package/scripts/bongos/doc-cli-guard.js +2 -0
  94. package/scripts/bongos/doc-control-claims.js +2 -2
  95. package/scripts/bongos/fitness-checks-identity.js +4 -15
  96. package/scripts/bongos/gen-diagrams.js +3 -3
  97. package/scripts/bongos/government-walk.js +3 -9
  98. package/scripts/bongos/hard-floor.js +52 -0
  99. package/scripts/bongos/publish-manifest.js +3 -3
  100. package/scripts/bongos/pulse.js +5 -3
  101. package/scripts/bongos/routine-schedule.js +38 -3
  102. package/scripts/bongos/run-unit-tests.js +17 -5
  103. package/scripts/bongos/ship-regen.js +2 -3
  104. package/scripts/gds/{goal-names.js → hard-floor.js} +3 -4
  105. package/scripts/security/dep-audit.js +50 -124
  106. package/src/bongos/module-scope-map.js +2 -2
  107. package/src/bongos/permission-path-check.js +1 -1
  108. package/src/bongos/route-rank-check.js +1 -8
  109. package/src/bongos/routes.js +1 -2
  110. package/src/module-api.js +1 -1
  111. package/src/modules.js +1 -1
  112. package/tests/acclaim_feed_evidence_db.mjs +42 -23
  113. package/tests/adr_0089_scope.mjs +7 -5
  114. package/tests/autonomy_daily_checks.mjs +201 -0
  115. package/tests/bump_version.mjs +52 -18
  116. package/tests/claude_materialize.mjs +12 -12
  117. package/tests/copy_desk_flags_db.mjs +22 -14
  118. package/tests/effective_visibility_predicate.mjs +0 -18
  119. package/tests/gating_review_retired.mjs +1 -1
  120. package/tests/government_act_alone_buildings.mjs +1 -3
  121. package/tests/government_board_membership_db.mjs +35 -22
  122. package/tests/government_protected_surfaces.mjs +29 -13
  123. package/tests/governor_docket.mjs +0 -2
  124. package/tests/governor_matter_acts.mjs +6 -8
  125. package/tests/hall_city.mjs +1 -1
  126. package/tests/hall_matter_card.mjs +2 -2
  127. package/tests/hall_oversight_world.mjs +16 -12
  128. package/tests/hall_street_desk.mjs +4 -3
  129. package/tests/hall_street_inside.mjs +1 -1
  130. package/tests/helpers.mjs +34 -0
  131. package/tests/howto_state.mjs +33 -56
  132. package/tests/howto_state_db.mjs +68 -0
  133. package/tests/idea_credit_streams_db.mjs +11 -3
  134. package/tests/integration_parked_ratchet.mjs +7 -1
  135. package/tests/nav_permission_atoms.mjs +1 -1
  136. package/tests/page_notes_db.mjs +12 -8
  137. package/tests/page_tweak_no_rearm.mjs +50 -0
  138. package/tests/publish_manifest.mjs +64 -0
  139. package/tests/routine_capture_kind.mjs +18 -7
  140. package/tests/routine_schedule.mjs +42 -0
  141. package/tests/search_isolation.mjs +5 -103
  142. package/tests/search_isolation_db.mjs +140 -0
  143. package/tests/session_earnings_mirror_db.mjs +2 -0
  144. package/tests/workflow_exec_paths.mjs +9 -11
  145. package/modules/lifecycle/routes/gate-approvals.js +0 -299
  146. package/scripts/bongos/artifact-staleness.js +0 -267
  147. package/scripts/bongos/ci-grade.js +0 -82
  148. package/scripts/bongos/classify-discipline.js +0 -153
  149. package/scripts/bongos/codemod-rename-src-gds.js +0 -145
  150. package/scripts/bongos/coreB-carve-proof.sh +0 -725
  151. package/scripts/bongos/gate-review.js +0 -330
  152. package/scripts/bongos/goal-names.js +0 -102
  153. package/scripts/bongos/rename-goals.js +0 -109
  154. package/scripts/bongos/seed-adopt-tasks.js +0 -75
  155. package/scripts/bongos/seed-api-contract-goal-tasks.js +0 -141
  156. package/scripts/bongos/seed-board-room-tasks.js +0 -196
  157. package/scripts/bongos/seed-bongos-app-tasks.js +0 -294
  158. package/scripts/bongos/seed-bongos-coreB-tranche1-tasks.js +0 -81
  159. package/scripts/bongos/seed-bongos-coreB-tranche2-tasks.js +0 -100
  160. package/scripts/bongos/seed-bongos-goal-hierarchy-tasks.js +0 -307
  161. package/scripts/bongos/seed-bongos-module-system-tasks.js +0 -346
  162. package/scripts/bongos/seed-bongos-tasks.js +0 -214
  163. package/scripts/bongos/seed-bongos-v1-seo-tasks.js +0 -104
  164. package/scripts/bongos/seed-bv1-community-tasks.js +0 -131
  165. package/scripts/bongos/seed-bv1-privacy-invites-tasks.js +0 -133
  166. package/scripts/bongos/seed-bv1-profile-page-tasks.js +0 -161
  167. package/scripts/bongos/seed-bv1-r01-delta-tasks.js +0 -154
  168. package/scripts/bongos/seed-bv2-tweak-mode-tasks.js +0 -277
  169. package/scripts/bongos/seed-gds-v4-tasks.js +0 -624
  170. package/scripts/bongos/seed-governance-tasks.js +0 -340
  171. package/scripts/bongos/seed-medusa-tasks.js +0 -440
  172. package/scripts/bongos/seed-module-upstreaming.js +0 -219
  173. package/scripts/bongos/seed-productization-tasks.js +0 -235
  174. package/scripts/bongos/seed-project-admin-console-tasks.js +0 -88
  175. package/scripts/bongos/seed-provisioning-tasks.js +0 -83
  176. package/scripts/bongos/seed-strict-versioning-tasks.js +0 -275
  177. package/scripts/bongos/seed-v2-world-tasks.js +0 -283
  178. package/scripts/gds/artifact-staleness.js +0 -7
  179. package/scripts/gds/ci-grade.js +0 -7
  180. package/scripts/gds/classify-discipline.js +0 -7
  181. package/scripts/gds/codemod-rename-src-gds.js +0 -7
  182. package/scripts/gds/coreB-carve-proof.sh +0 -8
  183. package/scripts/gds/gate-review.js +0 -7
  184. package/scripts/gds/rename-goals.js +0 -7
  185. package/scripts/gds/seed-adopt-tasks.js +0 -7
  186. package/scripts/gds/seed-api-contract-goal-tasks.js +0 -7
  187. package/scripts/gds/seed-board-room-tasks.js +0 -6
  188. package/scripts/gds/seed-bongos-app-tasks.js +0 -7
  189. package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +0 -7
  190. package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +0 -7
  191. package/scripts/gds/seed-bongos-goal-hierarchy-tasks.js +0 -7
  192. package/scripts/gds/seed-bongos-module-system-tasks.js +0 -7
  193. package/scripts/gds/seed-bongos-tasks.js +0 -7
  194. package/scripts/gds/seed-bongos-v1-seo-tasks.js +0 -6
  195. package/scripts/gds/seed-bv1-community-tasks.js +0 -6
  196. package/scripts/gds/seed-bv1-privacy-invites-tasks.js +0 -6
  197. package/scripts/gds/seed-bv1-profile-page-tasks.js +0 -6
  198. package/scripts/gds/seed-bv1-r01-delta-tasks.js +0 -6
  199. package/scripts/gds/seed-bv2-tweak-mode-tasks.js +0 -6
  200. package/scripts/gds/seed-gds-v4-tasks.js +0 -7
  201. package/scripts/gds/seed-governance-tasks.js +0 -7
  202. package/scripts/gds/seed-medusa-tasks.js +0 -7
  203. package/scripts/gds/seed-module-upstreaming.js +0 -7
  204. package/scripts/gds/seed-productization-tasks.js +0 -7
  205. package/scripts/gds/seed-project-admin-console-tasks.js +0 -6
  206. package/scripts/gds/seed-provisioning-tasks.js +0 -7
  207. package/scripts/gds/seed-strict-versioning-tasks.js +0 -7
  208. package/scripts/gds/seed-v2-world-tasks.js +0 -7
  209. package/scripts/migrate_cost_ledger.sh +0 -178
  210. package/tests/artifact_staleness.mjs +0 -207
  211. package/tests/gate_approvals.mjs +0 -611
  212. package/tests/gate_review.mjs +0 -152
  213. package/tests/goal_names.mjs +0 -120
  214. package/tests/newcomer_generator.mjs +0 -48
@@ -126,6 +126,16 @@ function carve(parent, names, why) {
126
126
  }
127
127
 
128
128
  const ADDED_SINCE_R101 = [
129
+ // task 1004859: the hard-floor list moved out of gate-review.js (retired below) into
130
+ // its own file, which keeps the self-protection gate-review.js had. Anchored on the
131
+ // retired entry so it lands where the list used to live.
132
+ {
133
+ after: 'scripts/bongos/gate-review.js',
134
+ glob: 'scripts/bongos/hard-floor.js',
135
+ why: 'the trust-boundary hard-floor list, moved out of the removed gate-review.js; '
136
+ + 'publish-manifest.js and adopt-preflight.js read it, so an edit here changes what '
137
+ + 'the public mirror withholds and what an adopted repo is warned about.',
138
+ },
129
139
  // task 1003366, audit ref MIT1 (2026-08-29 security audit, goal 1000084). The EIGHT
130
140
  // execution roots the merge resolver runs out of a cloned branch. The audit named four
131
141
  // of them ("the three gen-* scripts and .gitattributes"); modules/lifecycle/executed-
@@ -330,6 +340,12 @@ const RENAMED_SINCE_R101 = [
330
340
  // protection from code that still runs. Applied LAST, after renames and additions,
331
341
  // so an addition anchored on a retired entry still resolves.
332
342
  const RETIRED_SINCE_R101 = [
343
+ ...['gate-review.js', 'ci-grade.js'].map((f) => ({
344
+ glob: `scripts/bongos/${f}`,
345
+ why: 'task 1004859 deleted the two drivers of the planned gate-review and grade-gate '
346
+ + 'CI checks (ADR 0058). No workflow ever ran them and branch protection requires '
347
+ + 'only the unit check; the hard-floor list gate-review.js held moved to hard-floor.js.',
348
+ })),
333
349
  ...['boxes.js', 'box-access.js', 'box-credential.js'].map((f) => ({
334
350
  glob: `modules/dev-box/${f}`,
335
351
  why: 'task 1003892 (goal 1000120) deleted the dev-box module outright — the rank floor '
@@ -446,7 +462,7 @@ test('every glob added since R101 carries a documented reason', () => {
446
462
  // floor-gating ordinary source at Metic — src/bongos/logger.js, src/branding.js and
447
463
  // src/build-info.js are all in the closure — and locking newcomers out of routine work.
448
464
  // The entrypoints are different in kind: they exist ONLY to be executed by the pipeline,
449
- // which is exactly what group 'pipeline' already means for ship.js and gate-review.js.
465
+ // which is exactly what group 'pipeline' already means for ship.js and hard-floor.js.
450
466
  test('every execution root in the closure is a protected surface (MIT1 drift tripwire)', () => {
451
467
  const ec = require(path.join(ROOT, 'modules', 'lifecycle', 'executed-closure.js'));
452
468
  // ENTRYPOINTS lists both script-tree spellings across the C-series rename window; only
@@ -657,10 +673,10 @@ test('every credential reader in the tree is a protected surface (1002891 tripwi
657
673
  }
658
674
  });
659
675
 
660
- // ---------- the hard-floor subset: gate-review's second list may narrow, never diverge ----------
676
+ // ---------- the hard-floor subset: the second list may narrow, never diverge ----------
661
677
  //
662
678
  // task 1002875, from idea 1000620 (raised as a major by the BV1.R101 ship grader).
663
- // scripts/bongos/gate-review.js keeps its own HARD_FLOOR_GLOBS — the rank-INDEPENDENT
679
+ // scripts/bongos/hard-floor.js (gate-review.js until task 1004859) keeps HARD_FLOOR_GLOBS — the rank-INDEPENDENT
664
680
  // escalate-to-owner set — and the comment above it declares the array to be "A subset of
665
681
  // PROTECTED_GLOBS". That was a checkable invariant stated in prose and enforced by nothing.
666
682
  //
@@ -673,13 +689,13 @@ test('every credential reader in the tree is a protected surface (1002891 tripwi
673
689
  //
674
690
  // Checked at BOTH altitudes on purpose. The source literal is what a reviewer reads and
675
691
  // what the decision asked be parsed; the exported array is what compileGlobMatcher actually
676
- // compiles at gate-review.js:102. Asserting they agree means a future refactor that
692
+ // compiles in hard-floor.js. Asserting they agree means a future refactor that
677
693
  // computes or filters the list cannot leave the visible source saying one thing while the
678
694
  // gate enforces another.
679
695
  function hardFloorGlobsFromSource() {
680
- const src = fs.readFileSync(path.join(ROOT, 'scripts', 'bongos', 'gate-review.js'), 'utf8');
696
+ const src = fs.readFileSync(path.join(ROOT, 'scripts', 'bongos', 'hard-floor.js'), 'utf8');
681
697
  const block = src.match(/const HARD_FLOOR_GLOBS = \[([\s\S]*?)\n\];/);
682
- assert.ok(block, 'could not find the HARD_FLOOR_GLOBS array literal in gate-review.js — '
698
+ assert.ok(block, 'could not find the HARD_FLOOR_GLOBS array literal in hard-floor.js — '
683
699
  + 'if it was renamed or restructured, this test must be taught the new shape rather '
684
700
  + 'than deleted: it is the only thing holding the declared subset honest.');
685
701
  return [...block[1].matchAll(/'([^']+)'/g)].map((m) => m[1]);
@@ -688,21 +704,21 @@ function hardFloorGlobsFromSource() {
688
704
  test('every hard-floor glob is a protected surface (1002875 subset tripwire)', () => {
689
705
  const fromSource = hardFloorGlobsFromSource();
690
706
  assert.ok(fromSource.length >= 20,
691
- `fixture sanity: parsed ${fromSource.length} hard-floor globs out of gate-review.js, `
707
+ `fixture sanity: parsed ${fromSource.length} hard-floor globs out of hard-floor.js, `
692
708
  + 'and the array has held about two dozen since it was written. A parse that returns '
693
709
  + 'few or none passes the subset check vacuously.');
694
710
 
695
- const gr = require(path.join(ROOT, 'scripts', 'bongos', 'gate-review.js'));
711
+ const gr = require(path.join(ROOT, 'scripts', 'bongos', 'hard-floor.js'));
696
712
  assert.deepEqual(fromSource, gr.HARD_FLOOR_GLOBS,
697
- 'the HARD_FLOOR_GLOBS literal in gate-review.js no longer matches the array the module '
713
+ 'the HARD_FLOOR_GLOBS literal in hard-floor.js no longer matches the array the module '
698
714
  + 'exports and compiles into its matcher. The source and the enforced list must be the '
699
715
  + 'same thing, or a reviewer reading the file is reading a list the gate does not use.');
700
716
 
701
717
  const registry = new Set(ppc.PROTECTED_GLOBS);
702
718
  for (const glob of gr.HARD_FLOOR_GLOBS) {
703
719
  assert.ok(registry.has(glob),
704
- `'${glob}' is a HARD FLOOR surface in scripts/bongos/gate-review.js — CI escalates any `
705
- + 'edit to it to the owner regardless of rank — but it is not in '
720
+ `'${glob}' is a HARD FLOOR surface in scripts/bongos/hard-floor.js — a trust-boundary path `
721
+ + 'whatever the author\'s rank — but it is not in '
706
722
  + 'modules/government/protected-surfaces.json, so no rank floor covers it at all. '
707
723
  + 'The hard floor is declared to be a SUBSET of the protected set: it may be '
708
724
  + 'narrower (most protected paths are ordinary Metic work and should not ping the '
@@ -1290,8 +1306,8 @@ test('compileGlobMatcher folds BOTH sides', () => {
1290
1306
  assert.ok(!m(null), 'nor is a missing path');
1291
1307
  });
1292
1308
 
1293
- test('gate-review classifies a CASE-VARIANT gate surface as hard-floor', () => {
1294
- const gr = require(path.join(ROOT, 'scripts', 'bongos', 'gate-review.js'));
1309
+ test('the hard-floor matcher folds a CASE-VARIANT gate surface', () => {
1310
+ const gr = require(path.join(ROOT, 'scripts', 'bongos', 'hard-floor.js'));
1295
1311
  const literal = gr.HARD_FLOOR_GLOBS.find((g) => !g.includes('*') && !g.endsWith('/'));
1296
1312
  assert.ok(literal, 'no literal hard-floor glob — this test is reading the wrong shape');
1297
1313
  assert.ok(gr.matchesHardFloor(literal), 'the exact spelling must escalate (sanity)');
@@ -510,7 +510,6 @@ test('each contributed source gates on EXACTLY the atom its own list route requi
510
510
  const factories = [
511
511
  'modules/ideas/routes/blockers.js',
512
512
  'modules/lifecycle/routes/lifecycle.js',
513
- 'modules/lifecycle/routes/gate-approvals.js',
514
513
  'modules/onboarding/routes/access-requests.js',
515
514
  'modules/security/routes/reports.js',
516
515
  'modules/autonomy/routes/autonomy.js',
@@ -528,7 +527,6 @@ test('each contributed source gates on EXACTLY the atom its own list route requi
528
527
  override_requests: ['modules/lifecycle/routes/tasks.js', 'GET /override-requests'],
529
528
  removal_proposals: ['modules/lifecycle/routes/removal-proposals.js', 'GET /removal-proposals'],
530
529
  criteria_uat: ['modules/lifecycle/routes/done-when.js', 'GET /done-when/pending-review'],
531
- gate_approvals: ['modules/lifecycle/routes/gate-approvals.js', 'GET /gate-approvals'],
532
530
  access_requests: ['modules/onboarding/routes/access-requests.js', 'GET /access-requests'],
533
531
  redteam_reports: ['modules/security/routes/reports.js', 'GET /security/reports'],
534
532
  core_update: ['src/bongos/routes/core-update.js', 'GET /core-update'],
@@ -24,7 +24,6 @@ const lifeActs = require(path.join(ROOT, 'modules', 'lifecycle', 'matter-acts.js
24
24
  const lifeDb = require(path.join(ROOT, 'modules', 'lifecycle', 'db.js'));
25
25
  const doneWhen = require(path.join(ROOT, 'modules', 'lifecycle', 'done-when.js'));
26
26
  const doneWhenRoutes = require(path.join(ROOT, 'modules', 'lifecycle', 'routes', 'done-when.js'));
27
- const gateRoutes = require(path.join(ROOT, 'modules', 'lifecycle', 'routes', 'gate-approvals.js'));
28
27
  const lifeDocket = require(path.join(ROOT, 'modules', 'lifecycle', 'governor-docket.js'));
29
28
  const buildersRoute = require(path.join(ROOT, 'src', 'bongos', 'routes', 'builders.js'));
30
29
  const rrc = require(path.join(ROOT, 'src', 'bongos', 'route-rank-check.js'));
@@ -47,7 +46,6 @@ test('each building\'s act is checked strictly when the matter is opened: shape,
47
46
  const v = decisionBoard.validateMatterAction;
48
47
  assert.deepEqual(v('automation', { type: 'switch_on' }), { ok: true, action: { type: 'switch_on' } });
49
48
  assert.deepEqual(v('automation', { type: 'allow_goal', goal_id: '12' }).action, { type: 'allow_goal', goal_id: 12 });
50
- assert.deepEqual(v('release', { type: 'approve_gate', pr: 1500 }).action, { type: 'approve_gate', pr: 1500 });
51
49
  assert.deepEqual(v('release', { type: 'approve_override', request_id: 4 }).action, { type: 'approve_override', request_id: 4 });
52
50
  assert.deepEqual(v('review', { type: 'satisfy_criterion', criterion_id: 9 }).action, { type: 'satisfy_criterion', criterion_id: 9 });
53
51
  assert.deepEqual(v('treasury', { type: 'set_budget', builder_id: 7, monthly_usd: 50 }).action, { type: 'set_budget', builder_id: 7, monthly_usd: 50 });
@@ -58,6 +56,8 @@ test('each building\'s act is checked strictly when the matter is opened: shape,
58
56
  assert.equal(v('automation', { type: 'satisfy_criterion', criterion_id: 9 }).reason, 'bad_action', 'an act from another building');
59
57
  assert.equal(v('automation', { type: 'switch_on', goal_id: 3 }).reason, 'unknown_field');
60
58
  assert.equal(v('automation', { type: 'allow_goal' }).field, 'action.goal_id');
59
+ // task 1004859 removed the gate-approval route, so Release no longer carries an act for it
60
+ assert.equal(v('release', { type: 'approve_gate', pr: 1500 }).reason, 'bad_action', 'the retired gate act is refused');
61
61
  assert.equal(v('automation', { type: 'allow_goal', goal_id: 0 }).reason, 'bad_action');
62
62
  assert.equal(v('automation', { type: 'allow_goal', goal_id: true }).reason, 'bad_action');
63
63
  assert.equal(v('treasury', { type: 'set_budget', builder_id: 7, monthly_usd: -1 }).reason, 'bad_action');
@@ -161,7 +161,7 @@ test('a passed Review matter signs the criterion off by override, once', async (
161
161
  assert.match(signed[0].reason, /passed Review matter/);
162
162
  });
163
163
 
164
- test('a passed Release matter approves an override or clears a gate; a request already decided is said, not thrown', async () => {
164
+ test('a passed Release matter approves an override; a request already decided is said, not thrown', async () => {
165
165
  const calls = [];
166
166
  await stub(lifeDb, {
167
167
  decideOverrideRequest: async (a) => {
@@ -169,14 +169,12 @@ test('a passed Release matter approves an override or clears a gate; a request a
169
169
  if (a.requestId === 5) throw Object.assign(new Error('closed'), { code: 'REQUEST_NOT_OPEN' });
170
170
  return {};
171
171
  },
172
- }, () => stub(gateRoutes, { approveGatePr: async (a) => { calls.push(['gate', a]); return a.number === 2 ? { ok: false, code: 'pr_not_open' } : { ok: true, body: {} }; } }, async () => {
172
+ }, async () => {
173
173
  assert.equal((await closeMatter({ area: 'release', action: { type: 'approve_override', request_id: 4 } })).out.acted.done, true);
174
174
  assert.equal((await closeMatter({ area: 'release', action: { type: 'approve_override', request_id: 5 } })).out.acted.reason, 'request_not_open');
175
- assert.equal((await closeMatter({ area: 'release', action: { type: 'approve_gate', pr: 1500 } })).out.acted.done, true);
176
- assert.equal((await closeMatter({ area: 'release', action: { type: 'approve_gate', pr: 2 } })).out.acted.reason, 'pr_not_open');
177
- }));
175
+ });
178
176
  assert.deepEqual(calls[0], ['override', { requestId: 4, archonId: '42', decision: 'approve', note: 'A passed Release matter (matter 8) decided it.' }]);
179
- assert.equal(calls[2][1].number, 1500);
177
+ assert.equal(calls.length, 2);
180
178
  });
181
179
 
182
180
  test('a passed Treasury matter sets the limit through the same write as PATCH /builders/:id/budget', async () => {
@@ -384,7 +384,7 @@ const FEEDS = (ms, goals = [goal(1)], rollups = [rollup(1, { done: 2, active: 1
384
384
 
385
385
  test('the page reads the three feeds and draws the docket\'s pins and line', async () => {
386
386
  const soon = new Date(Date.now() + 2 * 3600000).toISOString();
387
- const ms = [matter({ id: 'board:1', building: 'charter', state: 'needs', deadline: soon }), matter({ id: 'blockers:2', state: 'needs' }), matter({ id: 'blockers:4', state: 'moving' }), matter({ id: 'gate_approvals:3', building: 'release', state: 'moving' }), matter({ id: 'removal_proposals:5', building: 'disputes', state: 'settled' })];
387
+ const ms = [matter({ id: 'board:1', building: 'charter', state: 'needs', deadline: soon }), matter({ id: 'blockers:2', state: 'needs' }), matter({ id: 'blockers:4', state: 'moving' }), matter({ id: 'override_requests:3', building: 'release', state: 'moving' }), matter({ id: 'removal_proposals:5', building: 'disputes', state: 'settled' })];
388
388
  const p = await bootPage({ feeds: FEEDS(ms) });
389
389
  assert.deepEqual([...p.calls].sort(), ['/api/bongos/goals', '/api/bongos/goals/rollups', '/api/bongos/government/docket']);
390
390
  const pins = p.byId.get('cityPins').children;
@@ -182,8 +182,9 @@ test('every act maps to the route that does it, with the body that route accepts
182
182
  const ov = card([{ id: 'override_requests:9', source: 'override_requests', building: 'release', state: 'needs', title: 'Release', ref: { type: 'override_request', id: '9' } }], 'override_requests:9');
183
183
  assert.deepEqual(C.request(ov, 'release'), { method: 'POST', url: '/api/bongos/override-requests/9/decide', body: { decision: 'approve' } });
184
184
  assert.equal(C.request(ov, 'hold'), null, 'holding records nothing');
185
+ // task 1004859 removed POST /gate-approvals/:pr/approve: a stale gate matter must send nothing
185
186
  const gate = card([{ id: 'gate_approvals:812', source: 'gate_approvals', building: 'release', state: 'needs', title: 'A PR', ref: { type: 'pull_request', id: 812 } }], 'gate_approvals:812');
186
- assert.deepEqual(C.request(gate, 'release'), { method: 'POST', url: '/api/bongos/gate-approvals/812/approve', body: {} });
187
+ assert.equal(C.request(gate, 'release'), null, 'the retired gate route is never called');
187
188
  });
188
189
 
189
190
  // Task 1004559 moved the core update onto the Release desk, check first: the ONLY
@@ -406,7 +407,6 @@ const SOURCE_FIXTURES = () => {
406
407
  { id: 'redteam_reports:4', source: 'redteam_reports', building: 'disputes', rule: 'decide', state: 'needs', title: 'Security report: sign-in limit skipped', href: '/watch', ref: { type: 'vulnerability_report', id: '4' }, acts: [{ key: 'confirm' }, { key: 'dispute' }] },
407
408
  { id: 'criteria_uat:3', source: 'criteria_uat', building: 'review', rule: 'signoff', state: 'needs', title: 'The export runs nightly', href: '/goals#/goal/7', ref: { type: 'criterion', id: '3' }, acts: [{ key: 'signoff', options: [{ key: 'live', label: 'tested it live' }] }, { key: 'force', options: [{ key: 'tested_by_hand', label: 'tested by hand' }] }] },
408
409
  { id: 'core_update:1.21.20', source: 'core_update', building: 'release', rule: 'decide', state: 'needs', title: 'Take core 1.21.20 (running 1.21.19)', href: '/settings#software-update', ref: { type: 'core_version', id: '1.21.20' } },
409
- { id: 'gate_approvals:88', source: 'gate_approvals', building: 'release', rule: 'decide', state: 'needs', title: 'PR 88 waits at the gate', href: '/gate', ref: { type: 'pull_request', id: '88' } },
410
410
  { id: 'autobongos_fence:project', source: 'autobongos_fence', building: 'automation', rule: 'decide', state: 'needs', title: 'Unattended building is off', href: '/autobongos', ref: { type: 'autonomy_fence', id: 'project' }, acts: [{ key: 'switch_on' }] },
411
411
  { id: 'module_rechecks:7', source: 'module_rechecks', building: 'release', rule: 'rule', state: 'needs', title: 'image-tools 1.2.0 failed its security check', href: '/modules', ref: { type: 'module_version', id: 'image-tools@1.2.0' }, acts: [{ key: 'recheck', options: [{ key: 'fixed', label: 'fixed since' }] }] },
412
412
  // task 1004712: a dry pot's borrow from the reserve
@@ -91,9 +91,9 @@ test('the shared confirm modal sits on --z-modal, closed by one hairline and no
91
91
  const modal = css.slice(css.indexOf('.ov-modal {'), css.indexOf('.ov-modal .scroll__h'));
92
92
  assert.ok(!/box-shadow/.test(modal), 'no shadow under the dialog');
93
93
  assert.match(modal, /border: var\(--line\) solid var\(--rule\)/, 'one rule hairline');
94
- // task 1004829: the Gate's approve is the ask card now, and the card is its own
95
- // confirmation, so the page opens no modal at all — and styles none.
96
- assert.ok(!/ov-modal-bg/.test(read('gate.js')), 'gate.js opens no modal: the ask card is the confirmation');
94
+ // task 1004829 made the Gate's approve an ask card with no modal; task 1004859
95
+ // removed the approve altogether. The page still opens no modal — and styles none.
96
+ assert.ok(!/ov-modal-bg/.test(read('gate.js')), 'gate.js opens no modal');
97
97
  assert.ok(!/-modal-bg/.test(read('gate.css')), 'gate.css styles no modal of its own');
98
98
  });
99
99
 
@@ -303,15 +303,19 @@ test('a failed load on Watch and Sessions is said in the page body', () => {
303
303
  assert.match(read('sessions.html'), /id="ledger-status" role="status"/, 'the count and a failed page load sit on the ledger itself');
304
304
  });
305
305
 
306
- test('the Gate draws a held PR as the ask card only for a viewer the approve route admits', () => {
306
+ test('the page at /gate is the Autobongos panel: no held-PR list, and a Metic is told the controls are the Archon\'s', () => {
307
+ // task 1004859 (owner, 2026-10-25: "Keep page, drop PR list"). The held-PR list
308
+ // waited on a gate-review CI check that never ran; its route is gone, so the page
309
+ // must not call it, and the URL stays /gate so bookmarks hold.
307
310
  const js = read('gate.js');
308
- // the server's own answer, never a rank guess on the client
309
- assert.match(js, /renderRow\(pr, resp\.viewer_may_approve === true\)/);
310
- assert.match(js, /if \(mayApprove && !pr\.already_approved && window\.OTBAsk\)/);
311
- assert.match(js, /q: `Let PR #\$\{pr\.number\} through\?`/, 'a question, not a label');
312
- assert.match(js, /'data-act': 'approve'/);
313
- assert.match(js, /Waiting for approval/, 'everyone else reads the facts as a plain row');
311
+ assert.ok(!/['\`]\/gate-approvals/.test(js), 'gate.js no longer calls the removed /gate-approvals routes');
312
+ assert.ok(!/renderRow\(pr|OTBAsk/.test(js), 'no held-PR row or ask card is drawn');
313
+ assert.match(js, /if \(rank !== 'archon'\) \{ showArchonOnly\(\); return; \}/, 'a Metic gets the note, not an empty page');
314
+ assert.match(js, /The Autobongos controls are for the \$\{rankLabel\('archon'\)\}/, 'the rank is named from the pack');
314
315
  const html = read('gate.html');
315
- assert.ok(html.indexOf('hall-ask.js') > 0 && html.indexOf('hall-ask.js') < html.indexOf('gate.js?'), 'the card script loads before the page');
316
- assert.match(html, /hall-ask\.css/);
316
+ assert.match(html, /<title>Autobongos — /, 'the page says what it is');
317
+ assert.match(html, /data-page-title="Autobongos"/);
318
+ assert.match(html, /id="gate-archon-only"/);
319
+ assert.ok(!/gate-list|hall-ask\.js/.test(html), 'the list mount and the ask-card script are gone');
320
+ assert.match(read('shell.js'), /\{ id: 'gate', label: 'Autobongos', icon: 'gate', href: '\/gate'/, 'the nav names it, at the same URL');
317
321
  });
@@ -51,7 +51,7 @@ function board() {
51
51
 
52
52
  // ── a long-winded docket: every source, long words everywhere ─────────────────
53
53
  const LONG = 'The extraordinarily long-winded proposal to reorganise every single floor of the hall before winter';
54
- const SOURCES = [['board', 'charter'], ['blockers', 'planning'], ['access_requests', 'people'], ['goal_join_requests', 'people'], ['override_requests', 'release'], ['removal_proposals', 'disputes'], ['redteam_reports', 'disputes'], ['criteria_uat', 'review'], ['core_update', 'release'], ['gate_approvals', 'release'], ['autobongos_fence', 'automation'], ['module_rechecks', 'release'], ['grade_appeals', 'disputes'], ['standing_rules', 'rules'], ['treasury_spending', 'treasury']];
54
+ const SOURCES = [['board', 'charter'], ['blockers', 'planning'], ['access_requests', 'people'], ['goal_join_requests', 'people'], ['override_requests', 'release'], ['removal_proposals', 'disputes'], ['redteam_reports', 'disputes'], ['criteria_uat', 'review'], ['core_update', 'release'], ['autobongos_fence', 'automation'], ['module_rechecks', 'release'], ['grade_appeals', 'disputes'], ['standing_rules', 'rules'], ['treasury_spending', 'treasury']];
55
55
  function matter(over = {}) {
56
56
  return { id: 'blockers:1', source: 'blockers', building: 'planning', rule: 'majority', state: 'needs', title: 'A matter', short: 'a matter', where: 'Planning', opened_at: '2026-09-29T10:00:00.000Z', deadline: null, proposer: null, tally: null, href: null, ref: null, ...over };
57
57
  }
@@ -78,6 +78,9 @@ function longMatters() {
78
78
  }
79
79
  }
80
80
  out.push(matter({ id: 'board:900', source: 'board', building: 'planning', short: 'idea to ratify', title: LONG, state: 'needs' }));
81
+ // A second override waiting at Release: task 1004859 removed the gate-approval source that
82
+ // used to fill a Release floor, and the stress docket must still reach its 30 rows.
83
+ out.push(matter({ id: 'override_requests:901', source: 'override_requests', building: 'release', title: `${LONG} 901`, short: 'a matter with a very long short description', state: 'needs', ref: { id: 901 } }));
81
84
  return out;
82
85
  }
83
86
  const ROOM = { open: [{ id: '1', subject_type: 'constitutional_amendment', subject: { title: LONG, author_id: '7' }, votes: [] }, { id: '900', subject_type: 'full_idea', subject: { title: LONG }, votes: [] }], closed: [] };
@@ -176,7 +179,6 @@ test('a row\'s buttons are the card\'s acts under the board\'s words, and send t
176
179
  matter({ id: 'access_requests:11', source: 'access_requests', building: 'people', state: 'needs', proposer: { name: 'Ana Ruiz' }, ref: { id: 11 } }),
177
180
  matter({ id: 'goal_join_requests:12', source: 'goal_join_requests', building: 'people', state: 'needs', proposer: { name: 'Kai Mo' }, href: '/builders/goal/42', ref: { id: 12 } }),
178
181
  matter({ id: 'override_requests:13', source: 'override_requests', building: 'release', state: 'needs', ref: { id: 13 } }),
179
- matter({ id: 'gate_approvals:14', source: 'gate_approvals', building: 'release', state: 'needs', ref: { id: 14 } }),
180
182
  matter({ id: 'board:1', source: 'board', building: 'charter', state: 'needs', ref: { type: 'board_item', id: '1' } }),
181
183
  ];
182
184
  const dk = docket(ms);
@@ -189,7 +191,6 @@ test('a row\'s buttons are the card\'s acts under the board\'s words, and send t
189
191
  const rel = D.rowsFor('release', 0, dk, { now: NOW });
190
192
  assert.deepEqual(rel[0].actions.map((a) => a.label), ['release', 'hold']);
191
193
  assert.equal(C.request(rel[0].card, 'release').url, '/api/bongos/override-requests/13/decide');
192
- assert.equal(C.request(D.rowsFor('release', 5, dk, { now: NOW })[0].card, 'release').url, '/api/bongos/gate-approvals/14/approve');
193
194
  // a vote: yes and no, and the no's reason is a chip, never free text
194
195
  const vote = row('charter', 0);
195
196
  assert.deepEqual(vote.actions.map((a) => [a.key, a.label]), [['for', 'yes'], ['object', 'no']]);
@@ -54,7 +54,7 @@ function proto(mode, st = {}) {
54
54
  function matter(over = {}) {
55
55
  return { id: 'blockers:1', source: 'blockers', building: 'planning', rule: 'majority', state: 'needs', title: 'A matter', short: 'a matter', where: 'Planning', opened_at: '2026-09-29T10:00:00.000Z', deadline: null, proposer: null, tally: null, href: null, ref: null, ...over };
56
56
  }
57
- const SOURCES = [['board', 'charter'], ['blockers', 'planning'], ['access_requests', 'people'], ['goal_join_requests', 'people'], ['override_requests', 'release'], ['removal_proposals', 'disputes'], ['redteam_reports', 'disputes'], ['criteria_uat', 'review'], ['core_update', 'release'], ['gate_approvals', 'release'], ['autobongos_fence', 'automation'], ['module_rechecks', 'release'], ['grade_appeals', 'disputes'], ['standing_rules', 'rules'], ['treasury_spending', 'treasury']];
57
+ const SOURCES = [['board', 'charter'], ['blockers', 'planning'], ['access_requests', 'people'], ['goal_join_requests', 'people'], ['override_requests', 'release'], ['removal_proposals', 'disputes'], ['redteam_reports', 'disputes'], ['criteria_uat', 'review'], ['core_update', 'release'], ['autobongos_fence', 'automation'], ['module_rechecks', 'release'], ['grade_appeals', 'disputes'], ['standing_rules', 'rules'], ['treasury_spending', 'treasury']];
58
58
  const READINGS = [['flow', 'review'], ['promises', 'planning'], ['money', 'treasury'], ['people', 'people'], ['fairness', 'disputes'], ['rules', 'rules']];
59
59
  function docket(matters = [], over = {}) {
60
60
  const counts = { needs: 0, moving: 0, settled: 0 };
package/tests/helpers.mjs CHANGED
@@ -1136,3 +1136,37 @@ export function fakeHttpsGet(routes) {
1136
1136
  };
1137
1137
  return { get, asked };
1138
1138
  }
1139
+
1140
+ // insertTestBuilders — insert throwaway `builders` rows that satisfy the MIGRATED schema
1141
+ // (task 1004863). builders.github_id became NOT NULL + UNIQUE, and the database tests
1142
+ // that each hand-wrote `INSERT INTO builders (id, github_login, …)` broke on it at once,
1143
+ // unnoticed, because the lanes that ran them were weekly or absent. One spelling here
1144
+ // fixes the drift, not just the column: a future NOT NULL column is added in one place.
1145
+ //
1146
+ // `db` is anything with `query(text, params)` (a pg Pool, or a client inside a
1147
+ // transaction). Each row is `{ id, login, ...columns }`: `github_id` defaults to
1148
+ // `test-gh-<id>` (unique per id, never a real GitHub id) and `display_name` to the login;
1149
+ // any other builders column may be named (rank, status, system_role, total_credits …).
1150
+ // A column one row names and another does not is written as DEFAULT for the other, so
1151
+ // the table's own defaults still apply. Column names come from the caller's literal
1152
+ // fixtures, never from input. `onConflict` is appended verbatim (default
1153
+ // `ON CONFLICT (id) DO NOTHING`; pass '' for none). Returns the inserted rows.
1154
+ export async function insertTestBuilders(db, rows, { onConflict = 'ON CONFLICT (id) DO NOTHING' } = {}) {
1155
+ if (!rows.length) return [];
1156
+ const shaped = rows.map(({ id, login, ...rest }) => {
1157
+ if (id == null || !login) throw new Error('insertTestBuilders: every row needs an id and a login');
1158
+ return { id, github_id: `test-gh-${id}`, github_login: login, display_name: login, ...rest };
1159
+ });
1160
+ const cols = [...new Set(shaped.flatMap((r) => Object.keys(r)))];
1161
+ const params = [];
1162
+ const tuples = shaped.map((r) => `(${cols.map((c) => {
1163
+ if (!(c in r)) return 'DEFAULT';
1164
+ params.push(r[c]);
1165
+ return `$${params.length}`;
1166
+ }).join(', ')})`);
1167
+ const { rows: out } = await db.query(
1168
+ `INSERT INTO builders (${cols.map((c) => `"${c}"`).join(', ')}) VALUES ${tuples.join(', ')} ${onConflict} RETURNING *`,
1169
+ params,
1170
+ );
1171
+ return out;
1172
+ }
@@ -1,11 +1,16 @@
1
1
  // tests/howto_state.mjs — per-person read/unread state for the how-to steps (task 1004634)
2
2
  //
3
- // Two halves. The DB-free half drives howto-state.js against an in-memory pool that
4
- // honours the three statements it issues, so the contract runs everywhere: unread by
5
- // default, mark read, skip all, a read upgrades a skip (never the reverse), the first
6
- // read time is kept, reset some or all, and an id the guide does not name is refused.
7
- // The DB half applies the real migration and runs the same calls against Postgres; it
8
- // skips when no Postgres is reachable (DATABASE_URL), like copy_desk_flags_db.mjs.
3
+ // The DB-free half. It drives howto-state.js against an in-memory pool that honours the
4
+ // three statements it issues, so the contract runs everywhere: unread by default, mark
5
+ // read, skip all, a read upgrades a skip (never the reverse), the first read time is
6
+ // kept, reset some or all, and an id the guide does not name is refused.
7
+ //
8
+ // The SAME contract runs against Postgres in tests/howto_state_db.mjs, which imports
9
+ // `contract` from here (task 1004863). That half used to live in this file, where it ran
10
+ // in the DB-free unit lane against whatever database DATABASE_URL reached — creating a
11
+ // stand-in `builders` table there if one was missing — and skipped in CI, so no lane
12
+ // ever ran it. Split out, it is in the integration lane and uses the migrated schema.
13
+ // The tests below run only when this file is the entry point, so importing it runs none.
9
14
  //
10
15
  // Run: node tests/howto_state.mjs
11
16
 
@@ -60,7 +65,8 @@ function fakePool() {
60
65
  };
61
66
  }
62
67
 
63
- async function contract(label, deps, A, B) {
68
+ // `test` is the caller's runner, so each file counts and reports its own cases.
69
+ export async function contract(test, label, deps, A, B) {
64
70
  const first = guide.STEPS[0].id;
65
71
  const second = guide.STEPS[1].id;
66
72
 
@@ -126,53 +132,24 @@ async function contract(label, deps, A, B) {
126
132
  });
127
133
  }
128
134
 
129
- await test('shapeState ignores a stored mark for a step the guide no longer has', async () => {
130
- const s = hs.shapeState([{ step_id: 'retired_step', how: 'read', read_at: new Date() }]);
131
- assert.equal(s.read, 0);
132
- assert.equal(s.total, guide.STEPS.length);
133
- });
134
-
135
- await test('the module declares its migration and the routes are mounted', async () => {
136
- const mod = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules/onboarding/module.json'), 'utf8'));
137
- assert.equal(mod.contributes.migrations, true);
138
- assert.ok(fs.existsSync(path.join(ROOT, 'modules/onboarding/migrations/onboarding_001_howto_reads.sql')));
139
- const src = fs.readFileSync(path.join(ROOT, 'modules/onboarding/routes/onboarding.js'), 'utf8');
140
- for (const r of ["router.get('/me/howto'", "router.post('/me/howto/read'", "router.post('/me/howto/reset'"]) assert.ok(src.includes(r), r);
141
- });
142
-
143
- await contract('in-memory', { pool: fakePool() }, 1, 2);
144
-
145
- // ── the real database ────────────────────────────────────────────────────────
146
- const { Pool } = require('pg');
147
- const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
148
- let dbUp = true;
149
- try { await pool.query('SELECT 1'); } catch (e) {
150
- dbUp = false;
151
- console.log(` (db half SKIPPED — no reachable Postgres: ${e.code || e.message}. Set DATABASE_URL to run.)`);
152
- }
153
- if (dbUp) {
154
- await pool.query(`
155
- CREATE TABLE IF NOT EXISTS schema_migrations (version text PRIMARY KEY);
156
- CREATE TABLE IF NOT EXISTS builders (id bigserial PRIMARY KEY, github_login text);
157
- `);
158
- await pool.query(fs.readFileSync(path.join(ROOT, 'modules/onboarding/migrations/onboarding_001_howto_reads.sql'), 'utf8'));
159
- await pool.query(fs.readFileSync(path.join(ROOT, 'modules/onboarding/migrations/onboarding_001_howto_reads.sql'), 'utf8')); // idempotent
160
- const A = 910000001;
161
- const B = 910000002;
162
- await pool.query('INSERT INTO builders (id, github_login) VALUES ($1, $2), ($3, $4) ON CONFLICT (id) DO NOTHING', [A, 'howto-a', B, 'howto-b']);
163
- await pool.query('DELETE FROM onboarding_howto_reads WHERE builder_id IN ($1, $2)', [A, B]);
164
- try {
165
- await contract('postgres', { pool }, A, B);
166
- await test('postgres: the CHECK refuses a malformed step id and a bad how', async () => {
167
- await assert.rejects(pool.query("INSERT INTO onboarding_howto_reads (builder_id, step_id) VALUES ($1, 'Bad Id')", [A]));
168
- await assert.rejects(pool.query("INSERT INTO onboarding_howto_reads (builder_id, step_id, how) VALUES ($1, 'welcome', 'seen')", [A]));
169
- });
170
- } finally {
171
- await pool.query('DELETE FROM onboarding_howto_reads WHERE builder_id IN ($1, $2)', [A, B]).catch(() => {});
172
- await pool.query('DELETE FROM builders WHERE id IN ($1, $2)', [A, B]).catch(() => {});
173
- }
174
- }
175
- await pool.end().catch(() => {});
135
+ const isMain = Boolean(process.argv[1]) && fs.realpathSync(url.fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
136
+ if (isMain) {
137
+ await test('shapeState ignores a stored mark for a step the guide no longer has', async () => {
138
+ const s = hs.shapeState([{ step_id: 'retired_step', how: 'read', read_at: new Date() }]);
139
+ assert.equal(s.read, 0);
140
+ assert.equal(s.total, guide.STEPS.length);
141
+ });
176
142
 
177
- console.log(`\n${passed} passed, ${failed} failed`);
178
- if (failed) process.exit(1);
143
+ await test('the module declares its migration and the routes are mounted', async () => {
144
+ const mod = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules/onboarding/module.json'), 'utf8'));
145
+ assert.equal(mod.contributes.migrations, true);
146
+ assert.ok(fs.existsSync(path.join(ROOT, 'modules/onboarding/migrations/onboarding_001_howto_reads.sql')));
147
+ const src = fs.readFileSync(path.join(ROOT, 'modules/onboarding/routes/onboarding.js'), 'utf8');
148
+ for (const r of ["router.get('/me/howto'", "router.post('/me/howto/read'", "router.post('/me/howto/reset'"]) assert.ok(src.includes(r), r);
149
+ });
150
+
151
+ await contract(test, 'in-memory', { pool: fakePool() }, 1, 2);
152
+
153
+ console.log(`\n${passed} passed, ${failed} failed`);
154
+ if (failed) process.exit(1);
155
+ }
@@ -0,0 +1,68 @@
1
+ // tests/howto_state_db.mjs — the how-to read/unread contract against a REAL Postgres
2
+ // (task 1004634; split out of tests/howto_state.mjs by task 1004863).
3
+ //
4
+ // tests/howto_state.mjs runs the contract against an in-memory pool in the DB-free unit
5
+ // lane. This runs the SAME contract (imported, not copied) against the real migration
6
+ // and the real CHECK constraints, in the integration lane (run-unit-tests.js INTEGRATION).
7
+ // It used to be the second half of that file, where it ran in the unit lane against
8
+ // whatever DATABASE_URL reached, created a stand-in `builders` table there if one was
9
+ // missing, and skipped in CI — so no lane ever executed it, and it broke unnoticed when
10
+ // builders.github_id became NOT NULL.
11
+ //
12
+ // Needs a MIGRATED database (scripts/migrate.sh). SKIPS (exit 0) with no reachable
13
+ // Postgres, like every *_db test.
14
+ //
15
+ // Run: DATABASE_URL=postgres://... node tests/howto_state_db.mjs
16
+
17
+ import { strict as assert } from 'node:assert';
18
+ import { createRequire } from 'node:module';
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import url from 'node:url';
22
+ import { contract } from './howto_state.mjs';
23
+ import { insertTestBuilders } from './helpers.mjs';
24
+
25
+ const require = createRequire(import.meta.url);
26
+ const ROOT = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..');
27
+ const { Pool } = require('pg');
28
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
29
+
30
+ try {
31
+ await pool.query('SELECT 1');
32
+ } catch (e) {
33
+ console.log(`howto_state_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}). Set DATABASE_URL to run.`);
34
+ await pool.end().catch(() => {});
35
+ process.exit(0);
36
+ }
37
+
38
+ let passed = 0;
39
+ let failed = 0;
40
+ async function test(name, fn) {
41
+ try { await fn(); passed++; console.log(` ok ${name}`); }
42
+ catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
43
+ }
44
+
45
+ const MIGRATION = fs.readFileSync(path.join(ROOT, 'modules/onboarding/migrations/onboarding_001_howto_reads.sql'), 'utf8');
46
+ await test('postgres: the migration re-applies cleanly over the migrated schema (idempotent)', async () => {
47
+ await pool.query(MIGRATION);
48
+ await pool.query(MIGRATION);
49
+ });
50
+
51
+ const A = 910000101;
52
+ const B = 910000102;
53
+ await insertTestBuilders(pool, [{ id: A, login: 'howto-a' }, { id: B, login: 'howto-b' }]);
54
+ await pool.query('DELETE FROM onboarding_howto_reads WHERE builder_id IN ($1, $2)', [A, B]);
55
+ try {
56
+ await contract(test, 'postgres', { pool }, A, B);
57
+ await test('postgres: the CHECK refuses a malformed step id and a bad how', async () => {
58
+ await assert.rejects(pool.query("INSERT INTO onboarding_howto_reads (builder_id, step_id) VALUES ($1, 'Bad Id')", [A]), (e) => e.code === '23514');
59
+ await assert.rejects(pool.query("INSERT INTO onboarding_howto_reads (builder_id, step_id, how) VALUES ($1, 'welcome', 'seen')", [A]), (e) => e.code === '23514');
60
+ });
61
+ } finally {
62
+ await pool.query('DELETE FROM onboarding_howto_reads WHERE builder_id IN ($1, $2)', [A, B]).catch(() => {});
63
+ await pool.query('DELETE FROM builders WHERE id IN ($1, $2)', [A, B]).catch(() => {});
64
+ await pool.end().catch(() => {});
65
+ }
66
+
67
+ console.log(`\nhowto_state_db: ${passed} passed, ${failed} failed`);
68
+ if (failed) process.exit(1);
@@ -133,13 +133,21 @@ async function fixture(client, { completeness = 70, developed = false, taskCount
133
133
  idea_id: idea.id,
134
134
  grade: 'full',
135
135
  completeness_score: completeness,
136
- ...(developer ? { developed_by: developer.id } : {}),
136
+ // developed_by and developed_at travel as a pair (ideas_grades_developed_pair_chk).
137
+ ...(developer ? { developed_by: developer.id, developed_at: new Date() } : {}),
138
+ });
139
+ // status is named, not placeholder-filled: versions_status_check admits only the
140
+ // lifecycle values, and 'building' is the one with no uniqueness rule of its own
141
+ // (versions_one_planning_idx allows exactly one 'planning' row, which the seeds own).
142
+ const version = await insertRow(client, 'versions', {
143
+ id: `IC6-${Date.now()}-${Math.random()}`.slice(0, 40), status: 'building',
137
144
  });
138
- const version = await insertRow(client, 'versions', { id: `IC6-${Date.now()}-${Math.random()}`.slice(0, 40) });
139
145
  const tasks = [];
140
146
  for (let i = 0; i < taskCount; i++) {
141
147
  const task = await insertRow(client, 'tasks', {
142
- version_id: version.id, title: `ic6 task ${i + 1}`, status: 'shipped',
148
+ // discipline is named for the same reason as the version status above:
149
+ // tasks_discipline_check admits only the live crafts.
150
+ version_id: version.id, title: `ic6 task ${i + 1}`, status: 'shipped', discipline: 'engineer',
143
151
  });
144
152
  await insertRow(client, 'ideas_task_lineage', {
145
153
  task_id: task.id, idea_id: idea.id, origin: 'promote',
@@ -51,7 +51,13 @@ const { INTEGRATION, INTEGRATION_PARKED, KNOWN_FAILING } = require('../scripts/b
51
51
  // LOWERED 11 → 6 by task 1004861: claim_gate_open_blocker, claim_gate_rebase,
52
52
  // clawback_confirmed_backlog, needs_rebase and reward_mode_cost_plus_only stopped adding a
53
53
  // second 'planning' version (and had three stale fixtures under it repaired).
54
- const MAX_PARKED = 6;
54
+ // LOWERED 6 → 1 by task 1004863: acclaim_feed_evidence_db, copy_desk_flags_db,
55
+ // government_board_membership_db, idea_credit_streams_db and session_earnings_mirror_db had
56
+ // stale fixtures (a missing builders.github_id, retired status/discipline values, a sliced
57
+ // copy of a route's SQL); repaired, they gate again.
58
+ // LOWERED 1 → 0: status-probe, the last entry, was deleted with its test by task 1004858.
59
+ // Nothing is parked; a new entry now needs this raised, deliberately.
60
+ const MAX_PARKED = 0;
55
61
 
56
62
  // The one test that proves the PUBLISHED package works end to end: packaged, installed into
57
63
  // a consumer, installed again from its own shipped lockfile, migrated onto EMPTY databases,
@@ -93,7 +93,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
93
93
  // says a held core version was tried and rolled back; task 1004731: the one-time
94
94
  // "Upgraded to core X" notice after a move; task 1004826: Standing is Accomplishments;
95
95
  // task 1004846: the new logo (its bump landed late, in task 1004852).
96
- const SHELL_SHA = 'bf052d6fabeb1953';
96
+ const SHELL_SHA = '670b2f71085c13b6';
97
97
 
98
98
  test('shell.js has not changed without its cache-bust being bumped', () => {
99
99
  const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);