@bongos/core 1.21.177 → 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 (121) hide show
  1. package/.bongos-core.json +140 -150
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +0 -6
  4. package/clients/bongos-client/index.cjs +0 -6
  5. package/clients/bongos-client/index.d.ts +0 -6
  6. package/clients/bongos-client/index.mjs +0 -6
  7. package/docs/adr/0042-builder-self-deploy-ci-auto-merge.md +3 -2
  8. package/docs/api/openapi.json +1 -87
  9. package/docs/api-reference.md +1 -8
  10. package/docs/canonical-permissions.md +3 -3
  11. package/docs/copy-inventory.md +25 -39
  12. package/docs/copy-registry.json +73 -200
  13. package/docs/file-map.md +3 -3
  14. package/docs/module-api-changelog.md +2 -0
  15. package/docs/modules-contract.md +2 -2
  16. package/docs/page-inventory.json +4 -7
  17. package/docs/page-readings.json +1520 -1569
  18. package/modules/government/catalog.js +10 -7
  19. package/modules/government/decision-board.js +0 -1
  20. package/modules/government/decision-rules.js +0 -1
  21. package/modules/government/docket.js +0 -1
  22. package/modules/government/protected-surfaces.js +1 -1
  23. package/modules/government/protected-surfaces.json +2 -3
  24. package/modules/hall-ui/public/atlas.html +1 -1
  25. package/modules/hall-ui/public/blockers.html +1 -1
  26. package/modules/hall-ui/public/board-room.html +1 -1
  27. package/modules/hall-ui/public/city.html +1 -1
  28. package/modules/hall-ui/public/collab.html +1 -1
  29. package/modules/hall-ui/public/commands.html +1 -1
  30. package/modules/hall-ui/public/copy-desk.html +1 -1
  31. package/modules/hall-ui/public/deploy.html +1 -1
  32. package/modules/hall-ui/public/diagrams.html +1 -1
  33. package/modules/hall-ui/public/drachmae.html +1 -1
  34. package/modules/hall-ui/public/fleet.html +1 -1
  35. package/modules/hall-ui/public/gate.css +4 -26
  36. package/modules/hall-ui/public/gate.html +12 -27
  37. package/modules/hall-ui/public/gate.js +26 -176
  38. package/modules/hall-ui/public/gate.states.json +4 -22
  39. package/modules/hall-ui/public/goal-map.html +1 -1
  40. package/modules/hall-ui/public/goals.html +1 -1
  41. package/modules/hall-ui/public/government.html +1 -1
  42. package/modules/hall-ui/public/idea.html +1 -1
  43. package/modules/hall-ui/public/ideas.html +1 -1
  44. package/modules/hall-ui/public/index.html +1 -1
  45. package/modules/hall-ui/public/modules.html +1 -1
  46. package/modules/hall-ui/public/my-agents.html +1 -1
  47. package/modules/hall-ui/public/primer.html +1 -1
  48. package/modules/hall-ui/public/profile.html +1 -1
  49. package/modules/hall-ui/public/project-settings.html +1 -1
  50. package/modules/hall-ui/public/ranks.html +1 -1
  51. package/modules/hall-ui/public/roadmap.html +1 -1
  52. package/modules/hall-ui/public/roster.html +1 -1
  53. package/modules/hall-ui/public/sessions.html +1 -1
  54. package/modules/hall-ui/public/settings.html +1 -1
  55. package/modules/hall-ui/public/shell.js +1 -1
  56. package/modules/hall-ui/public/street-card.js +5 -9
  57. package/modules/hall-ui/public/street-desk.js +2 -2
  58. package/modules/hall-ui/public/street-inside.js +1 -1
  59. package/modules/hall-ui/public/street.html +1 -1
  60. package/modules/hall-ui/public/studio.html +1 -1
  61. package/modules/hall-ui/public/task.html +1 -1
  62. package/modules/hall-ui/public/thinking.html +1 -1
  63. package/modules/hall-ui/public/tweak-editor.html +1 -1
  64. package/modules/hall-ui/public/watch.html +1 -1
  65. package/modules/hall-ui/public/work.html +1 -1
  66. package/modules/lifecycle/matter-acts.js +0 -6
  67. package/modules/lifecycle/module.json +0 -1
  68. package/package-lock.json +2 -2
  69. package/package.json +1 -1
  70. package/release-notes.json +22 -0
  71. package/scripts/bongos/adopt-preflight.js +3 -3
  72. package/scripts/bongos/audit-authorship.sh +1 -3
  73. package/scripts/bongos/bump-version.js +46 -9
  74. package/scripts/bongos/doc-cli-guard.js +2 -0
  75. package/scripts/bongos/doc-control-claims.js +2 -2
  76. package/scripts/bongos/government-walk.js +3 -9
  77. package/scripts/bongos/hard-floor.js +52 -0
  78. package/scripts/bongos/publish-manifest.js +3 -3
  79. package/scripts/bongos/run-unit-tests.js +17 -5
  80. package/scripts/gds/hard-floor.js +6 -0
  81. package/src/bongos/module-scope-map.js +2 -2
  82. package/src/bongos/permission-path-check.js +1 -1
  83. package/src/bongos/route-rank-check.js +1 -8
  84. package/src/bongos/routes.js +1 -2
  85. package/src/module-api.js +1 -1
  86. package/src/modules.js +1 -1
  87. package/tests/acclaim_feed_evidence_db.mjs +42 -23
  88. package/tests/adr_0089_scope.mjs +7 -5
  89. package/tests/bump_version.mjs +52 -18
  90. package/tests/copy_desk_flags_db.mjs +22 -14
  91. package/tests/gating_review_retired.mjs +1 -1
  92. package/tests/government_act_alone_buildings.mjs +1 -3
  93. package/tests/government_board_membership_db.mjs +35 -22
  94. package/tests/government_protected_surfaces.mjs +29 -13
  95. package/tests/governor_docket.mjs +0 -2
  96. package/tests/governor_matter_acts.mjs +6 -8
  97. package/tests/hall_city.mjs +1 -1
  98. package/tests/hall_matter_card.mjs +2 -2
  99. package/tests/hall_oversight_world.mjs +16 -12
  100. package/tests/hall_street_desk.mjs +4 -3
  101. package/tests/hall_street_inside.mjs +1 -1
  102. package/tests/helpers.mjs +34 -0
  103. package/tests/howto_state.mjs +33 -56
  104. package/tests/howto_state_db.mjs +68 -0
  105. package/tests/idea_credit_streams_db.mjs +11 -3
  106. package/tests/integration_parked_ratchet.mjs +7 -1
  107. package/tests/nav_permission_atoms.mjs +1 -1
  108. package/tests/page_notes_db.mjs +12 -8
  109. package/tests/page_tweak_no_rearm.mjs +50 -0
  110. package/tests/publish_manifest.mjs +64 -0
  111. package/tests/search_isolation.mjs +5 -103
  112. package/tests/search_isolation_db.mjs +140 -0
  113. package/tests/session_earnings_mirror_db.mjs +2 -0
  114. package/tests/workflow_exec_paths.mjs +9 -11
  115. package/modules/lifecycle/routes/gate-approvals.js +0 -299
  116. package/scripts/bongos/ci-grade.js +0 -82
  117. package/scripts/bongos/gate-review.js +0 -330
  118. package/scripts/gds/ci-grade.js +0 -7
  119. package/scripts/gds/gate-review.js +0 -7
  120. package/tests/gate_approvals.mjs +0 -611
  121. package/tests/gate_review.mjs +0 -152
