@bongos/core 1.19.972 → 1.19.973

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 (59) hide show
  1. package/.bongos-core.json +74 -64
  2. package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
  3. package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
  4. package/docs/adr/0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md +1 -1
  5. package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +107 -0
  6. package/docs/adr/README.md +1 -0
  7. package/docs/architecture.md +6 -6
  8. package/docs/branding-contract.md +1 -1
  9. package/docs/canonical-permissions.md +24 -24
  10. package/docs/copy-inventory.md +45 -38
  11. package/docs/copy-registry.json +130 -67
  12. package/docs/design/projects-hub-direction-v2.md +1 -1
  13. package/docs/design/vanilla-hall-ui-redesign-scope.md +2 -2
  14. package/docs/file-map.md +7 -7
  15. package/docs/handoff-template.md +1 -1
  16. package/docs/module-api-changelog.md +2 -0
  17. package/docs/modules-contract.md +1 -1
  18. package/docs/onboarding/drachmae-and-karma.md +2 -2
  19. package/docs/packs/engineer.md +5 -5
  20. package/docs/packs/ideator.md +1 -1
  21. package/docs/recipes/README.md +1 -1
  22. package/docs/recipes/audit-replay.md +2 -2
  23. package/docs/recipes/gds-db-backup.md +3 -3
  24. package/docs/recipes/managed-settings-remote-control.md +9 -9
  25. package/docs/recipes/memory-sync-conflicts.md +2 -2
  26. package/docs/recipes/ops-gotchas.md +24 -5
  27. package/docs/recipes/overnight-builder.md +3 -3
  28. package/docs/recipes/parallel-goal-waves.md +3 -3
  29. package/docs/recipes/reranking-a-goals-board.md +2 -2
  30. package/docs/recipes/search-index-ingestion.md +1 -1
  31. package/docs/recipes/ship-and-regrade.md +2 -2
  32. package/docs/recipes/staging-environment.md +1 -1
  33. package/docs/recipes/windows-builders.md +1 -1
  34. package/modules/hall-ui/public/deploy.html +10 -5
  35. package/modules/hall-ui/public/deploy.js +199 -110
  36. package/modules/hall-ui/public/project-settings.css +36 -0
  37. package/modules/provisioning/core-upgrade.js +61 -2
  38. package/modules/public-landing/public/contact.html +1 -1
  39. package/modules/public-landing/public/index.html +1 -1
  40. package/modules/public-landing/public/privacy.html +1 -1
  41. package/modules/public-landing/public/projects.html +1 -1
  42. package/modules/public-landing/public/terms.html +1 -1
  43. package/package-lock.json +2 -2
  44. package/package.json +1 -1
  45. package/scripts/gds/api-alias-caller-scan.js +47 -9
  46. package/scripts/gds/cloudbongos-token-isolation.conf +33 -0
  47. package/scripts/gds/provision-core-upgrade.js +132 -1
  48. package/scripts/gds/run-unit-tests.js +14 -9
  49. package/scripts/hall-preview/server.js +5 -0
  50. package/src/module-api.js +1 -1
  51. package/tests/api_alias_caller_ratchet.mjs +68 -2
  52. package/tests/core_upgrade_door.mjs +81 -0
  53. package/tests/core_upgrade_runner.mjs +132 -0
  54. package/tests/deploy_page_projects.mjs +234 -79
  55. package/tests/idea_spark_hall.mjs +101 -12
  56. package/tests/known_failing_ratchet.mjs +19 -9
  57. package/tests/landing_legal_pages.mjs +5 -1
  58. package/tests/projects_hub_pre_uat.mjs +12 -6
  59. package/tests/served_paths_shipped.mjs +36 -2
@@ -81,6 +81,6 @@
81
81
  </section>
82
82
  </main>
83
83
 
84
- <footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud&nbsp;Bongos. <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">Open source (AGPL-3.0)</a>.<nav class="footNav" aria-label="Site"><a href="/privacy">Privacy</a><a href="/terms" aria-current="page">Terms</a><a href="/contact">Contact</a></nav></footer>
84
+ <footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud&nbsp;Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).<nav class="footNav" aria-label="Site"><a href="/privacy">Privacy</a><a href="/terms" aria-current="page">Terms</a><a href="/contact">Contact</a></nav></footer>
85
85
  </body>
86
86
  </html>
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.972",
3
+ "version": "1.19.973",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.972",
9
+ "version": "1.19.973",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.972",
3
+ "version": "1.19.973",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -41,11 +41,22 @@
41
41
  // gate that cannot state its own rule cannot explain itself.
42
42
  // 5. tests/ (path-excluded). Tests are what keeps the alias exercised in CI. Moving them off it
43
43
  // would retire the alias by accident.
