@bongos/core 1.19.972 → 1.19.974
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.
- package/.bongos-core.json +74 -64
- package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
- package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
- package/docs/adr/0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md +1 -1
- package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +107 -0
- package/docs/adr/README.md +1 -0
- package/docs/architecture.md +6 -6
- package/docs/branding-contract.md +1 -1
- package/docs/canonical-permissions.md +24 -24
- package/docs/copy-inventory.md +45 -38
- package/docs/copy-registry.json +130 -67
- package/docs/design/projects-hub-direction-v2.md +1 -1
- package/docs/design/vanilla-hall-ui-redesign-scope.md +2 -2
- package/docs/file-map.md +7 -7
- package/docs/handoff-template.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +1 -1
- package/docs/onboarding/drachmae-and-karma.md +2 -2
- package/docs/packs/engineer.md +5 -5
- package/docs/packs/ideator.md +1 -1
- package/docs/recipes/README.md +1 -1
- package/docs/recipes/audit-replay.md +2 -2
- package/docs/recipes/gds-db-backup.md +3 -3
- package/docs/recipes/managed-settings-remote-control.md +9 -9
- package/docs/recipes/memory-sync-conflicts.md +2 -2
- package/docs/recipes/ops-gotchas.md +24 -5
- package/docs/recipes/overnight-builder.md +3 -3
- package/docs/recipes/parallel-goal-waves.md +3 -3
- package/docs/recipes/reranking-a-goals-board.md +2 -2
- package/docs/recipes/search-index-ingestion.md +1 -1
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/staging-environment.md +1 -1
- package/docs/recipes/windows-builders.md +1 -1
- package/modules/hall-ui/public/deploy.html +10 -5
- package/modules/hall-ui/public/deploy.js +199 -110
- package/modules/hall-ui/public/project-settings.css +36 -0
- package/modules/provisioning/core-upgrade.js +61 -2
- package/modules/public-landing/public/contact.html +1 -1
- package/modules/public-landing/public/index.html +1 -1
- package/modules/public-landing/public/privacy.html +1 -1
- package/modules/public-landing/public/projects.html +1 -1
- package/modules/public-landing/public/terms.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/api-alias-caller-scan.js +53 -9
- package/scripts/gds/cloudbongos-token-isolation.conf +33 -0
- package/scripts/gds/provision-core-upgrade.js +132 -1
- package/scripts/gds/run-unit-tests.js +14 -9
- package/scripts/hall-preview/server.js +5 -0
- package/src/module-api.js +1 -1
- package/tests/api_alias_caller_ratchet.mjs +74 -2
- package/tests/core_upgrade_door.mjs +81 -0
- package/tests/core_upgrade_runner.mjs +132 -0
- package/tests/deploy_page_projects.mjs +234 -79
- package/tests/idea_spark_hall.mjs +101 -12
- package/tests/known_failing_ratchet.mjs +19 -9
- package/tests/landing_legal_pages.mjs +5 -1
- package/tests/projects_hub_pre_uat.mjs +12 -6
- 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 Bongos. <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">
|
|
84
|
+
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud 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.
|
|
3
|
+
"version": "1.19.974",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.974",
|
|
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.
|
|
3
|
+
"version": "1.19.974",
|
|
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) —
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// still
|
|
48
|
-
//
|
|
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
|
|
103
|
-
|
|
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,21 @@ 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
|
+
// covers the heading rather than the heading being rewritten, because the heading is right.
|
|
150
|
+
//
|
|
151
|
+
// NARROWED after grade review on 1004113: the first cut of this was a bare `\bcallback\b`, which
|
|
152
|
+
// excused ANY line carrying the word — "the webhook fires a callback to /api/gds/tasks" would have
|
|
153
|
+
// gone silently uncounted, and a shrink-only ratchet whose exclusions can be triggered by a common
|
|
154
|
+
// noun is not shrink-only. `callback URL` is the OAuth-registration phrase specifically, and the
|
|
155
|
+
// negative cases below are tested, not just the positive ones.
|
|
156
|
+
const ALIAS_PINNED_AUTH_RE = /\/api\/gds\/auth\/|\bcallback URL\b/i;
|
|
157
|
+
|
|
117
158
|
function isExcludedPath(relPath) {
|
|
118
159
|
const p = relPath.replace(/\\/g, '/');
|
|
119
160
|
if (ALIAS_EXCLUDED_TREES.test(p)) return true;
|
|
@@ -127,6 +168,7 @@ function isCountedLine(line) {
|
|
|
127
168
|
if (!line.includes(ALIAS)) return false;
|
|
128
169
|
if (line.includes(CANONICAL)) return false;
|
|
129
170
|
if (ALIAS_COMMENTARY_RE.test(line)) return false;
|
|
171
|
+
if (ALIAS_PINNED_AUTH_RE.test(line)) return false;
|
|
130
172
|
return true;
|
|
131
173
|
}
|
|
132
174
|
|
|
@@ -193,8 +235,10 @@ function main() {
|
|
|
193
235
|
const show = process.argv.includes('--files') ? ranked : ranked.slice(0, 20);
|
|
194
236
|
console.log(`in-repo /api/gds callers: ${total} across ${ranked.length} file(s) (${scanned} file(s) scanned)`);
|
|
195
237
|
console.log('(the alias contract, the pinned OAuth callbacks, the uptime poller + smoke, the rename guardrails,');
|
|
196
|
-
console.log(' tests/,
|
|
197
|
-
console.log('
|
|
238
|
+
console.log(' tests/, the FROZEN RECORD under docs/ (adr, session-logs, audits, governance), migrations/ and');
|
|
239
|
+
console.log(' captured fixtures are excluded by construction; so is any line that also names /api/bongos, carries');
|
|
240
|
+
console.log(' the words alias / legacy / permanent / forever, or names a pinned /api/gds/auth/ callback URL.');
|
|
241
|
+
console.log(' The REST of docs/ is counted — live instruction that still teaches the alias should shrink.)');
|
|
198
242
|
console.log('');
|
|
199
243
|
for (const [f, n] of show) console.log(` ${String(n).padStart(5)} ${f}`);
|
|
200
244
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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.
|
|
74
|
+
const CORE_VERSION = '1.19.974'; // 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/
|
|
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,27 @@ 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
|
+
// …and the NEGATIVE half, which is the half that keeps a shrink-only metric shrink-only. The
|
|
168
|
+
// first cut of this rule was a bare `\bcallback\b`; a line that merely USES the common noun must
|
|
169
|
+
// still count, or the exclusion becomes a way to hide a caller by wording a sentence around it.
|
|
170
|
+
assert.equal(scan.isCountedLine('the webhook fires a callback to /api/gds/tasks'), true, 'a plain "callback" is not the pinned URL');
|
|
171
|
+
assert.equal(scan.isCountedLine('// callback handler posts to /api/gds/hooks'), true);
|
|
172
|
+
assert.equal(scan.isCountedLine('pass a callback; the poller reads /api/gds/versions'), true);
|
|
173
|
+
});
|
|
174
|
+
|
|
103
175
|
test('occurrences are counted, not lines — moving ONE of two callers on a line lowers the number', () => {
|
|
104
176
|
assert.equal(scan.countInText('f("/api/gds/a"); f("/api/gds/b");\n'), 2);
|
|
105
177
|
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);
|