@@ -22,7 +22,7 @@ const require = createRequire(import.meta.url);
22
22
  const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
23
23
 
24
24
  const bump = require(path.join(ROOT, 'scripts', 'bongos', 'bump-version.js'));
25
- const { bumpPatch, CONST_RE } = bump;
25
+ const { bumpPatch, nextFreePatch, registryVersions, CONST_RE } = bump;
26
26
  // The LIVE test below reads this repo's real release history (task 1003202).
27
27
  const { execFileSync } = require('node:child_process');
28
28
 
@@ -217,25 +217,59 @@ test('the push to main happens exactly once, and not inside the bump step', () =
217
217
  assert.ok(bumpStep.includes('git commit -m'), 'the bump step still commits locally — package-core stamps the version found AT the ref');
218
218
  });
219
219
 
220
- test('an already-published version is success, not a wedge', () => {
221
- // Publish-then-failed-push leaves a version on npm with no release commit.
222
- // The next run reads it as free (laneReleasedVersions is git-based), bumps to
223
- // it, and npm refuses the duplicate. Without this branch that run fails, and
224
- // so does every run after it — the lane wedges with no way out but a human.
220
+ test('an already-published version FAILS the publish and records nothing (task 1004886)', () => {
221
+ // Task 1003914 made a duplicate-version refusal count as success and fell through to
222
+ // the record step. But a version is left unrecorded only when its record push lost a
223
+ // race to a MOVED main, so the registry's copy is never this run's tree: on
224
+ // 2026-10-25 that recorded 1.21.175 on main for a tree npm's 1.21.175 (2026-10-23)
225
+ // never held. The refusal must now fail the step, which skips the record step.
225
226
  const pubAt = PUBLISH_YML.indexOf('id: pub');
226
227
  assert.ok(pubAt !== -1, 'the publish step must carry id: pub');
227
- const pubStep = PUBLISH_YML.slice(pubAt, PUBLISH_YML.indexOf('\n # PUBLISH VERDICT'));
228
- assert.match(
229
- pubStep,
230
- /cannot publish over\|previously published\|EPUBLISHCONFLICT/,
231
- 'the publish step must recognise npm refusing a duplicate version',
232
- );
233
- assert.match(pubStep, /already=true/, 'and must record that it took the already-present branch');
234
- assert.match(
235
- pubStep,
236
- /exit "\$RC"/,
237
- 'every OTHER publish failure must still fail the step — the tolerance is narrow on purpose',
238
- );
228
+ const pubStep = PUBLISH_YML.slice(pubAt, PUBLISH_YML.indexOf('\n # THE ONLY IRREVERSIBLE STEP'));
229
+ assert.match(pubStep, /cannot publish over\|previously published\|EPUBLISHCONFLICT/,
230
+ 'the publish step still recognises npm refusing a duplicate — to say so plainly');
231
+ assert.doesNotMatch(pubStep, /already=true/, 'no already-present success branch');
232
+ // After a non-zero npm publish, the only way out is `exit "$RC"`: the duplicate
233
+ // branch must not be able to reach the "published" line.
234
+ const failAt = pubStep.indexOf('if [ "$RC" -ne 0 ]; then');
235
+ const failBlock = pubStep.slice(failAt, pubStep.indexOf('\n fi\n echo "published', failAt));
236
+ assert.ok(failAt !== -1 && failBlock.trimEnd().endsWith('exit "$RC"'), 'every publish failure, the duplicate included, exits non-zero');
237
+ const recAt = PUBLISH_YML.indexOf('- name: Record the release on main');
238
+ const recIf = PUBLISH_YML.slice(recAt, PUBLISH_YML.indexOf('\n run:', recAt));
239
+ assert.doesNotMatch(recIf, /always\(\)|failure\(\)/, 'the record step must not run after a failed publish');
240
+ const verdict = PUBLISH_YML.slice(PUBLISH_YML.indexOf('# PUBLISH VERDICT'));
241
+ assert.doesNotMatch(verdict.slice(0, 1500), /ALREADY PRESENT/, 'the verdict no longer reports a recorded duplicate');
242
+ });
243
+
244
+ test('the registry listing is read and every listed version is skipped by the bump (task 1004886)', () => {
245
+ const verAt = PUBLISH_YML.indexOf('id: ver');
246
+ const verStep = PUBLISH_YML.slice(verAt, PUBLISH_YML.indexOf('\n - name:', verAt));
247
+ assert.match(verStep, /npm view @bongos\/core versions --json > "\$TAKEN_FILE"/, 'the resolve step reads the registry listing');
248
+ assert.match(verStep, /registryVersions\(.*\)\.has\(process\.argv\[2\]\) \? 0 : 1\)" "\$TAKEN_FILE" "\$HEAD_V"; then\s+TAKEN=true/,
249
+ 'a HEAD version already on the registry counts as taken, so it is bumped past');
250
+ assert.match(verStep, /echo '\[\]' > "\$TAKEN_FILE"/, 'an unreadable listing degrades to git-only, it does not stop the lane');
251
+ assert.match(verStep, /taken_file=/, 'the listing is handed on');
252
+ const bumpAt = PUBLISH_YML.indexOf('id: bump');
253
+ const bumpStep = PUBLISH_YML.slice(bumpAt, PUBLISH_YML.indexOf('\n - name:', bumpAt));
254
+ assert.match(bumpStep, /bump-version\.js --apply .*--taken-file "\$\{\{ steps\.ver\.outputs\.taken_file \}\}"/, 'the bump skips the listed versions');
255
+ });
256
+
257
+ test('bumpPatch skips every taken version (task 1004886)', () => {
258
+ const src = "const CORE_VERSION = '1.21.174'; // x\n";
259
+ assert.equal(bumpPatch(src).to, '1.21.175', 'nothing taken → the next patch, as before');
260
+ assert.equal(bumpPatch(src, { taken: new Set(['1.21.175']) }).to, '1.21.176', 'the 2026-10-25 shape: 1.21.175 is on npm with an older tree');
261
+ assert.equal(bumpPatch(src, { taken: new Set(['1.21.175', '1.21.176', '1.21.178']) }).to, '1.21.177', 'the first free patch, not the highest');
262
+ assert.equal(bumpPatch(src, { taken: new Set(['1.22.0', '1.21.174']) }).to, '1.21.175', 'other minors and the current version do not matter');
263
+ assert.match(bumpPatch(src, { taken: new Set(['1.21.175']) }).text, /^const CORE_VERSION = '1\.21\.176';/m, 'the literal carries the skipped-to version');
264
+ assert.throws(() => nextFreePatch(1, 0, 0, { has: () => true }), /no free patch/, 'a taken-list that covers everything throws, never spins');
265
+ });
266
+
267
+ test('registryVersions parses npm view output and drops anything that is not x.y.z', () => {
268
+ assert.deepEqual([...registryVersions('["1.21.174","1.21.175"]')], ['1.21.174', '1.21.175']);
269
+ assert.deepEqual([...registryVersions('"1.0.0"')], ['1.0.0'], 'npm prints a bare string when one version exists');
270
+ assert.deepEqual([...registryVersions('["1.2.3","1.2.4-rc.1","x; rm -rf",7]')], ['1.2.3']);
271
+ assert.equal(registryVersions('not json').size, 0);
272
+ assert.equal(registryVersions('').size, 0);
239
273
  });
240
274
 
241
275
  test('the record step gates on a bump actually having been made', () => {
@@ -26,15 +26,17 @@
26
26
  //
27
27
  // Run: DATABASE_URL=postgres://... node tests/copy_desk_flags_db.mjs
28
28
  //
29
- // It creates its OWN minimal `builders`/`tasks` stand-ins if they are absent, so
30
- // it works against a throwaway container as well as a full migrated database, and
31
- // it cleans up every row it writes.
29
+ // It needs a MIGRATED database (scripts/migrate.sh), the one the integration lane
30
+ // builds. It once created its own minimal `builders`/`tasks` stand-ins; those drifted
31
+ // from the real schema (builders.github_id became NOT NULL) and hid it, so they are
32
+ // gone (task 1004863). It cleans up every row it writes.
32
33
 
33
34
  import { createRequire } from 'node:module';
34
35
  import { strict as assert } from 'node:assert';
35
36
  import fs from 'node:fs';
36
37
  import path from 'node:path';
37
38
  import url from 'node:url';
39
+ import { insertTestBuilders } from './helpers.mjs';
38
40
 
39
41
  const require = createRequire(import.meta.url);
40
42
  const { Pool } = require('pg');
@@ -58,21 +60,19 @@ async function test(name, fn) {
58
60
  }
59
61
 
60
62
  // --- fixtures -------------------------------------------------------------
61
- // Minimal stand-ins ONLY if the real tables are absent (throwaway container).
62
- // Against a fully migrated DB these are no-ops and the real tables are used.
63
- await pool.query(`
64
- CREATE TABLE IF NOT EXISTS schema_migrations (version text PRIMARY KEY);
65
- CREATE TABLE IF NOT EXISTS builders (id bigserial PRIMARY KEY, github_login text, display_name text);
66
- CREATE TABLE IF NOT EXISTS tasks (id bigserial PRIMARY KEY);
67
- `);
68
-
69
63
  // Two throwaway builders + one task, at ids far above any real row.
70
64
  const A = 900000001;
71
65
  const B = 900000002;
72
66
  const T = 900000003;
73
- await pool.query('INSERT INTO builders (id, github_login, display_name) VALUES ($1,$2,$3),($4,$5,$6) ON CONFLICT (id) DO NOTHING',
74
- [A, 'cdtest-a', 'Copy Desk Test A', B, 'cdtest-b', 'Copy Desk Test B']);
75
- await pool.query('INSERT INTO tasks (id) VALUES ($1) ON CONFLICT (id) DO NOTHING', [T]);
67
+ await insertTestBuilders(pool, [
68
+ { id: A, login: 'cdtest-a', display_name: 'Copy Desk Test A' },
69
+ { id: B, login: 'cdtest-b', display_name: 'Copy Desk Test B' },
70
+ ]);
71
+ // The task needs a version (tasks.version_id is NOT NULL). Its own row, at status
72
+ // 'building': the seeds own the one 'planning' version the partial index allows.
73
+ const VER = 'ZZZ-CDF';
74
+ await pool.query("INSERT INTO versions (id, name, status) VALUES ($1, 'copy desk fixture', 'building') ON CONFLICT (id) DO NOTHING", [VER]);
75
+ await pool.query("INSERT INTO tasks (id, version_id, title, discipline) VALUES ($1, $2, 'copy desk fixture', 'engineer') ON CONFLICT (id) DO NOTHING", [T, VER]);
76
76
 
77
77
  // Apply the migration verbatim — this also proves it is idempotent, since a
78
78
  // migrated DB has already run it.
@@ -248,6 +248,12 @@ await test('closeFlag returns null on a second call — the route answers 409 fr
248
248
  // --- page asks (task 1004317 / TW06, copy_desk_002) -----------------------
249
249
  // A page ask's surface IS its page id, so these use SURF as the page id and the
250
250
  // cleanup above still catches every row.
251
+ //
252
+ // The section starts from no rows (task 1004863): the string-flag cases above leave
253
+ // open flags on SURF (some with a null string_id), and the one-open-per-target index
254
+ // keys on (flagged_by, surface, string_id) — so a page ask by the same builder on the
255
+ // same surface collided with them rather than testing anything about page asks.
256
+ await clean();
251
257
  console.log('copy_desk_flags — page asks (copy_desk_002)');
252
258
 
253
259
  const pageIns = (o = {}) => pool.query(
@@ -299,6 +305,8 @@ await test('pageAskSummary groups open asks by page: distinct askers, the oldest
299
305
 
300
306
  // --- cleanup --------------------------------------------------------------
301
307
  await clean();
308
+ await pool.query('DELETE FROM tasks WHERE id = $1', [T]).catch(() => {});
309
+ await pool.query('DELETE FROM versions WHERE id = $1', [VER]).catch(() => {});
302
310
  await pool.query('DELETE FROM builders WHERE id = ANY($1)', [[A, B]]).catch(() => {});
303
311
 
304
312
  console.log(`\n${passed} passed, ${failed} failed`);
@@ -43,7 +43,7 @@ test('cmdOn normalizes BOTH restore paths — a pre-retirement snapshot cannot r
43
43
 
44
44
  test('the CODEOWNERS file is gone; its GLOB guards remain where they enforce', () => {
45
45
  assert.ok(!fs.existsSync(path.join(ROOT, 'CODEOWNERS')), 'CODEOWNERS must not exist (ADR 0162)');
46
- const gateReview = fs.readFileSync(path.join(ROOT, 'scripts', 'bongos', 'gate-review.js'), 'utf8');
46
+ const gateReview = fs.readFileSync(path.join(ROOT, 'scripts', 'bongos', 'hard-floor.js'), 'utf8');
47
47
  assert.match(gateReview, /'CODEOWNERS'/, 'the HARD_FLOOR glob stays — reintroducing the file is itself gate-flagged');
48
48
  });
49
49
 
@@ -68,7 +68,7 @@ async function withPort(port, fn) {
68
68
  // The port the routers below are driven through (section 3); restored after
69
69
  // each gate test swaps in its own.
70
70
  const ATOMS = [
71
- 'autonomy.fence.manage', 'criterion.satisfy', 'criterion.unsatisfy', 'gate_approval.decide', 'override_request.decide',
71
+ 'autonomy.fence.manage', 'criterion.satisfy', 'criterion.unsatisfy', 'override_request.decide',
72
72
  'core.pin.move', 'builder.budget.set', 'goal.create', 'goal.reorder', 'version.focus.set', 'project.settings.manage',
73
73
  'security.report.adjudicate', 'access_request.review', 'government.manage',
74
74
  ];
@@ -211,7 +211,6 @@ app.use(attachFail);
211
211
  for (const rel of [
212
212
  'modules/autonomy/routes/autonomy.js',
213
213
  'modules/lifecycle/routes/done-when.js',
214
- 'modules/lifecycle/routes/gate-approvals.js',
215
214
  'modules/lifecycle/routes/tasks.js',
216
215
  'modules/lifecycle/routes/goals.js',
217
216
  'modules/lifecycle/routes/versions.js',
@@ -264,7 +263,6 @@ const REQUESTS = {
264
263
  'POST /autonomy/builders/:builderId/pause': ['POST', `/autonomy/builders/${BAD}/pause`, { paused: false }],
265
264
  'POST /done-when/:criterionId/satisfy': ['POST', `/done-when/${BAD}/satisfy`, {}],
266
265
  'POST /done-when/:criterionId/unsatisfy': ['POST', `/done-when/${BAD}/unsatisfy`, {}],
267
- 'POST /gate-approvals/:pr/approve': ['POST', `/gate-approvals/${BAD}/approve`, {}],
268
266
  'POST /override-requests/:id/decide': ['POST', `/override-requests/${BAD}/decide`, { decision: 'approve' }],
269
267
  'POST /provisioning/instances/:id/core-upgrade': ['POST', `/provisioning/instances/${BAD}/core-upgrade`, { to: '1.2.3' }],
270
268
  'POST /provisioning/instances/:id/update-channel': ['POST', `/provisioning/instances/${BAD}/update-channel`, { channel: 'nightly' }],
@@ -26,6 +26,7 @@
26
26
  // ids far above any real row, and deletes them at the end.
27
27
 
28
28
  import pg from 'pg';
29
+ import { insertTestBuilders } from './helpers.mjs';
29
30
  import { createRequire } from 'node:module';
30
31
  const require = createRequire(import.meta.url);
31
32
 
@@ -45,38 +46,48 @@ async function t(name, fn) {
45
46
  }
46
47
  const assert = (await import('node:assert')).strict;
47
48
 
48
- // Minimal stand-ins ONLY when the real tables are absent (throwaway container);
49
- // against a migrated database these are no-ops and the real tables are used.
50
- await pool.query(`
51
- CREATE TABLE IF NOT EXISTS builders (
52
- id bigserial PRIMARY KEY, github_login text, display_name text,
53
- status text DEFAULT 'active', system_role text);
54
- CREATE TABLE IF NOT EXISTS government_builder_ranks (
55
- builder_id bigint, rank_key text);
56
- `);
49
+ // Needs a MIGRATED database (the integration lane's). The stand-in tables this once
50
+ // created when the real ones were absent drifted from the schema (builders.github_id
51
+ // became NOT NULL) and are gone (task 1004863); the builder rows go through the shared
52
+ // helper so the next required column is added in one place.
53
+
54
+ // ONE TRANSACTION, ALWAYS ROLLED BACK (task 1004863). The machine principal below needs a
55
+ // real system_role, and system_role is UNIQUE with only two legal values — both held by
56
+ // the seeded system builders on any migrated database. So the fixture releases the seeded
57
+ // 'bfg' role inside this transaction, gives it to its own throwaway machine, and the
58
+ // rollback restores everything. The library under test is pointed at the same client, so
59
+ // it reads exactly the rows the fixture wrote.
60
+ const db = await pool.connect();
61
+ await db.query('BEGIN');
62
+ await db.query("UPDATE builders SET system_role = NULL WHERE system_role = 'bfg'");
57
63
 
58
64
  // Ids far above any real row, and every login prefixed, so cleanup is exact and
59
65
  // this can never touch a real builder or a real rank assignment.
60
66
  const B = 990000000;
61
67
  const ID = { xenos: B + 1, thetes: B + 2, metic: B + 3, archon: B + 4, gone: B + 5, bot: B + 6, ops: B + 7 };
62
68
  const cleanup = async () => {
63
- await pool.query('DELETE FROM government_builder_ranks WHERE builder_id >= $1', [B]);
64
- await pool.query('DELETE FROM builders WHERE id >= $1', [B]);
69
+ await db.query('DELETE FROM government_builder_ranks WHERE builder_id >= $1', [B]);
70
+ await db.query('DELETE FROM builders WHERE id >= $1', [B]);
65
71
  };
66
72
  await cleanup();
67
- await pool.query(`INSERT INTO builders (id, github_login, display_name, status, system_role) VALUES
68
- ($1,'gbmdb-x','Xenos','active',NULL),($2,'gbmdb-t','Thetes','active',NULL),
69
- ($3,'gbmdb-m','Metic','active',NULL),($4,'gbmdb-a','Archon','active',NULL),
70
- ($5,'gbmdb-gone','Deactivated Archon','inactive',NULL),
71
- ($6,'gbmdb-bot','A Machine','active','bfg'),
72
- ($7,'gbmdb-ops','Custom Ops','active',NULL)`,
73
- [ID.xenos, ID.thetes, ID.metic, ID.archon, ID.gone, ID.bot, ID.ops]);
74
- await pool.query(`INSERT INTO government_builder_ranks (builder_id, rank_key) VALUES
73
+ await insertTestBuilders(db, [
74
+ { id: ID.xenos, login: 'gbmdb-x', display_name: 'Xenos', status: 'active', system_role: null },
75
+ { id: ID.thetes, login: 'gbmdb-t', display_name: 'Thetes', status: 'active', system_role: null },
76
+ { id: ID.metic, login: 'gbmdb-m', display_name: 'Metic', status: 'active', system_role: null },
77
+ { id: ID.archon, login: 'gbmdb-a', display_name: 'Archon', status: 'active', system_role: null },
78
+ { id: ID.gone, login: 'gbmdb-gone', display_name: 'Deactivated Archon', status: 'inactive', system_role: null },
79
+ { id: ID.bot, login: 'gbmdb-bot', display_name: 'A Machine', status: 'active', system_role: 'bfg' },
80
+ { id: ID.ops, login: 'gbmdb-ops', display_name: 'Custom Ops', status: 'active', system_role: null },
81
+ ], { onConflict: '' });
82
+ // The custom rank is a real government_ranks row: government_builder_ranks.rank_key is a
83
+ // foreign key into it on a migrated database.
84
+ await db.query("INSERT INTO government_ranks (rank_key, label) VALUES ('gbmdb-ops-rank', 'Custom Ops (test)')");
85
+ await db.query(`INSERT INTO government_builder_ranks (builder_id, rank_key) VALUES
75
86
  ($1,'xenos'),($2,'thetes'),($3,'metic'),($4,'archon'),($5,'archon'),($6,'archon'),($7,'gbmdb-ops-rank')`,
76
87
  [ID.xenos, ID.thetes, ID.metic, ID.archon, ID.gone, ID.bot, ID.ops]);
77
88
 
78
89
  const api = require(process.cwd() + '/src/module-api.js');
79
- api.pool.query = (...a) => pool.query(...a);
90
+ api.pool.query = (...a) => db.query(...a); // the library reads through the same transaction
80
91
  const { resolveBoardMembers, isBoardMember } = require(process.cwd() + '/modules/government/board-membership.js');
81
92
 
82
93
  const board = (membership) => ({ membership, pass_rule: 'consent', window_minutes: null, close_early_on_full_turnout: true });
@@ -120,13 +131,13 @@ await t('the deactivated and the machine are STILL excluded under the widened fo
120
131
  });
121
132
 
122
133
  await t('no duplicate rows when a builder matches through two keys', async () => {
123
- await pool.query('INSERT INTO government_builder_ranks (builder_id, rank_key) VALUES ($1, $2)', [ID.archon, 'gbmdb-ops-rank']);
134
+ await db.query('INSERT INTO government_builder_ranks (builder_id, rank_key) VALUES ($1, $2)', [ID.archon, 'gbmdb-ops-rank']);
124
135
  try {
125
136
  const l = await logins('rank:archon,gbmdb-ops-rank');
126
137
  assert.deepEqual(l.filter((x) => x === 'a').length, 1,
127
138
  'DISTINCT holds — a double-counted member silently raises the consent bar');
128
139
  } finally {
129
- await pool.query("DELETE FROM government_builder_ranks WHERE builder_id=$1 AND rank_key='gbmdb-ops-rank'", [ID.archon]);
140
+ await db.query("DELETE FROM government_builder_ranks WHERE builder_id=$1 AND rank_key='gbmdb-ops-rank'", [ID.archon]);
130
141
  }
131
142
  });
132
143
 
@@ -145,6 +156,8 @@ await t('an unparseable predicate still seats nobody against a real db', async (
145
156
  });
146
157
 
147
158
  await cleanup();
159
+ await db.query('ROLLBACK');
160
+ db.release();
148
161
  console.log(`\n${passed} passed, ${failed} failed`);
149
162
  await pool.end();
150
163
  process.exit(failed ? 1 : 0);
@@ -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']]);