44
- // 6. docs/ (path-excluded) — a DELIBERATE SCOPE CALL, not an oversight. It is the largest
45
- // remaining bucket (~320) and it is where rename-history-check's per-file floors live, so a
46
- // docs pass has to separate the record of what we used to call it from instructions that
47
- // still teach the old spelling. 1004064's own brief says to treat docs as out of scope and
48
- // file it rather than widen the task; filed as the follow-up named in that task's session log.
44
+ // 6. THE FROZEN RECORD UNDER docs/ (path-excluded) — narrowed from a blanket `docs/` by task
45
+ // 1004113, which is the scope call 1004064 deferred. docs/ is not one kind of thing: part of
46
+ // it is the RECORD of what we used to call the thing, and part of it is live instruction that
47
+ // still TEACHES a reader to call the alias. A blanket exclusion left the second unpoliced
48
+ // forever; a blind sweep would have destroyed the first.
49
+ // The line is not invented here — it is the one the repo already draws twice over. The
50
+ // subtrees excluded (docs/adr, docs/session-logs, docs/audits) are exactly the frozen-record
51
+ // path list in the sibling detector gds-literal-scan.js, and — measured, not assumed — all 134 per-file
52
+ // floors in config/rename-history-baseline.json fall inside them (adr 116, session-logs 15,
53
+ // audits 3) and NONE falls outside. Rewriting a file with a floor fails the build on purpose:
54
+ // an ADR rewritten to today's vocabulary destroys the evidence that the words changed.
55
+ // docs/governance joins them on the same reading: every file in it is a dated authority sweep
56
+ // or a payment PROOF — an audit filed under another name. docs/recipes/failed-ship-recovery-proof.md
57
+ // is the one file named individually, for the same reason: it is a transcript of a recovery
58
+ // that happened, and rewriting the calls it records would make it misreport them.
59
+ // Everything else under docs/ is now COUNTED, and 1004113 swept it.
49
60
  // 7. MIGRATIONS + THE FROZEN RECORD (path-excluded). Same rule as the sibling detector: an
50
61
  // applied migration renamed is an applied migration re-run, and a rewritten ADR destroys the
51
62
  // evidence that the words changed.
@@ -55,6 +66,14 @@
55
66
  // so the ratchet and the sweep agree by construction.
56
67
  // 9. CAPTURED FIXTURES (path-excluded). scripts/hall-preview/fixtures/*.json are snapshots of
57
68
  // real API responses; rewriting one would make the fixture misreport what the server returned.
69
+ // 10. A LINE NAMING AN AUTH URL (line-excluded, task 1004113). `/api/gds/auth/...` in prose is the
70
+ // SAME pinned callback reason 2 path-excludes in code, reached from the docs side: the
71
+ // Authorization callback URL registered in GitHub's app settings is `${publicOrigin}/api/gds/auth/web/callback`,
72
+ // and a recipe that teaches a builder to register anything else breaks sign-in for the
73
+ // instance they are standing up. Seven occurrences across four recipes turn on this, and they
74
+ // must not be swept to make a number fall. This is a LINE rule rather than a path rule
75
+ // because the files carrying them (ops-gotchas.md, local-dev.md, staging-environment.md)
76
+ // are otherwise ordinary live instruction that SHOULD be counted.
58
77
  //
59
78
  // Usage:
60
79
  // node scripts/gds/api-alias-caller-scan.js # count + the top offending files
@@ -99,8 +118,15 @@ const ALIAS_EXCLUDED_PATHS = [
99
118
  // files are not tests (clients/bongos-client/index.mjs and its example, scripts/status-probe.mjs)
100
119
  // and the client is exactly the kind of caller this ratchet exists to police.
101
120
  /(^|\/)tests?\//,
102
- // 6. docs are a separate pass (scope call, see the header)
103
- /(^|\/)docs\//,
121
+ // 6. the FROZEN RECORD under docs/ — narrowed from a blanket `docs/` by task 1004113. These
122
+ // three subtrees mirror the frozen-record path list in gds-literal-scan.js and are where all 134
123
+ // rename-history floors live; docs/governance is dated audits + payment proofs under another
124
+ // name, and the one named file is a transcript of a recovery that happened.
125
+ /(^|\/)docs\/adr\//,
126
+ /(^|\/)docs\/session-logs\//,
127
+ /(^|\/)docs\/audits\//,
128
+ /(^|\/)docs\/governance\//,
129
+ /^docs\/recipes\/failed-ship-recovery-proof\.md$/,
104
130
  // 7. migrations + the frozen record
105
131
  /(^|\/)migrations\//,
106
132
  /(^|\/)limitations\//,
@@ -114,6 +140,15 @@ const ALIAS_EXCLUDED_TREES = /(^|\/)(node_modules|public-docs)\/|\.min\.js$|(^|\
114
140
  // A line that is ABOUT the alias rather than a caller of it. Rule 8 above.
115
141
  const ALIAS_COMMENTARY_RE = /\balias\b|\blegacy\b|\bpermanent\b|\bforever\b/i;
116
142
 
143
+ // A line naming an AUTH url under the alias. Rule 10 above: this is the pinned OAuth callback,
144
+ // registered outside the repo, so it is a fact about GitHub's app settings rather than a caller
145
+ // anyone may move. Kept as a line rule because the files it appears in are otherwise countable.
146
+ // Two shapes: the URL itself, and prose ABOUT it — a heading such as "Authorized callback URL must
147
+ // include the full /api/gds path" names the prefix without the /auth/ segment, and sweeping it left
148
+ // the heading contradicting the body two lines below it. Caught in review on task 1004113; the rule
149
+ // is widened rather than the heading rewritten, because the heading is right.
150
+ const ALIAS_PINNED_AUTH_RE = /\/api\/gds\/auth\/|\bcallback\b/i;
151
+
117
152
  function isExcludedPath(relPath) {
118
153
  const p = relPath.replace(/\\/g, '/');
119
154
  if (ALIAS_EXCLUDED_TREES.test(p)) return true;
@@ -127,6 +162,7 @@ function isCountedLine(line) {
127
162
  if (!line.includes(ALIAS)) return false;
128
163
  if (line.includes(CANONICAL)) return false;
129
164
  if (ALIAS_COMMENTARY_RE.test(line)) return false;
165
+ if (ALIAS_PINNED_AUTH_RE.test(line)) return false;
130
166
  return true;
131
167
  }
132
168
 
@@ -193,8 +229,10 @@ function main() {
193
229
  const show = process.argv.includes('--files') ? ranked : ranked.slice(0, 20);
194
230
  console.log(`in-repo /api/gds callers: ${total} across ${ranked.length} file(s) (${scanned} file(s) scanned)`);
195
231
  console.log('(the alias contract, the pinned OAuth callbacks, the uptime poller + smoke, the rename guardrails,');
196
- console.log(' tests/, docs/, migrations/ and captured fixtures are excluded by construction; so is any line that');
197
- console.log(' also names /api/bongos or carries the words alias / legacy / permanent / forever)');
232
+ console.log(' tests/, the FROZEN RECORD under docs/ (adr, session-logs, audits, governance), migrations/ and');
233
+ console.log(' captured fixtures are excluded by construction; so is any line that also names /api/bongos, carries');
234
+ console.log(' the words alias / legacy / permanent / forever, or names a pinned /api/gds/auth/ callback URL.');
235
+ console.log(' The REST of docs/ is counted — live instruction that still teaches the alias should shrink.)');
198
236
  console.log('');
199
237
  for (const [f, n] of show) console.log(` ${String(n).padStart(5)} ${f}`);
200
238
  if (show.length < ranked.length) console.log(` … and ${ranked.length - show.length} more file(s) — pass --files for all`);
@@ -0,0 +1,33 @@
1
+ # /etc/systemd/system/cloudbongos.service.d/task-1004176-token-isolation.conf
2
+ #
3
+ # task 1004176 / ADR 0327 §5 — the web tier must not be able to read the
4
+ # control-plane cloud tokens. Full rationale, the verification commands and the
5
+ # trap that makes a working config look broken: docs/recipes/ops-gotchas.md,
6
+ # "systemd". Kept there rather than repeated here so one edit cannot leave two
7
+ # copies disagreeing.
8
+ #
9
+ # A drop-in, not a unit edit: cloudbongos.service was hand-written at standup and
10
+ # is not generated from this repo, so an edit to it has no home here. A drop-in
11
+ # is additive, survives a main-unit rewrite, and rolls back by deleting one file.
12
+ #
13
+ # WHAT IS DELIBERATELY ABSENT. The task originally asked for the "standard
14
+ # sandboxing directives" and for the two tiers to run as different unix users.
15
+ # Neither is here, and ADR 0327 §5 records why:
16
+ # ProtectHome= would break the web tier outright — its checkout is
17
+ # /home/lars/cloudbongos-instance, and the secret-box
18
+ # master key it legitimately needs for per-builder AI keys
19
+ # is ~/.config/cloudbongos/builder-secret.key.
20
+ # ProtectSystem=strict needs a ReadWritePaths allowlist that a live site should
21
+ # not be handed blind. Worth doing deliberately, later.
22
+ # PrivateTmp= unevaluated: nothing established whether the web tier and
23
+ # the runners share anything through /tmp. Not guessed at.
24
+ # different users would mean relocating the checkout, the config dir,
25
+ # web.env and that master key. The namespace gives the same
26
+ # protection for this file at a fraction of the risk.
27
+ # NoNewPrivileges is present: no web-tier path shells out with sudo or setuid
28
+ # (checked in src/ and modules/), and 30 days of the unit's own logs contain no
29
+ # escalation or EPERM.
30
+ #
31
+ [Service]
32
+ InaccessiblePaths=/etc/cloudbongos/box.env
33
+ NoNewPrivileges=yes
@@ -41,6 +41,12 @@ const { isValidSlug } = require('../../modules/provisioning/provisioning.js');
41
41
  // changes between the request and the drain.
42
42
  const UPGRADE_SHAPES = Object.freeze(new Set(['co-tenant', 'standalone', 'control-plane']));
43
43
 
44
+ // The strict three-number form a core version takes. Re-stated here rather than imported
45
+ // from the web tier's core-upgrade.js: a module may not be required from scripts/ (ADR
46
+ // 0083) and this side needs it to guard a SHELL interpolation, which is a stricter duty
47
+ // than validating a request body.
48
+ const CORE_VERSION_RE = /^[0-9]+\.[0-9]+\.[0-9]+$/;
49
+
44
50
  // The box's own answer to "what core am I running?" — loopback, on the instance's port.
45
51
  //
46
52
  // THE APEX CANNOT BE ASKED (ADR 0293 D6). It sits behind pre-launch basic_auth and its
@@ -432,6 +438,115 @@ function preflightUpgrade(inst, to, deps) {
432
438
  };
433
439
  }
434
440
 
441
+ // ── WHAT THE MOVE WOULD CARRY (task 1004173) ────────────────────────────────
442
+ //
443
+ // The owner, 2026-09-22: "I don't actually see what would change when I hit the button."
444
+ // The pre-flight above answers a different question — whether the machinery will let the
445
+ // move happen — and answering it was being presented as a preview. ADR 0293 D4 promised
446
+ // both halves in so many words: "the control shows what a bump would move — live version
447
+ // → target, AND WHAT SITS BETWEEN THEM." Only the first half was built, so the owner was
448
+ // asked to restart a live site to take ninety-one releases of work the page named none of.
449
+ //
450
+ // WHERE THE ANSWER ALREADY LIVES. Every ship writes a merge commit whose summary is
451
+ // already prose for exactly this reader — the owner, not a developer — because ship.js
452
+ // requires one and posts it to Discord. So this reads the merge summaries between the two
453
+ // releases rather than inventing a changelog nobody would maintain.
454
+ //
455
+ // IT READS THE CONTROL PLANE'S OWN CORE CHECKOUT, ALWAYS, whichever instance is being
456
+ // previewed. "What changed in the core between 1.19.881 and 1.19.972" is a question about
457
+ // the CORE repository, and a tenant's instance directory is a checkout of the TENANT's
458
+ // repo with the core vendored in from npm — no core history in it at all, and the wrong
459
+ // history if you squint. The control plane is the machine that builds the core, so it is
460
+ // the machine that can answer.
461
+ const CHANGE_CAP = 20;
462
+ const CHANGE_TEXT_MAX = 400;
463
+
464
+ /**
465
+ * The merge summaries in a `git log --merges --format=%s%x1f%b%x1e` dump. PURE.
466
+ *
467
+ * TWO MERGE SHAPES CARRY A SUMMARY AND THE SECOND IS THE COMMON ONE. A hand-landed ship
468
+ * writes `Merge <branch>: <summary>`; the CI auto-merge writes `Merge pull request #N
469
+ * from <owner>/<branch>` with `task <id>: <summary>` in the BODY. Measured over
470
+ * 1.19.881→1.19.972: 114 usable summaries, of which 98 were the second shape. A parser
471
+ * that read subjects only would have found eleven of them and quietly called that the
472
+ * release.
473
+ *
474
+ * EVERYTHING ELSE IS DROPPED, and the drops are the majority: `Merge origin/main into
475
+ * <branch> (pre-stage freshness…)`, `Merge remote-tracking branch…`, a regen chore. Those
476
+ * are the mechanics of landing work, not the work, and listing them would bury the
477
+ * eleven sentences the owner actually wants under a hundred they do not.
478
+ */
479
+ function parseChangeRecords(stdout) {
480
+ const out = [];
481
+ const seen = new Set();
482
+ for (const rec of String(stdout || '').split('\x1e')) {
483
+ const r = rec.trim();
484
+ if (!r) continue;
485
+ const i = r.indexOf('\x1f');
486
+ const subject = (i < 0 ? r : r.slice(0, i)).trim();
487
+ const body = i < 0 ? '' : r.slice(i + 1);
488
+ let text = null;
489
+ let id = null;
490
+ // The negative lookahead is what keeps the two housekeeping shapes out: both begin
491
+ // "Merge " and both would otherwise match a `<token>:` somewhere later in the line.
492
+ let m = /^Merge\s+(?!pull request\b|remote-tracking\b)([^\s:]+):\s*(\S.*)$/.exec(subject);
493
+ if (m) {
494
+ text = m[2];
495
+ } else {
496
+ const first = body.split('\n').map((l) => l.trim()).find(Boolean) || '';
497
+ m = /^task\s+(\d+):\s*(\S.*)$/.exec(first);
498
+ if (m) { id = m[1]; text = m[2]; }
499
+ }
500
+ if (!text) continue;
501
+ // It reaches a DOM, and a ship summary is occasionally a paragraph.
502
+ text = text.length > CHANGE_TEXT_MAX ? `${text.slice(0, CHANGE_TEXT_MAX - 1)}…` : text;
503
+ if (seen.has(text)) continue; // a branch merged twice says its sentence once
504
+ seen.add(text);
505
+ out.push(id ? { id, text } : { text });
506
+ }
507
+ return out;
508
+ }
509
+
510
+ /**
511
+ * What landed between two core releases, as the summaries their ships wrote.
512
+ *
513
+ * Returns `{ from, to, entries, more }`, or `{ from, to, error }` — NEVER an empty
514
+ * `entries` for a range it could not resolve. An empty list reads as "nothing changed",
515
+ * which on a page whose job is to say what a restart buys is the one wrong impression it
516
+ * must not give, and it is exactly what a missing checkout or a shallow clone produces.
517
+ *
518
+ * Every version reaching a shell is re-checked against the strict three-number form
519
+ * FIRST. They arrive from an intent row whose column has the same CHECK, but a value that
520
+ * is interpolated into a command is checked where it is interpolated — the rule
521
+ * upgradeInvocation states about the slug.
522
+ */
523
+ function changesBetween(fromVersion, toVersion, deps) {
524
+ const bad = [fromVersion, toVersion].find((v) => !CORE_VERSION_RE.test(String(v || '')));
525
+ if (bad !== undefined) return { from: fromVersion || null, to: toVersion || null, error: 'could not work out which releases this covers' };
526
+ // The CONTROL PLANE's executor and the CONTROL PLANE's checkout, never the previewed
527
+ // instance's: under PROVISION_REMOTE_HOST `exec` is the remote box, and a tenant's own
528
+ // directory holds no core history at all.
529
+ const exec = deps.controlExec || deps.exec;
530
+ const selfRoot = deps.selfInstanceRoot || selfInstanceRoot;
531
+ const dir = selfRoot();
532
+ const shaOf = (v) => {
533
+ const r = exec(`git -C ${dir} log --format=%H --max-count=1 -F --grep='release: core ${v} '`, { allowFail: true });
534
+ const sha = String((r && r.stdout) || '').trim().split('\n')[0] || '';
535
+ return /^[0-9a-f]{7,40}$/.test(sha) ? sha : null;
536
+ };
537
+ const from = shaOf(fromVersion);
538
+ const to = shaOf(toVersion);
539
+ if (!from || !to) {
540
+ return { from: fromVersion, to: toVersion, error: 'could not find those releases in the core history on this machine' };
541
+ }
542
+ const r = exec(`git -C ${dir} log --merges --format=%s%x1f%b%x1e ${from}..${to}`, { allowFail: true });
543
+ if (!r || r.softFailed === true || r.ok === false) {
544
+ return { from: fromVersion, to: toVersion, error: 'could not read the core history on this machine' };
545
+ }
546
+ const all = parseChangeRecords(r.stdout);
547
+ return { from: fromVersion, to: toVersion, entries: all.slice(0, CHANGE_CAP), more: Math.max(0, all.length - CHANGE_CAP) };
548
+ }
549
+
435
550
  // Run a dry run for one instance and cache its verdict — the ON-DEMAND leg, drained from
436
551
  // a `core-upgrade-preflight` intent the owner's arming queues (task 1004121). The tick
437
552
  // does not do this: a verdict that is only ever produced on a background schedule is
@@ -446,6 +561,22 @@ async function preflightCoreUpgrade(inst, deps, intent) {
446
561
  }
447
562
  if (!to) return { ok: false, error: 'no target version to preview' };
448
563
  const verdict = (deps.preflightUpgrade || preflightUpgrade)(inst, to, deps);
564
+ // AND WHAT THE MOVE WOULD CARRY, taken in the same breath (task 1004173). It rides the
565
+ // verdict rather than a column of its own for the reason the verdict's own `target`
566
+ // exists: both answers are about ONE move, and storing them together is what makes
567
+ // "is this still the move on offer" a single question with a single answer. It also
568
+ // costs the owner no second wait — the dry run is already the thing they are waiting on.
569
+ //
570
+ // `from` is the version the box is on RIGHT NOW, read back from the snapshot the tick
571
+ // writes, and it is stored because the answer is only true of that starting point. The
572
+ // web tier withholds the list when the box has moved since, the same way it withholds a
573
+ // verdict taken against a superseded target.
574
+ let servedNow = null;
575
+ try {
576
+ const { rows } = await db.query('SELECT served_version FROM provisioning_core_version_snapshot WHERE instance_id = $1', [inst.id]);
577
+ servedNow = (rows[0] && rows[0].served_version) || null;
578
+ } catch { /* no snapshot yet — changesBetween refuses on the missing version, and says so */ }
579
+ verdict.changes = (deps.changesBetween || changesBetween)(servedNow, to, deps);
449
580
  await db.query(
450
581
  `INSERT INTO provisioning_core_version_snapshot (instance_id, preflight, preflight_at, observed_at)
451
582
  VALUES ($1, $2::jsonb, now(), now())
@@ -537,4 +668,4 @@ async function snapshotAllCoreVersions(deps) {
537
668
  return { seen };
538
669
  }
539
670
 
540
- module.exports = { UPGRADE_SHAPES, resolveRunAs, versionCmd, parseServedVersion, parseSchemaPending, targetFault, parsePreflight, upgradeInvocation, preflightUpgrade, preflightCoreUpgrade, coreUpgradeInstance, snapshotCoreVersions, snapshotAllCoreVersions };
671
+ module.exports = { UPGRADE_SHAPES, CHANGE_CAP, parseChangeRecords, changesBetween, resolveRunAs, versionCmd, parseServedVersion, parseSchemaPending, targetFault, parsePreflight, upgradeInvocation, preflightUpgrade, preflightCoreUpgrade, coreUpgradeInstance, snapshotCoreVersions, snapshotAllCoreVersions };
@@ -209,15 +209,20 @@ const INTEGRATION = new Set([
209
209
  // (V4.R16) diagnosed them as STALE — they predated #481's Narc-advisory change,
210
210
  // NOT a grader regression. Now green and back under this gate so the quality-
211
211
  // gate brain can't silently regress.
212
- const KNOWN_FAILING = new Set([
213
- // #1002617: red in every clean environment (Linux CI and a Windows checkout
214
- // alike), surfaced by the unit lane's first-ever runs. Parked so the `unit`
215
- // check can be required (a gate that's already red gates nothing); per-test
216
- // diagnoses live in that task. Remove each here as it is fixed.
217
- // PAIRING: tests/known_failing_ratchet.mjs keeps EXECUTING each parked test
218
- // and fails the suite the moment one starts passing — so this set cannot rot.
219
- 'served_paths_shipped',
220
- ]);
212
+ // EMPTY, and that is the intended steady state (task 1002617 closed the last entry).
213
+ //
214
+ // This set existed because the unit lane's first-ever runs (task 1002610) surfaced three
215
+ // tests red in every clean environment, and a gate that is already red gates nothing —
216
+ // so they were parked to make `unit` requireable while each was diagnosed. platform_boot
217
+ // and publish_manifest were fixed earlier; served_paths_shipped was the survivor, and its
218
+ // failure turned out to be its own self-guard correctly reporting that the source scan
219
+ // had gone blind when the OpenAPI serve moved to a root ladder (task 1002200).
220
+ //
221
+ // Adding an entry here is a real cost and should be rare: a parked test is invisible
222
+ // coverage, and this one sat parked long enough that its diagnosis had to be redone.
223
+ // PAIRING: tests/known_failing_ratchet.mjs keeps EXECUTING each parked test and fails the
224
+ // suite the moment one starts passing — so this set cannot rot silently.
225
+ const KNOWN_FAILING = new Set([]);
221
226
 
222
227
  // Task 1003845: on Windows + Node 24 a suite that hard-exits after serving HTTP can
223
228
  // print a clean verdict and THEN abort natively (the cliExit class in
@@ -123,6 +123,11 @@ const FIXTURE_FALLBACKS = [
123
123
  // the profile's connections section (task 1002296) — one degree fixture serves
124
124
  // any handle, since the handle is the address and the label is the payload.
125
125
  [/^connections\/degree\/[^/]+$/, 'connections__degree'],
126
+ // the deploy door's per-project preview (task 1004171). One fixture serves any id
127
+ // because the page renders ONE project and the id is the address, not the payload —
128
+ // without it /deploy?project=… only ever rendered its "could not read" state, which
129
+ // is the trap this directory's README calls out by name.
130
+ [/^provisioning\/instances\/\d+\/core-upgrade$/, 'provisioning__instances__core-upgrade'],
126
131
  ];
127
132
 
128
133
  // A DATE THAT ROTS CANNOT DEMO AN AGE SIGNAL (task 1004041). The help-request
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.972'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.973'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -39,7 +39,7 @@ test('a collapsed enumeration is visible as scanned=0, not as a flattering total
39
39
 
40
40
  const allExcluded = scan.scanAliasCallers(fake({
41
41
  'tests/x.mjs': '/api/gds\n',
42
- 'docs/x.md': '/api/gds\n',
42
+ 'docs/adr/0001-x.md': '/api/gds\n', // frozen record — NOT `docs/x.md`, which counts since task 1004113
43
43
  }));
44
44
  assert.equal(allExcluded.total, 0);
45
45
  assert.equal(allExcluded.scanned, 0, 'an every-path-excluded run is also scanned=0 — the same signal');
@@ -67,7 +67,11 @@ test('the exclusions are real and each one is reachable', () => {
67
67
  'scripts/gds/api-alias-caller-scan.js',
68
68
  'tests/api_alias_exceptions.mjs', // 5 tests
69
69
  'modules/dev-box/app/test/thing.mjs',
70
- 'docs/architecture.md', // 6 docs
70
+ 'docs/adr/0016-permissions.md', // 6 the FROZEN RECORD under docs/ (narrowed, task 1004113)
71
+ 'docs/session-logs/2026-09-16-x.md',
72
+ 'docs/audits/2026-08-01-x.md',
73
+ 'docs/governance/authority-sweep-2026-07-24.md',
74
+ 'docs/recipes/failed-ship-recovery-proof.md',
71
75
  'migrations/033_audit_log.sql', // 7 migrations
72
76
  'scripts/hall-preview/fixtures/tasks.json', // 9 captured fixtures
73
77
  'node_modules/pkg/index.js', // vendored
@@ -81,11 +85,58 @@ test('the exclusions are real and each one is reachable', () => {
81
85
  'clients/bongos-client/index.mjs', // a .mjs that is NOT a test — a blanket \.mjs$ would blind the ratchet
82
86
  'bin/bongos.js',
83
87
  'CLAUDE.md',
88
+ // task 1004113: live instruction under docs/ is counted. If this list ever goes back to being
89
+ // excluded, the ~95 occurrences the sweep moved can silently grow back unpoliced.
90
+ 'docs/architecture.md',
91
+ 'docs/canonical-permissions.md',
92
+ 'docs/recipes/parallel-goal-waves.md',
93
+ 'docs/packs/engineer.md',
94
+ 'docs/onboarding/drachmae-and-karma.md',
84
95
  ]) {
85
96
  assert.equal(scan.isExcludedPath(p), false, `${p} must NOT be excluded`);
86
97
  }
87
98
  });
88
99
 
100
+ // ---------------------------------------------------------------------------
101
+ // task 1004113 — the docs/ split. This is the case the parent task (1004064)
102
+ // deferred, and it is proved in BOTH directions on purpose: a narrowing that
103
+ // only asserts "the frozen record is excluded" would pass identically if the
104
+ // blanket `docs/` exclusion were still in place, which is exactly the vacuous
105
+ // shape this file's header warns about.
106
+ // ---------------------------------------------------------------------------
107
+
108
+ test('docs/ is SPLIT: the frozen record stays out, live instruction is now counted', () => {
109
+ const r = scan.scanAliasCallers(fake({
110
+ 'docs/adr/0010-pms-architecture.md': 'GET /api/gds/tasks\n',
111
+ 'docs/session-logs/2026-09-16-x.md': 'GET /api/gds/tasks\n',
112
+ 'docs/audits/2026-08-01-x.md': 'GET /api/gds/tasks\n',
113
+ 'docs/governance/rank-map-2026-08-06.md': 'GET /api/gds/tasks\n',
114
+ 'docs/recipes/failed-ship-recovery-proof.md': 'POST /api/gds/claims/1/resolve\n',
115
+ 'docs/recipes/how-to-do-a-thing.md': 'node scripts/gds/api.js GET /api/gds/tasks/1\n',
116
+ 'docs/packs/engineer.md': 'create a task via POST /api/gds/tasks\n',
117
+ }));
118
+ assert.deepEqual(
119
+ Object.keys(r.byFile).sort(),
120
+ ['docs/packs/engineer.md', 'docs/recipes/how-to-do-a-thing.md'],
121
+ 'only the instructional docs count — and they MUST count, or the narrowing did nothing',
122
+ );
123
+ assert.equal(r.total, 2);
124
+ assert.equal(r.scanned, 2, 'the frozen-record files are excluded before they are read, not counted as zero');
125
+ });
126
+
127
+ test('the rename-history floors all sit INSIDE the excluded subtrees — the line is measured, not asserted', () => {
128
+ // The narrowing's load-bearing evidence: rename-history-check.js fails the build when the old
129
+ // name is ERASED from a file with a floor, so a docs sweep could have failed a DIFFERENT gate.
130
+ // It did not, because every floor is in a subtree this scanner still excludes. If a floor is ever
131
+ // added outside them, this reds and whoever added it has to decide which gate wins.
132
+ const floors = Object.keys(
133
+ JSON.parse(readFileSync(path.join(ROOT, 'config', 'rename-history-baseline.json'), 'utf8')).frozenLiteralFloors || {},
134
+ );
135
+ assert.ok(floors.length > 100, `the baseline must actually carry floors (got ${floors.length})`);
136
+ const outside = floors.filter((f) => !scan.isExcludedPath(f));
137
+ assert.deepEqual(outside, [], 'a file with a rename-history floor must never be inside the caller ratchet');
138
+ });
139
+
89
140
  test('a line ABOUT the alias is not a caller of it — and the rule is not so loose it excuses everything', () => {
90
141
  // Excluded: names the canonical prefix too, or says what it is.
91
142
  assert.equal(scan.isCountedLine('mount /api/bongos and /api/gds'), false);
@@ -100,6 +151,21 @@ test('a line ABOUT the alias is not a caller of it — and the rule is not so lo
100
151
  assert.equal(scan.isCountedLine('a line about a legacy alias, forever'), false);
101
152
  });
102
153
 
154
+ test('a PINNED auth URL is not a caller anyone may move (rule 10, task 1004113)', () => {
155
+ // These are registered in GitHub's OAuth app settings. A recipe that teaches a builder to
156
+ // register /api/bongos/auth/web/callback breaks sign-in on the instance they are standing up,
157
+ // so the ratchet must never create pressure to rewrite them.
158
+ assert.equal(scan.isCountedLine(' https://example.com/api/gds/auth/web/callback'), false);
159
+ assert.equal(scan.isCountedLine('the server sends `${publicOrigin}/api/gds/auth/web/callback`'), false);
160
+ assert.equal(scan.isCountedLine('`/api/gds/auth/web/start` = no session'), false);
161
+ // …and prose ABOUT that URL, which names the prefix without the /auth/ segment. Sweeping this
162
+ // heading left it contradicting its own body two lines down; caught in review on 1004113.
163
+ assert.equal(scan.isCountedLine('### Authorized callback URL must include the full /api/gds path'), false);
164
+ // The rule is NOT so wide that an ordinary caller in an auth-adjacent file escapes it.
165
+ assert.equal(scan.isCountedLine('node scripts/gds/api.js GET /api/gds/me'), true);
166
+ assert.equal(scan.isCountedLine('POST /api/gds/auth-free/thing'), true, 'only the /auth/ segment, not any word starting auth');
167
+ });
168
+
103
169
  test('occurrences are counted, not lines — moving ONE of two callers on a line lowers the number', () => {
104
170
  assert.equal(scan.countInText('f("/api/gds/a"); f("/api/gds/b");\n'), 2);
105
171
  assert.equal(scan.countInText('f("/api/bongos/a"); f("/api/gds/b");\n'), 0, 'the canonical name on the line is the alias-commentary rule, by design');
@@ -756,5 +756,86 @@ await t('lastCoreMove reads MOVES only — a preview refusal is not a failed dep
756
756
  assert.deepEqual(seen[0].params, [41]);
757
757
  });
758
758
 
759
+
760
+ console.log('\nthe change list is withheld when the box has moved since (task 1004173):');
761
+
762
+ const PF = (over = {}) => ({ target: '1.19.949', ok: true, lines: [], error: null,
763
+ changes: { from: '1.19.900', to: '1.19.949', entries: [{ text: 'a thing landed' }], more: 0 }, ...over });
764
+
765
+ await t('a verdict taken from where the box IS keeps its change list', () => {
766
+ const p = CU.previewFromSnapshot(ACTIVE, { ...SNAP, preflight: PF() }, null, null);
767
+ assert.equal(p.preflight.changes.entries.length, 1);
768
+ });
769
+
770
+ await t('THE LIST GOES WHEN THE BOX HAS MOVED — the verdict beside it does NOT', () => {
771
+ // Two independent staleness questions. The verdict is about a TARGET; the list is
772
+ // about a RANGE, so it also depends on where the box starts. Dropping both would throw
773
+ // away a good verdict; dropping neither would show a range starting somewhere the box
774
+ // no longer is.
775
+ const p = CU.previewFromSnapshot(ACTIVE, { ...SNAP, served_version: '1.19.930', preflight: PF() }, null, null);
776
+ assert.equal(p.preflight.changes, null, 'the range no longer starts where the box is');
777
+ assert.equal(p.preflight.ok, true, 'but the check is still a true answer about this target');
778
+ assert.equal(p.preflight.target, '1.19.949');
779
+ });
780
+
781
+ await t('a verdict about a SUPERSEDED target is withheld whole, list and all', () => {
782
+ const p = CU.previewFromSnapshot(ACTIVE, { ...SNAP, preflight: PF({ target: '1.19.920' }) }, null, null);
783
+ assert.equal(p.preflight, null, 'a stale pass reads as a guarantee — worse than no answer');
784
+ });
785
+
786
+ await t('withholding does not MUTATE the cached verdict', () => {
787
+ // previewFromSnapshot runs per request over a row the caller may reuse; blanking the
788
+ // list in place would make the second reader see a range the first one invalidated.
789
+ const snap = { ...SNAP, served_version: '1.19.930', preflight: PF() };
790
+ CU.previewFromSnapshot(ACTIVE, snap, null, null);
791
+ assert.ok(snap.preflight.changes, 'the snapshot is untouched');
792
+ });
793
+
794
+ console.log('\nis the last move still NEWS, or history the box moved past? (task 1004172):');
795
+
796
+ await t('a failure the box has already moved past is marked superseded', () => {
797
+ // The live defect, 2026-09-22: the control plane's row led with "The move to core
798
+ // 1.19.921 did not land", undated, while the box served 1.19.969 and a move to
799
+ // 1.19.972 was on offer. Two days old, a different target, and a cause fixed and
800
+ // released in between — all of it rendered as the present.
801
+ const old = { action: 'core-upgrade', state: 'error', target_version: '1.19.921', resolved_at: new Date().toISOString(), last_error: 'Postgres has no role "root"' };
802
+ const p = CU.previewFromSnapshot(ACTIVE, SNAP, null, old);
803
+ assert.equal(p.last_move.superseded, true, 'a different move is on offer now');
804
+ });
805
+
806
+ await t('a failure against the move STILL on offer is NOT superseded', () => {
807
+ const now = { action: 'core-upgrade', state: 'error', target_version: '1.19.949', resolved_at: new Date().toISOString(), last_error: 'working tree not clean' };
808
+ const p = CU.previewFromSnapshot(ACTIVE, SNAP, null, now);
809
+ assert.equal(p.last_move.superseded, false, 'this is the button in front of the owner');
810
+ });
811
+
812
+ await t('a box that reached the target by another road supersedes the failure', () => {
813
+ // The exact shape of the live case: the move failed, and the version arrived anyway.
814
+ const old = { action: 'core-upgrade', state: 'error', target_version: '1.19.949', resolved_at: new Date().toISOString(), last_error: 'x' };
815
+ const p = CU.previewFromSnapshot(ACTIVE, { ...SNAP, served_version: '1.19.949', recommended: '1.19.949' }, null, old);
816
+ assert.equal(p.last_move.superseded, true);
817
+ });
818
+
819
+ await t('AN UNREADABLE REGISTRY LEAVES A FAILURE STANDING — not knowing is not evidence of recovery', () => {
820
+ const now = { action: 'core-upgrade', state: 'error', target_version: '1.19.949', resolved_at: new Date().toISOString(), last_error: 'x' };
821
+ const p = CU.previewFromSnapshot(ACTIVE, { ...SNAP, recommended: null }, null, now);
822
+ assert.equal(p.last_move.superseded, false, 'the direction to fail in is showing an old failure, never hiding a live one');
823
+ });
824
+
825
+ await t('lastMoveSuperseded computes no version ORDERING — only identity', () => {
826
+ // The page's standing rule is that no version comparison happens outside the runner's
827
+ // channel resolution. This helper answers "is this news" without ever asking which of
828
+ // two versions is newer, and the source is held to that.
829
+ const src = read('modules', 'provisioning', 'core-upgrade.js');
830
+ const body = /function lastMoveSuperseded\([^)]*\) \{([\s\S]*?)\n\}/.exec(src);
831
+ assert.ok(body, 'the helper is there to read');
832
+ assert.doesNotMatch(body[1], /parseVersion|semver|split\(/, 'no version parsing');
833
+ assert.doesNotMatch(body[1], /[<>]=?/, 'and no ordering comparison');
834
+ // And it is genuinely reachable both ways from the same inputs.
835
+ assert.equal(CU.lastMoveSuperseded(null, '1.19.900', '1.19.949'), false, 'no target is not superseded');
836
+ assert.equal(CU.lastMoveSuperseded('1.19.949', '1.19.900', '1.19.949'), false);
837
+ assert.equal(CU.lastMoveSuperseded('1.19.921', '1.19.900', '1.19.949'), true);
838
+ });
839
+
759
840
  console.log(`\ncore_upgrade_door: ${passed} passed, ${failed} failed`);
760
841
  if (failed) process.exit(1);