@bongos/core 1.19.755 → 1.19.757
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 +31 -16
- package/docs/module-api-changelog.md +4 -0
- package/modules/lifecycle/db-tasks.js +39 -6
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness-checks-error-envelope.js +134 -0
- package/scripts/gds/fitness-checks-packaging.js +132 -0
- package/scripts/gds/fitness.js +2 -213
- package/src/bongos/optimizer.js +27 -3
- package/src/bongos/serve-internal.js +56 -2
- package/src/module-api.js +1 -1
- package/tests/claimable_blocker_status.mjs +106 -0
- package/tests/single_host_apex_routes.mjs +170 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.757",
|
|
6
|
+
"core_contract": "1.19.757",
|
|
7
|
+
"source_commit": "cc213a1bab1fc07008953502c035d32a98ced469",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-16T02:
|
|
9
|
+
"built_at": "2026-09-16T02:31:57.888Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 483,
|
|
13
13
|
"agent_docs_stubbed": 25,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2115,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2623,
|
|
20
|
+
"tree_sha256": "6dfa78abe36908cd3c7762f36e1145d5ca9a22362b4825d6eb8c240e05a01815",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2512,7 +2512,7 @@
|
|
|
2512
2512
|
{
|
|
2513
2513
|
"path": "docs/module-api-changelog.md",
|
|
2514
2514
|
"mode": "0000644",
|
|
2515
|
-
"sha256": "
|
|
2515
|
+
"sha256": "c415a541beec31293c9fe17473fce5706b0c043818a3da9d2c092d6a36892a12"
|
|
2516
2516
|
},
|
|
2517
2517
|
{
|
|
2518
2518
|
"path": "docs/modules-contract.md",
|
|
@@ -5472,7 +5472,7 @@
|
|
|
5472
5472
|
{
|
|
5473
5473
|
"path": "modules/lifecycle/db-tasks.js",
|
|
5474
5474
|
"mode": "0000644",
|
|
5475
|
-
"sha256": "
|
|
5475
|
+
"sha256": "381e3bf734537972e33bc7ca3f23097f3f264f42bf33d29cb0c5378b01459c68"
|
|
5476
5476
|
},
|
|
5477
5477
|
{
|
|
5478
5478
|
"path": "modules/lifecycle/db-versions.js",
|
|
@@ -7567,12 +7567,12 @@
|
|
|
7567
7567
|
{
|
|
7568
7568
|
"path": "package-lock.json",
|
|
7569
7569
|
"mode": "0000644",
|
|
7570
|
-
"sha256": "
|
|
7570
|
+
"sha256": "93d77133102c1137fe1e2772f30fabcd7e4630aece1c1ec238db81580f23f634"
|
|
7571
7571
|
},
|
|
7572
7572
|
{
|
|
7573
7573
|
"path": "package.json",
|
|
7574
7574
|
"mode": "0000644",
|
|
7575
|
-
"sha256": "
|
|
7575
|
+
"sha256": "eb18ccfda9df0b09d536acaff0fdb18df77860d53a35cfa65b41cc1aa70e361c"
|
|
7576
7576
|
},
|
|
7577
7577
|
{
|
|
7578
7578
|
"path": "public-docs/index.html",
|
|
@@ -8024,11 +8024,21 @@
|
|
|
8024
8024
|
"mode": "0000644",
|
|
8025
8025
|
"sha256": "b8b1a7ed1d8dd217b24757a3ec9f4b3aa58b26a3e2a7b2cef9de217792e5ea43"
|
|
8026
8026
|
},
|
|
8027
|
+
{
|
|
8028
|
+
"path": "scripts/gds/fitness-checks-error-envelope.js",
|
|
8029
|
+
"mode": "0000644",
|
|
8030
|
+
"sha256": "ab11c75ae177397fdd507505732450fee3531d42b99ba0b34811d06563ebd21c"
|
|
8031
|
+
},
|
|
8027
8032
|
{
|
|
8028
8033
|
"path": "scripts/gds/fitness-checks-identity.js",
|
|
8029
8034
|
"mode": "0000644",
|
|
8030
8035
|
"sha256": "583b21bf6527b657058ebfab50aff8c51cec96a0b1baa4d01daea55e90d0ccf7"
|
|
8031
8036
|
},
|
|
8037
|
+
{
|
|
8038
|
+
"path": "scripts/gds/fitness-checks-packaging.js",
|
|
8039
|
+
"mode": "0000644",
|
|
8040
|
+
"sha256": "1f0611c9b8cb30e20119d50f67bc3a1148d6b5d869337361ae0ac58724aaca12"
|
|
8041
|
+
},
|
|
8032
8042
|
{
|
|
8033
8043
|
"path": "scripts/gds/fitness-checks-route-pins.js",
|
|
8034
8044
|
"mode": "0000644",
|
|
@@ -8057,7 +8067,7 @@
|
|
|
8057
8067
|
{
|
|
8058
8068
|
"path": "scripts/gds/fitness.js",
|
|
8059
8069
|
"mode": "0000644",
|
|
8060
|
-
"sha256": "
|
|
8070
|
+
"sha256": "4856485e5bb5bb03e47cd6953659c25214411d0b63287f6ab2faa4c34e1e27bc"
|
|
8061
8071
|
},
|
|
8062
8072
|
{
|
|
8063
8073
|
"path": "scripts/gds/gate-review.js",
|
|
@@ -9192,7 +9202,7 @@
|
|
|
9192
9202
|
{
|
|
9193
9203
|
"path": "src/bongos/optimizer.js",
|
|
9194
9204
|
"mode": "0000644",
|
|
9195
|
-
"sha256": "
|
|
9205
|
+
"sha256": "eaa5ba49269f8188222b0c979ea60e05f1e6ddc1a0ba8064f735a3f8ac25e0ce"
|
|
9196
9206
|
},
|
|
9197
9207
|
{
|
|
9198
9208
|
"path": "src/bongos/path-match.js",
|
|
@@ -9342,7 +9352,7 @@
|
|
|
9342
9352
|
{
|
|
9343
9353
|
"path": "src/bongos/serve-internal.js",
|
|
9344
9354
|
"mode": "0000644",
|
|
9345
|
-
"sha256": "
|
|
9355
|
+
"sha256": "f4bba033b2052b61d026682b0a06dd3044d77138e1978ad601060fbebbfdc34d"
|
|
9346
9356
|
},
|
|
9347
9357
|
{
|
|
9348
9358
|
"path": "src/bongos/uptime-poller.js",
|
|
@@ -9372,7 +9382,7 @@
|
|
|
9372
9382
|
{
|
|
9373
9383
|
"path": "src/module-api.js",
|
|
9374
9384
|
"mode": "0000644",
|
|
9375
|
-
"sha256": "
|
|
9385
|
+
"sha256": "9798743d1dae3c952b257d48b1ee56341bf0a946504ebb6be66fd88f1a8cddcd"
|
|
9376
9386
|
},
|
|
9377
9387
|
{
|
|
9378
9388
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10002,7 +10012,7 @@
|
|
|
10002
10012
|
{
|
|
10003
10013
|
"path": "tests/claimable_blocker_status.mjs",
|
|
10004
10014
|
"mode": "0000644",
|
|
10005
|
-
"sha256": "
|
|
10015
|
+
"sha256": "1aae622cb425a287309bc68f5537d754ff9e31b2c4cd8506c7ec72288da69220"
|
|
10006
10016
|
},
|
|
10007
10017
|
{
|
|
10008
10018
|
"path": "tests/claims_bind_session.mjs",
|
|
@@ -12644,6 +12654,11 @@
|
|
|
12644
12654
|
"mode": "0000644",
|
|
12645
12655
|
"sha256": "2396f97cac08f5fd752a73f3728a0ae90807fe7af7fbb5da2340c4436d280f0f"
|
|
12646
12656
|
},
|
|
12657
|
+
{
|
|
12658
|
+
"path": "tests/single_host_apex_routes.mjs",
|
|
12659
|
+
"mode": "0000644",
|
|
12660
|
+
"sha256": "d6869d2749e06a2c67c280e1800ea5e84eb042542507c684aaf01b3b8d4da12f"
|
|
12661
|
+
},
|
|
12647
12662
|
{
|
|
12648
12663
|
"path": "tests/single_host_hall.mjs",
|
|
12649
12664
|
"mode": "0000644",
|
|
@@ -1984,5 +1984,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1984
1984
|
landed since 1.19.753 with no explicit bump. run 35039836268. (task 1002620)
|
|
1985
1985
|
1.19.755 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1986
1986
|
landed since 1.19.754 with no explicit bump. run 35047010813. (task 1002620)
|
|
1987
|
+
1.19.756 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1988
|
+
landed since 1.19.755 with no explicit bump. run 35047623582. (task 1002620)
|
|
1989
|
+
1.19.757 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1990
|
+
landed since 1.19.756 with no explicit bump. run 35048307666. (task 1002620)
|
|
1987
1991
|
---------------------------------------------------------------------------
|
|
1988
1992
|
```
|
|
@@ -766,14 +766,48 @@ async function listTasks({ versionId, status, kind, discipline, goalId, createdB
|
|
|
766
766
|
// it just stops the query's LOCKING from referencing the view relations.
|
|
767
767
|
// If the view bodies change in a future migration, change these in lockstep
|
|
768
768
|
// (or fold back to the views once per-view DDL contention is a solved problem).
|
|
769
|
+
// THE BLOCKER GUARD, spelled ONCE (task 1003913).
|
|
770
|
+
//
|
|
771
|
+
// A task is hidden from the queue only while a blocker on it is still OPEN — not
|
|
772
|
+
// forever because a link row exists. Nothing ever deletes a `task_blockers` row,
|
|
773
|
+
// so the older `NOT EXISTS (SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id)`
|
|
774
|
+
// meant that a task which had EVER been blocked vanished from /builder-start the
|
|
775
|
+
// moment its blocker resolved — workable by id, unfindable in the feed, and
|
|
776
|
+
// permanent.
|
|
777
|
+
//
|
|
778
|
+
// Migration core_239 fixed exactly this in the `claimable_tasks` VIEW and the two
|
|
779
|
+
// queries below were missed, because the predicate existed in three spellings and
|
|
780
|
+
// only one of them was a migration.
|
|
781
|
+
//
|
|
782
|
+
// BOTH queries below interpolate this constant — the feed and the session-fit
|
|
783
|
+
// projection, which is the whole point of it being one. The first cut of this fix
|
|
784
|
+
// declared the constant and then hand-copied the clause into SESSION_FIT_SQL
|
|
785
|
+
// anyway, recreating the two-spellings bug inside the file written to remove it.
|
|
786
|
+
// The grader caught it; tests/claimable_blocker_status.mjs now COUNTS the
|
|
787
|
+
// interpolations instead of merely checking that the constant exists.
|
|
788
|
+
//
|
|
789
|
+
// src/bongos/optimizer.js keeps its OWN copy of this text: it inlines the view
|
|
790
|
+
// bodies on purpose (a lock incident, task 2181) and cannot require a module file
|
|
791
|
+
// across the ADR 0083 boundary. It deduplicates within itself the same way.
|
|
792
|
+
//
|
|
793
|
+
// THE LEFT JOIN IS LOAD-BEARING. A legacy markdown-anchor link carries
|
|
794
|
+
// `blocker_ref` with no `blockers` row at all; an inner join would drop it from the
|
|
795
|
+
// NOT EXISTS and silently UNHIDE it. `b.id IS NULL` keeps an unresolvable link
|
|
796
|
+
// blocking, which is the fail-closed arm.
|
|
797
|
+
const OPEN_BLOCKER_GUARD = `
|
|
798
|
+
AND NOT EXISTS (
|
|
799
|
+
SELECT 1
|
|
800
|
+
FROM task_blockers tb
|
|
801
|
+
LEFT JOIN blockers b ON b.id = tb.blocker_id
|
|
802
|
+
WHERE tb.task_id = t.id
|
|
803
|
+
AND (b.id IS NULL OR b.status = 'open')
|
|
804
|
+
)`;
|
|
805
|
+
|
|
769
806
|
const CLAIMABLE_SQL = `
|
|
770
807
|
SELECT t.*, pms_calibrated_minutes(t.est_minutes, t.kind) AS est_minutes_calibrated
|
|
771
808
|
FROM tasks t
|
|
772
809
|
WHERE t.status = 'ready'
|
|
773
|
-
AND t.title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'
|
|
774
|
-
AND NOT EXISTS (
|
|
775
|
-
SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id
|
|
776
|
-
)
|
|
810
|
+
AND t.title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'${OPEN_BLOCKER_GUARD}
|
|
777
811
|
AND NOT EXISTS (
|
|
778
812
|
SELECT 1 FROM claims c
|
|
779
813
|
JOIN tasks t2 ON t2.id = c.task_id
|
|
@@ -783,8 +817,7 @@ const CLAIMABLE_SQL = `
|
|
|
783
817
|
const SESSION_FIT_SQL = `
|
|
784
818
|
SELECT t.id,
|
|
785
819
|
CASE
|
|
786
|
-
WHEN t.automation_tag = 'overnight-safe' AND COALESCE(t.manual_degree, 5) <= 2
|
|
787
|
-
AND NOT EXISTS (SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id)
|
|
820
|
+
WHEN t.automation_tag = 'overnight-safe' AND COALESCE(t.manual_degree, 5) <= 2${OPEN_BLOCKER_GUARD}
|
|
788
821
|
THEN ARRAY['autonomous','check-in','active']
|
|
789
822
|
WHEN t.automation_tag = 'live-only' OR COALESCE(t.manual_degree, 5) >= 4
|
|
790
823
|
THEN ARRAY['active']
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.757",
|
|
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.757",
|
|
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.757",
|
|
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",
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// scripts/gds/fitness-checks-error-envelope.js — Check 17: no route hand-rolls a
|
|
2
|
+
// FLAT error (BV1 R03 / task 1990 / ADR 0116).
|
|
3
|
+
//
|
|
4
|
+
// Carved out of scripts/gds/fitness.js by task 1003825 alongside the packaging pair,
|
|
5
|
+
// for the headroom reason documented there. Self-contained: the scan needs only
|
|
6
|
+
// CORE_ROOT / rel / walkJs / walkJsAllRoots from fitness-lib.js, and FLAT_ERROR_BASELINE
|
|
7
|
+
// travels WITH the check rather than staying behind — the baseline and the gate that
|
|
8
|
+
// reads it are one decision, and splitting them is how a ratchet quietly loses its floor.
|
|
9
|
+
//
|
|
10
|
+
// fitness.js requires the four public names straight back (checkErrorEnvelope,
|
|
11
|
+
// scanFlatErrorSites, countFlatErrors, FLAT_ERROR_BASELINE — the last three are read by
|
|
12
|
+
// the R04 sweep and its tests), so nothing outside this file changes shape.
|
|
13
|
+
// stripCommentsForFlat stays PRIVATE here: it was never on fitness.js's exports, and
|
|
14
|
+
// exporting it now to mirror the carve would be a new public name nothing consumes.
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
|
|
21
|
+
const { CORE_ROOT, rel, walkJs, walkJsAllRoots } = require('./fitness-lib.js');
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Check 17 — no route hand-rolls a FLAT error (BV1 R03 / task 1990 / ADR 0116)
|
|
24
|
+
// ============================================================================
|
|
25
|
+
//
|
|
26
|
+
// The error-envelope contract (ADR 0116) says every failure is the ONE envelope
|
|
27
|
+
// `{error:{code,message,details}}`, emitted via res.fail() or a thrown ApiError
|
|
28
|
+
// (the R02 middleware, task 1989). A hand-rolled flat error —
|
|
29
|
+
// `res.status(n).json({ error: 'x' })` or `.json({ error })` — breaks the
|
|
30
|
+
// contract: the caller gets a bare string where the envelope should be.
|
|
31
|
+
//
|
|
32
|
+
// RATCHET, not big-bang. The R04 sweep (task 1991) migrates the ~562 existing
|
|
33
|
+
// sites; until it finishes they'd all "fail" a strict check, so this HOLDS THE
|
|
34
|
+
// LINE: it fails only when the count rises ABOVE the committed baseline (a NEW
|
|
35
|
+
// hand-rolled flat error), and nudges the baseline DOWN as the sweep shrinks it.
|
|
36
|
+
// When the sweep reaches 0, FLAT_ERROR_BASELINE is 0 and any new flat error
|
|
37
|
+
// hard-fails immediately. Detector is shared with the sweep + its test.
|
|
38
|
+
const FLAT_ERROR_BASELINE = 0; // R04 sweep (task 1991) landed the surface at 0 — the gate is now hard-zero: any new hand-rolled flat error fails the build immediately.
|
|
39
|
+
// A flat error is a `.json({ … })` whose FIRST key is `error` with a value that
|
|
40
|
+
// is NOT an object literal (a string/var/shorthand). The envelope's nested
|
|
41
|
+
// `error: { … }` and `.json(envelope(…))` deliberately do NOT match.
|
|
42
|
+
// The `(?!\s*\{)` lookahead is whitespace-aware: without the inner `\s*` the
|
|
43
|
+
// preceding `\s*` backtracks to zero and the lookahead is tested at the space in
|
|
44
|
+
// `error: {`, so the compliant nested envelope was wrongly flagged (task 2029).
|
|
45
|
+
const FLAT_ERROR_VALUE_RE = /\.json\(\s*\{\s*error\s*:\s*(?!\s*\{)/g; // error: 'x' | error: code
|
|
46
|
+
const FLAT_ERROR_SHORTHAND_RE = /\.json\(\s*\{\s*error\s*[,}]/g; // { error } | { error, … }
|
|
47
|
+
|
|
48
|
+
// Strip // line comments + /* */ block comments AND blank out string/template
|
|
49
|
+
// contents (keeping the delimiters) so neither a DOC example of the anti-pattern
|
|
50
|
+
// — e.g. the res.fail() usage note in middleware/error-envelope.js — nor a
|
|
51
|
+
// pattern quoted inside a string literal is counted as a real flat error once
|
|
52
|
+
// the gate is hard-zero (task 1991). Real flat errors are unaffected: in
|
|
53
|
+
// `res.status(404).json({ error: 'x' })` the `.json({ error:` the detector keys
|
|
54
|
+
// on is CODE (outside the string), so blanking the `'x'` content still matches.
|
|
55
|
+
function stripCommentsForFlat(src) {
|
|
56
|
+
let out = '';
|
|
57
|
+
let i = 0;
|
|
58
|
+
let quote = null;
|
|
59
|
+
while (i < src.length) {
|
|
60
|
+
const c = src[i];
|
|
61
|
+
const d = src[i + 1];
|
|
62
|
+
if (quote) {
|
|
63
|
+
// inside a string/template: drop content, keep only the closing delimiter
|
|
64
|
+
if (c === '\\') { i += 2; continue; }
|
|
65
|
+
if (c === quote) { out += c; quote = null; }
|
|
66
|
+
i++; continue;
|
|
67
|
+
}
|
|
68
|
+
if (c === '"' || c === "'" || c === '`') { quote = c; out += c; i++; continue; }
|
|
69
|
+
if (c === '/' && d === '/') { while (i < src.length && src[i] !== '\n') i++; continue; }
|
|
70
|
+
if (c === '/' && d === '*') { i += 2; while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) i++; i += 2; continue; }
|
|
71
|
+
out += c; i++;
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function countFlatErrors(text) {
|
|
77
|
+
const code = stripCommentsForFlat(text);
|
|
78
|
+
return (code.match(FLAT_ERROR_VALUE_RE) || []).length
|
|
79
|
+
+ (code.match(FLAT_ERROR_SHORTHAND_RE) || []).length;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// scanFlatErrorSites() — per-file flat-error counts across the API surface
|
|
83
|
+
// (core src/bongos + every module), skipping tests. Shared by the check below, the
|
|
84
|
+
// R04 codemod, and tests/api_error_envelope_buildcheck.mjs.
|
|
85
|
+
function scanFlatErrorSites() {
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
const byFile = {};
|
|
88
|
+
let total = 0;
|
|
89
|
+
const files = [
|
|
90
|
+
...walkJs(path.join(CORE_ROOT, 'src', 'bongos')),
|
|
91
|
+
...walkJsAllRoots('modules'),
|
|
92
|
+
];
|
|
93
|
+
for (const abs of files) {
|
|
94
|
+
const relFile = rel(abs);
|
|
95
|
+
if (seen.has(relFile)) continue;
|
|
96
|
+
seen.add(relFile);
|
|
97
|
+
if (relFile.includes('/tests/') || relFile.endsWith('.test.js')) continue;
|
|
98
|
+
const n = countFlatErrors(fs.readFileSync(abs, 'utf8'));
|
|
99
|
+
if (n) { byFile[relFile] = n; total += n; }
|
|
100
|
+
}
|
|
101
|
+
return { total, byFile };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function checkErrorEnvelope() {
|
|
105
|
+
const { total, byFile } = scanFlatErrorSites();
|
|
106
|
+
const violations = [];
|
|
107
|
+
const warnings = [];
|
|
108
|
+
if (total > FLAT_ERROR_BASELINE) {
|
|
109
|
+
const top = Object.entries(byFile).sort((a, b) => b[1] - a[1]).slice(0, 8)
|
|
110
|
+
.map(([f, n]) => `${f} (${n})`).join(', ');
|
|
111
|
+
violations.push(
|
|
112
|
+
`${total} hand-rolled flat-error sites — ABOVE the ${FLAT_ERROR_BASELINE} baseline. A new route emitted a flat \`res.status(n).json({error:'x'})\` instead of \`res.fail('x', n)\` / \`throw new ApiError('x')\` (ADR 0116). Top files: ${top}. Use the envelope helper (src/bongos/middleware/error-envelope.js).`
|
|
113
|
+
);
|
|
114
|
+
} else if (total < FLAT_ERROR_BASELINE) {
|
|
115
|
+
warnings.push(
|
|
116
|
+
`${total} flat-error sites — BELOW the ${FLAT_ERROR_BASELINE} baseline. The sweep tightened; lower FLAT_ERROR_BASELINE to ${total} in fitness.js so the ratchet can't slip back (BV1 R04 / task 1991).`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
name: 'no route hand-rolls a flat error (ADR 0116 / R03)',
|
|
121
|
+
ok: violations.length === 0,
|
|
122
|
+
hardFail: violations.length > 0,
|
|
123
|
+
violations,
|
|
124
|
+
warnings,
|
|
125
|
+
note: `≤ ${FLAT_ERROR_BASELINE} flat \`.json({error:'…'})\` sites (ratchet → 0 as the R04 sweep lands); currently ${total}. New failures must use res.fail()/ApiError (the R02 envelope).`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
checkErrorEnvelope,
|
|
131
|
+
scanFlatErrorSites,
|
|
132
|
+
countFlatErrors,
|
|
133
|
+
FLAT_ERROR_BASELINE,
|
|
134
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// scripts/gds/fitness-checks-packaging.js — Checks 16 + 16b: core code must not
|
|
2
|
+
// HARD-require a boundary-EXCLUDED config or module.
|
|
3
|
+
//
|
|
4
|
+
// Carved out of scripts/gds/fitness.js by task 1003825, on the task-1003192
|
|
5
|
+
// precedent. That file had returned to the 1,500-line ratchet with ONE line of
|
|
6
|
+
// headroom (1,499 of 1,500, fitness-ratchets.js OVERSIZE_LINES), so the next check
|
|
7
|
+
// registered there would have tipped it over and redded the ratchet for whoever
|
|
8
|
+
// wrote it. These two checks left together because they are one subject — the same
|
|
9
|
+
// question asked of a config and of a module — and because the pair depends only on
|
|
10
|
+
// CORE_ROOT / rel / walkJs / walkJsAllRoots, all already public on fitness-lib.js,
|
|
11
|
+
// so the carve adds no import cycle.
|
|
12
|
+
//
|
|
13
|
+
// fitness.js requires both names straight back, so the CHECKS registry and the
|
|
14
|
+
// module surface the tests read are unchanged.
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
|
|
21
|
+
const { CORE_ROOT, rel, walkJs, walkJsAllRoots } = require('./fitness-lib.js');
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Check 16 — core code must not HARD-require an instance-excluded config/*.json
|
|
24
|
+
// ============================================================================
|
|
25
|
+
//
|
|
26
|
+
// A vanilla self-host boot ships only the PUBLISHABLE tree (publish-manifest.js). An
|
|
27
|
+
// instance-only config (config/devbox-app.json, config/bongos-cli.json, config/branding.json,
|
|
28
|
+
// config/modules.json, …) is boundary-EXCLUDED, so a bare `require('../../config/<excluded>.json')`
|
|
29
|
+
// in core code throws MODULE_NOT_FOUND and CRASHES the boot on a fresh instance — the exact
|
|
30
|
+
// class that shipped in R88 (two hard-requires in src/bongos/serve-internal.js; task 1824). The
|
|
31
|
+
// cure is to READ the config instead of requiring it, tolerating a missing file
|
|
32
|
+
// (tryLoadInstanceConfig in src/bongos/serve-internal.js / fs.existsSync → fail-to-vanilla), which
|
|
33
|
+
// also drops the require() literal so it no longer matches here. Check 1 (core↔host) scans
|
|
34
|
+
// code→code IMPORTS and misses this code→config class. Only an EXCLUDED config is a violation;
|
|
35
|
+
// a publishable neutral.*/schema config (which ships with the core) is fine.
|
|
36
|
+
const CONFIG_REQUIRE_RE = /require\(\s*['"]([^'"]*config\/[^'"]*\.json)['"]\s*\)/g;
|
|
37
|
+
|
|
38
|
+
function checkCoreRequiresExcludedConfig() {
|
|
39
|
+
const { isPublishable } = require('./publish-manifest');
|
|
40
|
+
const violations = [];
|
|
41
|
+
// src/ + scripts/ are CORE; modules ship from BOTH roots (ADR 0108 §Consequences #4). Dedup by
|
|
42
|
+
// repo-relative path so a shared checkout scans each file once.
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
const allAbs = [
|
|
45
|
+
...['src', 'scripts'].flatMap((d) => walkJs(path.join(CORE_ROOT, d))),
|
|
46
|
+
...walkJsAllRoots('modules'),
|
|
47
|
+
];
|
|
48
|
+
for (const abs of allAbs) {
|
|
49
|
+
const relFile = rel(abs);
|
|
50
|
+
if (seen.has(relFile)) continue;
|
|
51
|
+
seen.add(relFile);
|
|
52
|
+
if (!isPublishable(relFile)) continue; // only CORE (publishable) code can crash a vanilla boot
|
|
53
|
+
for (const raw of fs.readFileSync(abs, 'utf8').split('\n')) {
|
|
54
|
+
const trimmed = raw.trim();
|
|
55
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue;
|
|
56
|
+
let m;
|
|
57
|
+
CONFIG_REQUIRE_RE.lastIndex = 0;
|
|
58
|
+
while ((m = CONFIG_REQUIRE_RE.exec(raw))) {
|
|
59
|
+
const relCfg = rel(path.resolve(path.dirname(abs), m[1]));
|
|
60
|
+
if (!isPublishable(relCfg)) {
|
|
61
|
+
violations.push(`${relFile} → require('${m[1]}') hard-requires an instance-excluded config (${relCfg}) — guard the read (tryLoadInstanceConfig in src/bongos/serve-internal.js / fs.existsSync, fail-to-vanilla) so a fresh core boots (task 1824).`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
name: 'core never hard-requires an excluded config (task 1824)',
|
|
68
|
+
ok: violations.length === 0,
|
|
69
|
+
hardFail: violations.length > 0,
|
|
70
|
+
violations,
|
|
71
|
+
warnings: [],
|
|
72
|
+
note: "a publishable src/·scripts/·modules/ .js must not require('…config/X.json') when X.json is boundary-excluded — guard the read so a vanilla core boots.",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Check 16b — core code must not require an instance-EXCLUDED module (task 2135)
|
|
78
|
+
// ============================================================================
|
|
79
|
+
//
|
|
80
|
+
// Sibling of Check 16 for the code→MODULE class. A publishable core file that
|
|
81
|
+
// require()s a boundary-EXCLUDED module (modules/game, modules/art-pipeline,
|
|
82
|
+
// modules/character-anim, …) throws MODULE_NOT_FOUND once that module lives in the
|
|
83
|
+
// consumer's instance repo rather than the core package (R89 split). The cure is
|
|
84
|
+
// either to reclassify the module INTO core (if it belongs there — e.g. provisioning,
|
|
85
|
+
// sibling of dev-box) or to exclude the requiring file with the feature it serves
|
|
86
|
+
// (art tooling). Catches BOTH require forms: a direct literal
|
|
87
|
+
// require('../modules/<key>/…') AND a require(path.join(<root>, 'modules/<key>/…')).
|
|
88
|
+
// Only an EXCLUDED module required from a PUBLISHABLE file is a violation; requiring a
|
|
89
|
+
// publishable (core) module is fine, and the loader's DYNAMIC dispatch
|
|
90
|
+
// (path.join(dir,'routes',rk) — no literal modules/<key>/ string) is invisible here.
|
|
91
|
+
const MODULE_PATH_IN_STRING_RE = /['"](?:[^'"]*\/)?modules\/([a-z0-9][a-z0-9-]*)\/[^'"]*['"]/g;
|
|
92
|
+
|
|
93
|
+
function checkCoreRequiresExcludedModule() {
|
|
94
|
+
const { isPublishable } = require('./publish-manifest');
|
|
95
|
+
const violations = [];
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
const allAbs = [
|
|
98
|
+
...['src', 'scripts'].flatMap((d) => walkJs(path.join(CORE_ROOT, d))),
|
|
99
|
+
...walkJsAllRoots('modules'),
|
|
100
|
+
];
|
|
101
|
+
for (const abs of allAbs) {
|
|
102
|
+
const relFile = rel(abs);
|
|
103
|
+
if (seen.has(relFile)) continue;
|
|
104
|
+
seen.add(relFile);
|
|
105
|
+
if (!isPublishable(relFile)) continue; // only CORE (publishable) code can crash a vanilla boot
|
|
106
|
+
for (const raw of fs.readFileSync(abs, 'utf8').split('\n')) {
|
|
107
|
+
const trimmed = raw.trim();
|
|
108
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue;
|
|
109
|
+
if (!/require\s*\(/.test(raw)) continue; // only lines that actually require()
|
|
110
|
+
let m;
|
|
111
|
+
MODULE_PATH_IN_STRING_RE.lastIndex = 0;
|
|
112
|
+
while ((m = MODULE_PATH_IN_STRING_RE.exec(raw))) {
|
|
113
|
+
const key = m[1];
|
|
114
|
+
if (isPublishable(`modules/${key}/module.json`)) continue; // requiring a CORE module is fine
|
|
115
|
+
violations.push(`${relFile} → require(...'modules/${key}/…') pulls in an instance-EXCLUDED module — reclassify it into core (if it belongs there) or exclude ${relFile} with the feature it serves, so a fresh core boots (task 2135).`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
name: 'core never requires an excluded module (task 2135)',
|
|
121
|
+
ok: violations.length === 0,
|
|
122
|
+
hardFail: violations.length > 0,
|
|
123
|
+
violations,
|
|
124
|
+
warnings: [],
|
|
125
|
+
note: "a publishable src/·scripts/·modules/ .js must not require('…modules/<key>/…') when that module is boundary-excluded — MODULE_NOT_FOUND post-split (R89).",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
checkCoreRequiresExcludedConfig,
|
|
131
|
+
checkCoreRequiresExcludedModule,
|
|
132
|
+
};
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -37,6 +37,8 @@ const { CORE_ROOT, INSTANCE_ROOT, MODULE_ROOTS, REPO_ROOT, rel, walkJs, walkJsAl
|
|
|
37
37
|
const { IDENTITY_SCAN_ALLOWED, IDENTITY_SCAN_EXCLUDE, IDENTITY_SCAN_EXTRA_FILES, IDENTITY_SCAN_EXTS, IDENTITY_SCAN_ROOTS, IDENTITY_SCAN_SKIP_DIRS, PROMPT_IDENTITY_LITERALS, checkDesignContract, checkKernelPromptsNoHardcodedIdentity, checkNeutralConfigsNoHostIdentity, collectIdentityFiles, scanPromptIdentity, stripCommentsForIdentity } = require('./fitness-checks-identity.js');
|
|
38
38
|
const { WRITE_VALIDATION_BASELINE, WRITE_VALIDATION_EXEMPT, checkWriteRoutesValidated, scanUnvalidatedWriteRoutes } = require('./fitness-checks-write-validation.js');
|
|
39
39
|
const { GOAL_VOCAB_CITATION, GOAL_VOCAB_LINE_SCOPED, GOAL_VOCAB_SURFACE, LIFECYCLE_DB_FILES, PRELAUNCH_SURFACE, RENAME_IDENTITY_RE, VOCAB_ALLOW, VOCAB_BANNED, VOCAB_CITATION, VOCAB_EXTS, VOCAB_HISTORICAL_REF, VOCAB_SURFACE, checkGoalMembershipVocabulary, checkGovernmentRenameIdentity, checkPrelaunchVocabulary, checkVocabularyWall, vocabFilesToScan } = require('./fitness-checks-vocabulary.js');
|
|
40
|
+
const { checkCoreRequiresExcludedConfig, checkCoreRequiresExcludedModule } = require('./fitness-checks-packaging.js'); // Checks 16 + 16b — carved out by task 1003825 (the 1,500-line ratchet)
|
|
41
|
+
const { FLAT_ERROR_BASELINE, checkErrorEnvelope, countFlatErrors, scanFlatErrorSites } = require('./fitness-checks-error-envelope.js'); // Check 17 — carved out by task 1003825
|
|
40
42
|
// ============================================================================
|
|
41
43
|
// Check 1 — core ↔ host/game import boundary (ADR 0062 §1 cut-line)
|
|
42
44
|
// ============================================================================
|
|
@@ -1084,219 +1086,6 @@ function checkHallSectionRegistry() {
|
|
|
1084
1086
|
};
|
|
1085
1087
|
}
|
|
1086
1088
|
|
|
1087
|
-
// ============================================================================
|
|
1088
|
-
// Check 16 — core code must not HARD-require an instance-excluded config/*.json
|
|
1089
|
-
// ============================================================================
|
|
1090
|
-
//
|
|
1091
|
-
// A vanilla self-host boot ships only the PUBLISHABLE tree (publish-manifest.js). An
|
|
1092
|
-
// instance-only config (config/devbox-app.json, config/bongos-cli.json, config/branding.json,
|
|
1093
|
-
// config/modules.json, …) is boundary-EXCLUDED, so a bare `require('../../config/<excluded>.json')`
|
|
1094
|
-
// in core code throws MODULE_NOT_FOUND and CRASHES the boot on a fresh instance — the exact
|
|
1095
|
-
// class that shipped in R88 (two hard-requires in src/bongos/serve-internal.js; task 1824). The
|
|
1096
|
-
// cure is to READ the config instead of requiring it, tolerating a missing file
|
|
1097
|
-
// (tryLoadInstanceConfig in src/bongos/serve-internal.js / fs.existsSync → fail-to-vanilla), which
|
|
1098
|
-
// also drops the require() literal so it no longer matches here. Check 1 (core↔host) scans
|
|
1099
|
-
// code→code IMPORTS and misses this code→config class. Only an EXCLUDED config is a violation;
|
|
1100
|
-
// a publishable neutral.*/schema config (which ships with the core) is fine.
|
|
1101
|
-
const CONFIG_REQUIRE_RE = /require\(\s*['"]([^'"]*config\/[^'"]*\.json)['"]\s*\)/g;
|
|
1102
|
-
|
|
1103
|
-
function checkCoreRequiresExcludedConfig() {
|
|
1104
|
-
const { isPublishable } = require('./publish-manifest');
|
|
1105
|
-
const violations = [];
|
|
1106
|
-
// src/ + scripts/ are CORE; modules ship from BOTH roots (ADR 0108 §Consequences #4). Dedup by
|
|
1107
|
-
// repo-relative path so a shared checkout scans each file once.
|
|
1108
|
-
const seen = new Set();
|
|
1109
|
-
const allAbs = [
|
|
1110
|
-
...['src', 'scripts'].flatMap((d) => walkJs(path.join(CORE_ROOT, d))),
|
|
1111
|
-
...walkJsAllRoots('modules'),
|
|
1112
|
-
];
|
|
1113
|
-
for (const abs of allAbs) {
|
|
1114
|
-
const relFile = rel(abs);
|
|
1115
|
-
if (seen.has(relFile)) continue;
|
|
1116
|
-
seen.add(relFile);
|
|
1117
|
-
if (!isPublishable(relFile)) continue; // only CORE (publishable) code can crash a vanilla boot
|
|
1118
|
-
for (const raw of fs.readFileSync(abs, 'utf8').split('\n')) {
|
|
1119
|
-
const trimmed = raw.trim();
|
|
1120
|
-
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue;
|
|
1121
|
-
let m;
|
|
1122
|
-
CONFIG_REQUIRE_RE.lastIndex = 0;
|
|
1123
|
-
while ((m = CONFIG_REQUIRE_RE.exec(raw))) {
|
|
1124
|
-
const relCfg = rel(path.resolve(path.dirname(abs), m[1]));
|
|
1125
|
-
if (!isPublishable(relCfg)) {
|
|
1126
|
-
violations.push(`${relFile} → require('${m[1]}') hard-requires an instance-excluded config (${relCfg}) — guard the read (tryLoadInstanceConfig in src/bongos/serve-internal.js / fs.existsSync, fail-to-vanilla) so a fresh core boots (task 1824).`);
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
return {
|
|
1132
|
-
name: 'core never hard-requires an excluded config (task 1824)',
|
|
1133
|
-
ok: violations.length === 0,
|
|
1134
|
-
hardFail: violations.length > 0,
|
|
1135
|
-
violations,
|
|
1136
|
-
warnings: [],
|
|
1137
|
-
note: "a publishable src/·scripts/·modules/ .js must not require('…config/X.json') when X.json is boundary-excluded — guard the read so a vanilla core boots.",
|
|
1138
|
-
};
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
// ============================================================================
|
|
1142
|
-
// Check 16b — core code must not require an instance-EXCLUDED module (task 2135)
|
|
1143
|
-
// ============================================================================
|
|
1144
|
-
//
|
|
1145
|
-
// Sibling of Check 16 for the code→MODULE class. A publishable core file that
|
|
1146
|
-
// require()s a boundary-EXCLUDED module (modules/game, modules/art-pipeline,
|
|
1147
|
-
// modules/character-anim, …) throws MODULE_NOT_FOUND once that module lives in the
|
|
1148
|
-
// consumer's instance repo rather than the core package (R89 split). The cure is
|
|
1149
|
-
// either to reclassify the module INTO core (if it belongs there — e.g. provisioning,
|
|
1150
|
-
// sibling of dev-box) or to exclude the requiring file with the feature it serves
|
|
1151
|
-
// (art tooling). Catches BOTH require forms: a direct literal
|
|
1152
|
-
// require('../modules/<key>/…') AND a require(path.join(<root>, 'modules/<key>/…')).
|
|
1153
|
-
// Only an EXCLUDED module required from a PUBLISHABLE file is a violation; requiring a
|
|
1154
|
-
// publishable (core) module is fine, and the loader's DYNAMIC dispatch
|
|
1155
|
-
// (path.join(dir,'routes',rk) — no literal modules/<key>/ string) is invisible here.
|
|
1156
|
-
const MODULE_PATH_IN_STRING_RE = /['"](?:[^'"]*\/)?modules\/([a-z0-9][a-z0-9-]*)\/[^'"]*['"]/g;
|
|
1157
|
-
|
|
1158
|
-
function checkCoreRequiresExcludedModule() {
|
|
1159
|
-
const { isPublishable } = require('./publish-manifest');
|
|
1160
|
-
const violations = [];
|
|
1161
|
-
const seen = new Set();
|
|
1162
|
-
const allAbs = [
|
|
1163
|
-
...['src', 'scripts'].flatMap((d) => walkJs(path.join(CORE_ROOT, d))),
|
|
1164
|
-
...walkJsAllRoots('modules'),
|
|
1165
|
-
];
|
|
1166
|
-
for (const abs of allAbs) {
|
|
1167
|
-
const relFile = rel(abs);
|
|
1168
|
-
if (seen.has(relFile)) continue;
|
|
1169
|
-
seen.add(relFile);
|
|
1170
|
-
if (!isPublishable(relFile)) continue; // only CORE (publishable) code can crash a vanilla boot
|
|
1171
|
-
for (const raw of fs.readFileSync(abs, 'utf8').split('\n')) {
|
|
1172
|
-
const trimmed = raw.trim();
|
|
1173
|
-
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue;
|
|
1174
|
-
if (!/require\s*\(/.test(raw)) continue; // only lines that actually require()
|
|
1175
|
-
let m;
|
|
1176
|
-
MODULE_PATH_IN_STRING_RE.lastIndex = 0;
|
|
1177
|
-
while ((m = MODULE_PATH_IN_STRING_RE.exec(raw))) {
|
|
1178
|
-
const key = m[1];
|
|
1179
|
-
if (isPublishable(`modules/${key}/module.json`)) continue; // requiring a CORE module is fine
|
|
1180
|
-
violations.push(`${relFile} → require(...'modules/${key}/…') pulls in an instance-EXCLUDED module — reclassify it into core (if it belongs there) or exclude ${relFile} with the feature it serves, so a fresh core boots (task 2135).`);
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
return {
|
|
1185
|
-
name: 'core never requires an excluded module (task 2135)',
|
|
1186
|
-
ok: violations.length === 0,
|
|
1187
|
-
hardFail: violations.length > 0,
|
|
1188
|
-
violations,
|
|
1189
|
-
warnings: [],
|
|
1190
|
-
note: "a publishable src/·scripts/·modules/ .js must not require('…modules/<key>/…') when that module is boundary-excluded — MODULE_NOT_FOUND post-split (R89).",
|
|
1191
|
-
};
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
// ============================================================================
|
|
1195
|
-
// Check 17 — no route hand-rolls a FLAT error (BV1 R03 / task 1990 / ADR 0116)
|
|
1196
|
-
// ============================================================================
|
|
1197
|
-
//
|
|
1198
|
-
// The error-envelope contract (ADR 0116) says every failure is the ONE envelope
|
|
1199
|
-
// `{error:{code,message,details}}`, emitted via res.fail() or a thrown ApiError
|
|
1200
|
-
// (the R02 middleware, task 1989). A hand-rolled flat error —
|
|
1201
|
-
// `res.status(n).json({ error: 'x' })` or `.json({ error })` — breaks the
|
|
1202
|
-
// contract: the caller gets a bare string where the envelope should be.
|
|
1203
|
-
//
|
|
1204
|
-
// RATCHET, not big-bang. The R04 sweep (task 1991) migrates the ~562 existing
|
|
1205
|
-
// sites; until it finishes they'd all "fail" a strict check, so this HOLDS THE
|
|
1206
|
-
// LINE: it fails only when the count rises ABOVE the committed baseline (a NEW
|
|
1207
|
-
// hand-rolled flat error), and nudges the baseline DOWN as the sweep shrinks it.
|
|
1208
|
-
// When the sweep reaches 0, FLAT_ERROR_BASELINE is 0 and any new flat error
|
|
1209
|
-
// hard-fails immediately. Detector is shared with the sweep + its test.
|
|
1210
|
-
const FLAT_ERROR_BASELINE = 0; // R04 sweep (task 1991) landed the surface at 0 — the gate is now hard-zero: any new hand-rolled flat error fails the build immediately.
|
|
1211
|
-
// A flat error is a `.json({ … })` whose FIRST key is `error` with a value that
|
|
1212
|
-
// is NOT an object literal (a string/var/shorthand). The envelope's nested
|
|
1213
|
-
// `error: { … }` and `.json(envelope(…))` deliberately do NOT match.
|
|
1214
|
-
// The `(?!\s*\{)` lookahead is whitespace-aware: without the inner `\s*` the
|
|
1215
|
-
// preceding `\s*` backtracks to zero and the lookahead is tested at the space in
|
|
1216
|
-
// `error: {`, so the compliant nested envelope was wrongly flagged (task 2029).
|
|
1217
|
-
const FLAT_ERROR_VALUE_RE = /\.json\(\s*\{\s*error\s*:\s*(?!\s*\{)/g; // error: 'x' | error: code
|
|
1218
|
-
const FLAT_ERROR_SHORTHAND_RE = /\.json\(\s*\{\s*error\s*[,}]/g; // { error } | { error, … }
|
|
1219
|
-
|
|
1220
|
-
// Strip // line comments + /* */ block comments AND blank out string/template
|
|
1221
|
-
// contents (keeping the delimiters) so neither a DOC example of the anti-pattern
|
|
1222
|
-
// — e.g. the res.fail() usage note in middleware/error-envelope.js — nor a
|
|
1223
|
-
// pattern quoted inside a string literal is counted as a real flat error once
|
|
1224
|
-
// the gate is hard-zero (task 1991). Real flat errors are unaffected: in
|
|
1225
|
-
// `res.status(404).json({ error: 'x' })` the `.json({ error:` the detector keys
|
|
1226
|
-
// on is CODE (outside the string), so blanking the `'x'` content still matches.
|
|
1227
|
-
function stripCommentsForFlat(src) {
|
|
1228
|
-
let out = '';
|
|
1229
|
-
let i = 0;
|
|
1230
|
-
let quote = null;
|
|
1231
|
-
while (i < src.length) {
|
|
1232
|
-
const c = src[i];
|
|
1233
|
-
const d = src[i + 1];
|
|
1234
|
-
if (quote) {
|
|
1235
|
-
// inside a string/template: drop content, keep only the closing delimiter
|
|
1236
|
-
if (c === '\\') { i += 2; continue; }
|
|
1237
|
-
if (c === quote) { out += c; quote = null; }
|
|
1238
|
-
i++; continue;
|
|
1239
|
-
}
|
|
1240
|
-
if (c === '"' || c === "'" || c === '`') { quote = c; out += c; i++; continue; }
|
|
1241
|
-
if (c === '/' && d === '/') { while (i < src.length && src[i] !== '\n') i++; continue; }
|
|
1242
|
-
if (c === '/' && d === '*') { i += 2; while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) i++; i += 2; continue; }
|
|
1243
|
-
out += c; i++;
|
|
1244
|
-
}
|
|
1245
|
-
return out;
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
function countFlatErrors(text) {
|
|
1249
|
-
const code = stripCommentsForFlat(text);
|
|
1250
|
-
return (code.match(FLAT_ERROR_VALUE_RE) || []).length
|
|
1251
|
-
+ (code.match(FLAT_ERROR_SHORTHAND_RE) || []).length;
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
// scanFlatErrorSites() — per-file flat-error counts across the API surface
|
|
1255
|
-
// (core src/bongos + every module), skipping tests. Shared by the check below, the
|
|
1256
|
-
// R04 codemod, and tests/api_error_envelope_buildcheck.mjs.
|
|
1257
|
-
function scanFlatErrorSites() {
|
|
1258
|
-
const seen = new Set();
|
|
1259
|
-
const byFile = {};
|
|
1260
|
-
let total = 0;
|
|
1261
|
-
const files = [
|
|
1262
|
-
...walkJs(path.join(CORE_ROOT, 'src', 'bongos')),
|
|
1263
|
-
...walkJsAllRoots('modules'),
|
|
1264
|
-
];
|
|
1265
|
-
for (const abs of files) {
|
|
1266
|
-
const relFile = rel(abs);
|
|
1267
|
-
if (seen.has(relFile)) continue;
|
|
1268
|
-
seen.add(relFile);
|
|
1269
|
-
if (relFile.includes('/tests/') || relFile.endsWith('.test.js')) continue;
|
|
1270
|
-
const n = countFlatErrors(fs.readFileSync(abs, 'utf8'));
|
|
1271
|
-
if (n) { byFile[relFile] = n; total += n; }
|
|
1272
|
-
}
|
|
1273
|
-
return { total, byFile };
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
function checkErrorEnvelope() {
|
|
1277
|
-
const { total, byFile } = scanFlatErrorSites();
|
|
1278
|
-
const violations = [];
|
|
1279
|
-
const warnings = [];
|
|
1280
|
-
if (total > FLAT_ERROR_BASELINE) {
|
|
1281
|
-
const top = Object.entries(byFile).sort((a, b) => b[1] - a[1]).slice(0, 8)
|
|
1282
|
-
.map(([f, n]) => `${f} (${n})`).join(', ');
|
|
1283
|
-
violations.push(
|
|
1284
|
-
`${total} hand-rolled flat-error sites — ABOVE the ${FLAT_ERROR_BASELINE} baseline. A new route emitted a flat \`res.status(n).json({error:'x'})\` instead of \`res.fail('x', n)\` / \`throw new ApiError('x')\` (ADR 0116). Top files: ${top}. Use the envelope helper (src/bongos/middleware/error-envelope.js).`
|
|
1285
|
-
);
|
|
1286
|
-
} else if (total < FLAT_ERROR_BASELINE) {
|
|
1287
|
-
warnings.push(
|
|
1288
|
-
`${total} flat-error sites — BELOW the ${FLAT_ERROR_BASELINE} baseline. The sweep tightened; lower FLAT_ERROR_BASELINE to ${total} in fitness.js so the ratchet can't slip back (BV1 R04 / task 1991).`
|
|
1289
|
-
);
|
|
1290
|
-
}
|
|
1291
|
-
return {
|
|
1292
|
-
name: 'no route hand-rolls a flat error (ADR 0116 / R03)',
|
|
1293
|
-
ok: violations.length === 0,
|
|
1294
|
-
hardFail: violations.length > 0,
|
|
1295
|
-
violations,
|
|
1296
|
-
warnings,
|
|
1297
|
-
note: `≤ ${FLAT_ERROR_BASELINE} flat \`.json({error:'…'})\` sites (ratchet → 0 as the R04 sweep lands); currently ${total}. New failures must use res.fail()/ApiError (the R02 envelope).`,
|
|
1298
|
-
};
|
|
1299
|
-
}
|
|
1300
1089
|
|
|
1301
1090
|
// ============================================================================
|
|
1302
1091
|
// Check 2c — every SKILL.md frontmatter parses the way Claude Code's YAML reader reads it (task 1003548)
|
package/src/bongos/optimizer.js
CHANGED
|
@@ -34,6 +34,24 @@ const { pool } = require('./pool');
|
|
|
34
34
|
// claimTask / claimTasksBatch will actually allow.
|
|
35
35
|
const { touchesOverlap } = require('./path-match');
|
|
36
36
|
|
|
37
|
+
// The blocker guard, once (task 1003913). Same predicate as migration core_239
|
|
38
|
+
// and as db-tasks.js's OPEN_BLOCKER_GUARD, deliberately duplicated ACROSS those
|
|
39
|
+
// files — this is core, db-tasks.js is a module, and core may not require module
|
|
40
|
+
// code (ADR 0083). Within THIS file there is no such excuse, so the two inlined
|
|
41
|
+
// view bodies below interpolate one constant.
|
|
42
|
+
//
|
|
43
|
+
// The LEFT JOIN is load-bearing: a legacy markdown-anchor link carries
|
|
44
|
+
// blocker_ref with no blockers row, and an inner join would drop it from the
|
|
45
|
+
// NOT EXISTS and silently unhide it. b.id IS NULL is the fail-closed arm.
|
|
46
|
+
const OPEN_BLOCKER_GUARD = `
|
|
47
|
+
AND NOT EXISTS (
|
|
48
|
+
SELECT 1
|
|
49
|
+
FROM task_blockers tb
|
|
50
|
+
LEFT JOIN blockers b ON b.id = tb.blocker_id
|
|
51
|
+
WHERE tb.task_id = t.id
|
|
52
|
+
AND (b.id IS NULL OR b.status = 'open')
|
|
53
|
+
)`;
|
|
54
|
+
|
|
37
55
|
const VALID_MODES = ['active', 'check-in', 'autonomous'];
|
|
38
56
|
const DEFAULT_MAX_PARALLEL = 3;
|
|
39
57
|
|
|
@@ -58,6 +76,13 @@ async function optimizeSession({ builderId, mode, timeBudgetMin, maxParallel })
|
|
|
58
76
|
// backend queued an ACCESS EXCLUSIVE DDL on the views and parked every view
|
|
59
77
|
// SELECT behind it. Plan-identical (Postgres inlines simple views); change in
|
|
60
78
|
// lockstep with the views / listClaimableTasks if a migration alters them.
|
|
79
|
+
//
|
|
80
|
+
// THAT LOCKSTEP FAILED ONCE, WHICH IS WHY A TEST NOW HOLDS IT (task 1003913).
|
|
81
|
+
// Migration core_239 added the blocker-STATUS guard to claimable_tasks and both
|
|
82
|
+
// bodies below kept the old "any link row hides it forever" form, so a task
|
|
83
|
+
// whose blocker had been resolved stayed invisible here. A comment asking the
|
|
84
|
+
// next editor to remember is not enforcement: tests/claimable_blocker_status.mjs
|
|
85
|
+
// now asserts this file, db-tasks.js and the migration carry the same predicate.
|
|
61
86
|
const { rows } = await pool.query(
|
|
62
87
|
`SELECT ct.*, sf.fits_session_modes, sf.credits_per_minute
|
|
63
88
|
FROM (
|
|
@@ -65,7 +90,7 @@ async function optimizeSession({ builderId, mode, timeBudgetMin, maxParallel })
|
|
|
65
90
|
FROM tasks t
|
|
66
91
|
WHERE t.status = 'ready'
|
|
67
92
|
AND t.title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'
|
|
68
|
-
|
|
93
|
+
${OPEN_BLOCKER_GUARD}
|
|
69
94
|
AND NOT EXISTS (
|
|
70
95
|
SELECT 1 FROM claims c
|
|
71
96
|
JOIN tasks t2 ON t2.id = c.task_id
|
|
@@ -76,8 +101,7 @@ async function optimizeSession({ builderId, mode, timeBudgetMin, maxParallel })
|
|
|
76
101
|
JOIN (
|
|
77
102
|
SELECT t.id,
|
|
78
103
|
CASE
|
|
79
|
-
WHEN t.automation_tag = 'overnight-safe' AND COALESCE(t.manual_degree, 5) <= 2
|
|
80
|
-
AND NOT EXISTS (SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id)
|
|
104
|
+
WHEN t.automation_tag = 'overnight-safe' AND COALESCE(t.manual_degree, 5) <= 2${OPEN_BLOCKER_GUARD}
|
|
81
105
|
THEN ARRAY['autonomous','check-in','active']
|
|
82
106
|
WHEN t.automation_tag = 'live-only' OR COALESCE(t.manual_degree, 5) >= 4
|
|
83
107
|
THEN ARRAY['active']
|
|
@@ -492,6 +492,35 @@ function isBuildersHost(req) {
|
|
|
492
492
|
return cfg !== '' && host === cfg;
|
|
493
493
|
}
|
|
494
494
|
|
|
495
|
+
// Is this instance served entirely from ONE host — publicOrigin and buildersOrigin
|
|
496
|
+
// the same name (task 1002861)? Then "the builders host" and "the apex" are the same
|
|
497
|
+
// request, isBuildersHost() is true for everything, and the builders rewrite shim has
|
|
498
|
+
// no separate subdomain to root the hall at. Surfaces that only ever live on the apex
|
|
499
|
+
// must therefore be handed through it rather than rewritten under /builders.
|
|
500
|
+
//
|
|
501
|
+
// Compares HOSTS, not origin strings: http vs https, a port, or a trailing slash are
|
|
502
|
+
// all routing-irrelevant here and would each give a false negative on a string compare.
|
|
503
|
+
// Empty on either side is NOT single-host — an unconfigured origin must not silently
|
|
504
|
+
// widen what the shim lets past.
|
|
505
|
+
function singleHostInstance() {
|
|
506
|
+
let pub = '';
|
|
507
|
+
try {
|
|
508
|
+
const po = (branding().domains || {}).publicOrigin || '';
|
|
509
|
+
pub = po ? new URL(po).host.toLowerCase().split(':')[0] : '';
|
|
510
|
+
} catch { return false; }
|
|
511
|
+
const hall = configuredBuildersHost();
|
|
512
|
+
return pub !== '' && hall !== '' && pub === hall;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Paths that exist ONLY on the apex and are registered BELOW the builders rewrite
|
|
516
|
+
// shim. Kept as one named predicate so the shim and its audit stay in one place: a
|
|
517
|
+
// future apex-only surface added below the shim belongs here the day it is mounted.
|
|
518
|
+
// `/modules/*` is deliberately absent — it serves on EVERY host and the shim exempts
|
|
519
|
+
// it unconditionally, which is a different rule, not an entry in this list.
|
|
520
|
+
function isApexOnlyPath(p) {
|
|
521
|
+
return p === '/docs' || p.startsWith('/docs/');
|
|
522
|
+
}
|
|
523
|
+
|
|
495
524
|
// Mount the internal web surfaces onto `app`, in the exact order server.js used.
|
|
496
525
|
// Call this ONCE, right after app setup (trust proxy etc.) and BEFORE any
|
|
497
526
|
// entrypoint-specific tail (the game static + themed 404 in server.js; the plain
|
|
@@ -837,9 +866,34 @@ function mountInternalSurfaces(app) {
|
|
|
837
866
|
// lets the request reach it.
|
|
838
867
|
//
|
|
839
868
|
// The general hazard: ANY apex-rooted route registered BELOW this shim is
|
|
840
|
-
// eaten the same way.
|
|
841
|
-
//
|
|
869
|
+
// eaten the same way. The audit for task 1002861 walked every registration
|
|
870
|
+
// below this point: the complete set is this one and the /docs family
|
|
871
|
+
// immediately after it. Everything else down there is already /builders-scoped
|
|
872
|
+
// and passes through untouched, and /downloads/* dodges the problem entirely
|
|
873
|
+
// by being registered ABOVE the shim (see the note at its own mount).
|
|
842
874
|
if (req.path.startsWith('/modules/')) return next();
|
|
875
|
+
// /docs is APEX-ONLY, which makes it the opposite case to /modules above, and
|
|
876
|
+
// the exemption has to be narrower because of it (task 1002861).
|
|
877
|
+
//
|
|
878
|
+
// On a SINGLE-HOST instance — publicOrigin === buildersOrigin, e.g. a
|
|
879
|
+
// demo.<apex> standalone, or any instance whose free-plan TLS can't cover a
|
|
880
|
+
// two-level builders.<sub>.<apex> — isBuildersHost() is true for the apex
|
|
881
|
+
// itself (task 1001971 taught it the configured buildersOrigin). So this shim
|
|
882
|
+
// fires on every apex request and rewrote /docs to /builders/docs, which no
|
|
883
|
+
// handler matches: GET /docs 404'd at the edge while loopback /docs returned
|
|
884
|
+
// 200, because a loopback Host matches neither origin. That asymmetry is the
|
|
885
|
+
// whole bug, and it is why it read as an edge/proxy fault for so long.
|
|
886
|
+
//
|
|
887
|
+
// Deliberately NOT a blanket exemption. On a TWO-host instance the builders
|
|
888
|
+
// subdomain is not supposed to serve /docs at all — the handler's own comment
|
|
889
|
+
// says the module hosts skip it — and today the only thing actually enforcing
|
|
890
|
+
// that is this shim, since isModuleSurfaceHost excludes the builders host by
|
|
891
|
+
// construction (the hall is partitioned out of webSurfaces). Exempting /docs
|
|
892
|
+
// unconditionally would therefore START serving it on builders.<apex>: a
|
|
893
|
+
// behaviour change nobody asked for, smuggled in under a 404 fix. Gating on
|
|
894
|
+
// singleHostInstance() fixes the standalone and leaves two-host routing
|
|
895
|
+
// byte-identical.
|
|
896
|
+
if (singleHostInstance() && isApexOnlyPath(req.path)) return next();
|
|
843
897
|
if (req.path === '/builders' || req.path.startsWith('/builders/')) return next();
|
|
844
898
|
// Prefix with /builders so the handlers below match. req.url carries the
|
|
845
899
|
// query string; preserve it. '/' → '/builders/', '/watch' → '/builders/watch'.
|
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.757'; // 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');
|
|
@@ -95,4 +95,110 @@ await test('the feed and the claim path enforce the SAME rule', () => {
|
|
|
95
95
|
'the by-id claim path checks blocker STATUS, and the view must not be laxer or stricter');
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
/* ───────────────────────────────────────────────────────────────────────────
|
|
99
|
+
Task 1003913 — the migration was never the only copy.
|
|
100
|
+
|
|
101
|
+
core_239 fixed claimable_tasks and the feed did not move, because the same
|
|
102
|
+
predicate is written in FOUR more places that no migration touches:
|
|
103
|
+
CLAIMABLE_SQL and SESSION_FIT_SQL in modules/lifecycle/db-tasks.js, and the
|
|
104
|
+
two view bodies src/bongos/optimizer.js inlines on purpose (a lock incident,
|
|
105
|
+
task 2181). Everything above this line passed throughout that bug — the file
|
|
106
|
+
asserted the migration and then asked db-claims.js one question. So the gap
|
|
107
|
+
was never the rigour of the assertions, it was their REACH.
|
|
108
|
+
|
|
109
|
+
COMMENTS ARE STRIPPED BEFORE SCANNING. db-tasks.js documents the old broken
|
|
110
|
+
form in prose so the next reader knows what was wrong; a raw-text scan would
|
|
111
|
+
read that as the bug and red CI on a correct file. (Exactly the defect filed
|
|
112
|
+
as task 1003832 against government_require_permission.mjs.)
|
|
113
|
+
─────────────────────────────────────────────────────────────────────────── */
|
|
114
|
+
|
|
115
|
+
// Strip // line comments and /* */ blocks, then flatten whitespace, so every
|
|
116
|
+
// assertion below is about code that actually runs.
|
|
117
|
+
const code = (...p) => read(...p)
|
|
118
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
119
|
+
.replace(/^\s*\/\/.*$/gm, ' ')
|
|
120
|
+
.replace(/\s+/g, ' ');
|
|
121
|
+
|
|
122
|
+
// The bug, as a pattern: a task_blockers existence check that never looks at the
|
|
123
|
+
// blocker. This is the exact clause core_239 replaced.
|
|
124
|
+
//
|
|
125
|
+
// THE OPTIONAL SPACES AFTER '(' AND BEFORE ')' ARE NOT COSMETIC. The migration
|
|
126
|
+
// writes the clause across lines, so normalising collapses it to "( SELECT"; the
|
|
127
|
+
// JS copies write it on one line as "(SELECT". A pattern pinned to the
|
|
128
|
+
// migration's spacing matched neither JS copy, so this tripwire passed for the
|
|
129
|
+
// whole life of the bug it exists to catch, and only the COUNT assertion below
|
|
130
|
+
// failed when the fix was reverted. Caught by mutating db-tasks.js:791 rather
|
|
131
|
+
// than by reading it.
|
|
132
|
+
const UNGUARDED = /NOT EXISTS \(\s?SELECT 1 FROM task_blockers tb WHERE tb\.task_id = t\.id\s?\)/;
|
|
133
|
+
// The fix, as a pattern.
|
|
134
|
+
const GUARDED = /FROM task_blockers tb LEFT JOIN blockers b ON b\.id = tb\.blocker_id WHERE tb\.task_id = t\.id AND \(b\.id IS NULL OR b\.status = 'open'\)/g;
|
|
135
|
+
|
|
136
|
+
const RUNTIME = [
|
|
137
|
+
['modules', 'lifecycle', 'db-tasks.js'],
|
|
138
|
+
['src', 'bongos', 'optimizer.js'],
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
await test('no RUNNING query still carries the unguarded clause', () => {
|
|
142
|
+
// The tripwire that would have caught core_239's miss on the day it landed.
|
|
143
|
+
for (const f of RUNTIME) {
|
|
144
|
+
assert.doesNotMatch(code(...f), UNGUARDED,
|
|
145
|
+
`${f.join('/')} hides a task because a link row EXISTS, not because its blocker is open — ` +
|
|
146
|
+
'the bug core_239 fixed in the view and this copy kept');
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await test('each file declares the guard ONCE and every query it serves USES it', () => {
|
|
151
|
+
// The assertion this file needed and did not have. Round 1 of this task
|
|
152
|
+
// declared OPEN_BLOCKER_GUARD in db-tasks.js, interpolated it into
|
|
153
|
+
// CLAIMABLE_SQL, and then hand-copied the same clause into SESSION_FIT_SQL —
|
|
154
|
+
// two spellings of one predicate, inside the file written to remove exactly
|
|
155
|
+
// that. The old check was assert.match on /\$\{OPEN_BLOCKER_GUARD\}/, which is
|
|
156
|
+
// non-global and therefore satisfied by ONE use. Counting is the whole point.
|
|
157
|
+
for (const f of RUNTIME) {
|
|
158
|
+
const src = code(...f);
|
|
159
|
+
const decls = (src.match(/const OPEN_BLOCKER_GUARD = /g) || []).length;
|
|
160
|
+
const uses = (src.match(/\$\{OPEN_BLOCKER_GUARD\}/g) || []).length;
|
|
161
|
+
assert.equal(decls, 1, `${f.join('/')} must declare the guard exactly once`);
|
|
162
|
+
assert.equal(uses, 2, `${f.join('/')} serves two queries and both must interpolate the guard, not re-type it`);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await test('the declared guard is core_239-s predicate, verbatim', () => {
|
|
167
|
+
// One declaration per file, so the files agree with the migration iff each
|
|
168
|
+
// DECLARATION does. optimizer.js cannot import db-tasks.js across the ADR 0083
|
|
169
|
+
// boundary, so nothing but this comparison holds the three together.
|
|
170
|
+
const fromMigration = (body.match(GUARDED) || [])[0];
|
|
171
|
+
assert.ok(fromMigration, 'core_239 still carries the predicate this pins everything else to');
|
|
172
|
+
for (const f of RUNTIME) {
|
|
173
|
+
const decl = (code(...f).match(/const OPEN_BLOCKER_GUARD = `[^`]*`/) || [])[0];
|
|
174
|
+
assert.ok(decl, `${f.join('/')} declares the guard as a template literal`);
|
|
175
|
+
assert.ok(decl.includes(fromMigration),
|
|
176
|
+
`${f.join('/')}'s guard must be core_239's predicate verbatim — a paraphrase is how the next one drifts`);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await test('the guard actually reaches both composed statements', () => {
|
|
181
|
+
// Counting interpolations proves the constant is referenced twice; this proves
|
|
182
|
+
// what those references EXPAND to, which is what Postgres receives.
|
|
183
|
+
for (const f of RUNTIME) {
|
|
184
|
+
const raw = read(...f);
|
|
185
|
+
const gm = raw.match(/const OPEN_BLOCKER_GUARD = `([\s\S]*?)`;/);
|
|
186
|
+
assert.ok(gm, `${f.join('/')} declares the guard`);
|
|
187
|
+
const expanded = raw.split('\${OPEN_BLOCKER_GUARD}').join(gm[1]).replace(/\s+/g, ' ');
|
|
188
|
+
const hits = (expanded.match(GUARDED) || []).length;
|
|
189
|
+
assert.equal(hits, 3, `${f.join('/')}: one declaration plus two expanded uses`);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await test('the promotion sweep keeps its own, deliberately different, guard', () => {
|
|
194
|
+
// PROMOTABLE_BACKLOG_WHERE uses an INNER join + b.status = 'open' on purpose:
|
|
195
|
+
// it promotes backlog->ready and an unresolvable legacy link must not block
|
|
196
|
+
// that. Asserted so the sweep above does not "tidy" it into the LEFT form and
|
|
197
|
+
// quietly change which tasks get promoted.
|
|
198
|
+
const raw = code('modules', 'lifecycle', 'db-tasks.js');
|
|
199
|
+
assert.match(raw, /PROMOTABLE_BACKLOG_WHERE/);
|
|
200
|
+
assert.match(raw, /FROM task_blockers tb JOIN blockers b ON b\.id = tb\.blocker_id WHERE tb\.task_id = t\.id AND b\.status = 'open'/,
|
|
201
|
+
'the sweep resolves the blocker too — it just answers the legacy-link case differently');
|
|
202
|
+
});
|
|
203
|
+
|
|
98
204
|
summary();
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// tests/single_host_apex_routes.mjs — apex-only routes must survive the builders
|
|
2
|
+
// rewrite shim on a SINGLE-HOST instance (task 1002861).
|
|
3
|
+
//
|
|
4
|
+
// THE BUG. On a single-host standalone (publicOrigin === buildersOrigin, e.g.
|
|
5
|
+
// demo.cloudbongos.com, or any instance whose free-plan TLS cannot cover a
|
|
6
|
+
// two-level builders.<sub>.<apex>) GET /docs returned 404 at the public edge while
|
|
7
|
+
// loopback /docs returned 200 and the files plainly shipped. Since task 1001971
|
|
8
|
+
// isBuildersHost() also returns true when the request host equals the CONFIGURED
|
|
9
|
+
// buildersOrigin — which on a single-host instance is the apex — so the builders
|
|
10
|
+
// rewrite shim fired on every apex request and rewrote /docs to /builders/docs,
|
|
11
|
+
// which no handler matches. A loopback Host matches neither origin, so loopback
|
|
12
|
+
// skipped the shim entirely: that asymmetry is why this read as an edge/proxy
|
|
13
|
+
// fault for months rather than an application routing bug.
|
|
14
|
+
//
|
|
15
|
+
// WHAT THIS PINS, AND WHY IT NEEDS TWO PROCESSES. The fix is deliberately NARROW:
|
|
16
|
+
// the shim hands apex-only paths through only when the instance is single-host. On
|
|
17
|
+
// a TWO-host instance the builders subdomain still must not serve /docs, and today
|
|
18
|
+
// this shim is the only thing enforcing that (isModuleSurfaceHost excludes the
|
|
19
|
+
// builders host by construction, since the hall is partitioned out of webSurfaces).
|
|
20
|
+
// A blanket exemption would quietly START serving /docs on builders.<apex>. Both
|
|
21
|
+
// shapes are therefore asserted, and the instance shape is read from branding at
|
|
22
|
+
// require time, so each shape needs its own process with its own env.
|
|
23
|
+
//
|
|
24
|
+
// Uses node:http, NOT fetch: `Host` is a forbidden header name in the fetch spec,
|
|
25
|
+
// so fetch silently drops the override and every request goes to 127.0.0.1 —
|
|
26
|
+
// which makes a host-routing test pass vacuously and keep passing with the fix
|
|
27
|
+
// reverted (learning 1000264, found the hard way in task 1002848).
|
|
28
|
+
//
|
|
29
|
+
// Run: node --test tests/single_host_apex_routes.mjs
|
|
30
|
+
|
|
31
|
+
import { test, after } from 'node:test';
|
|
32
|
+
import assert from 'node:assert/strict';
|
|
33
|
+
import http from 'node:http';
|
|
34
|
+
import { spawn } from 'node:child_process';
|
|
35
|
+
import path from 'node:path';
|
|
36
|
+
import { fileURLToPath } from 'node:url';
|
|
37
|
+
|
|
38
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
39
|
+
|
|
40
|
+
// Boot serve-internal in a child process so branding() picks up this shape's env.
|
|
41
|
+
// Prints "PORT <n>" and stays up until killed.
|
|
42
|
+
// `express` is required BARE on purpose: a worktree has no node_modules of its own
|
|
43
|
+
// and resolves by walking up to the main checkout, so an absolute path built from
|
|
44
|
+
// ROOT would miss. cwd is ROOT below, which is what that walk-up starts from.
|
|
45
|
+
const SERVER_SRC = `
|
|
46
|
+
const express = require('express');
|
|
47
|
+
const { mountInternalSurfaces } = require(${JSON.stringify(path.join(ROOT, 'src/bongos/serve-internal.js'))});
|
|
48
|
+
const app = express();
|
|
49
|
+
mountInternalSurfaces(app);
|
|
50
|
+
const s = app.listen(0, () => console.log('PORT ' + s.address().port));
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const servers = [];
|
|
54
|
+
|
|
55
|
+
function startServer(env) {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const child = spawn(process.execPath, ['-e', SERVER_SRC], {
|
|
58
|
+
cwd: ROOT,
|
|
59
|
+
env: { ...process.env, ...env },
|
|
60
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
61
|
+
});
|
|
62
|
+
servers.push(child);
|
|
63
|
+
let out = '';
|
|
64
|
+
let err = '';
|
|
65
|
+
const timer = setTimeout(() => reject(new Error(`server did not start; stderr: ${err}`)), 30000);
|
|
66
|
+
child.stdout.setEncoding('utf8');
|
|
67
|
+
child.stderr.setEncoding('utf8');
|
|
68
|
+
child.stderr.on('data', (c) => { err += c; });
|
|
69
|
+
child.stdout.on('data', (c) => {
|
|
70
|
+
out += c;
|
|
71
|
+
const m = out.match(/PORT (\d+)/);
|
|
72
|
+
if (m) { clearTimeout(timer); resolve(Number(m[1])); }
|
|
73
|
+
});
|
|
74
|
+
child.on('error', (e) => { clearTimeout(timer); reject(e); });
|
|
75
|
+
child.on('exit', (code) => {
|
|
76
|
+
if (!/PORT \d+/.test(out)) { clearTimeout(timer); reject(new Error(`server exited ${code}: ${err}`)); }
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
after(() => { for (const c of servers) { try { c.kill(); } catch { /* already gone */ } } });
|
|
82
|
+
|
|
83
|
+
function get(port, p, host) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const req = http.request({ host: '127.0.0.1', port, path: p, headers: { Host: host } }, (res) => {
|
|
86
|
+
let body = '';
|
|
87
|
+
res.setEncoding('utf8');
|
|
88
|
+
res.on('data', (c) => { body += c; });
|
|
89
|
+
res.on('end', () => resolve({ status: res.statusCode, body, headers: res.headers }));
|
|
90
|
+
});
|
|
91
|
+
req.on('error', reject);
|
|
92
|
+
req.end();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const SINGLE = { BONGOS_PUBLIC_ORIGIN: 'https://demo.example.com', BONGOS_BUILDERS_ORIGIN: 'https://demo.example.com' };
|
|
97
|
+
const TWO = { BONGOS_PUBLIC_ORIGIN: 'https://example.com', BONGOS_BUILDERS_ORIGIN: 'https://builders.example.com' };
|
|
98
|
+
|
|
99
|
+
let singlePort;
|
|
100
|
+
let twoPort;
|
|
101
|
+
|
|
102
|
+
test('boot both instance shapes', async () => {
|
|
103
|
+
[singlePort, twoPort] = await Promise.all([startServer(SINGLE), startServer(TWO)]);
|
|
104
|
+
assert.ok(singlePort > 0 && twoPort > 0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// The harness is only meaningful if the Host header actually reaches the app.
|
|
108
|
+
// Assert that first — otherwise every case below is an apex-vs-apex tautology.
|
|
109
|
+
test('the harness really varies the Host', async () => {
|
|
110
|
+
const a = await get(twoPort, '/builders/', 'example.com');
|
|
111
|
+
const b = await get(twoPort, '/builders/', 'builders.example.com');
|
|
112
|
+
assert.notEqual(a.headers.location, b.headers.location,
|
|
113
|
+
'the two hosts must route differently, or the Host header is being dropped');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ---- single-host: the bug ------------------------------------------------
|
|
117
|
+
|
|
118
|
+
test('SINGLE-HOST: /docs serves on the apex (it used to 404 — the shim ate it)', async () => {
|
|
119
|
+
const r = await get(singlePort, '/docs', 'demo.example.com');
|
|
120
|
+
assert.equal(r.status, 200);
|
|
121
|
+
assert.ok(/redoc|openapi/i.test(r.body), 'the body is the API docs page, not some other handler');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('SINGLE-HOST: the rest of the /docs family serves too', async () => {
|
|
125
|
+
for (const p of ['/docs/openapi.json', '/docs/redoc.standalone.js']) {
|
|
126
|
+
const r = await get(singlePort, p, 'demo.example.com');
|
|
127
|
+
assert.equal(r.status, 200, `${p} must serve on a single-host apex`);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('SINGLE-HOST: /docs/openapi.json is still the spec, not a rewritten page', async () => {
|
|
132
|
+
const r = await get(singlePort, '/docs/openapi.json', 'demo.example.com');
|
|
133
|
+
const spec = JSON.parse(r.body);
|
|
134
|
+
assert.ok(spec.openapi.startsWith('3.'), 'a real OpenAPI document');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('SINGLE-HOST: the hall still works — the exemption did not disable the shim', async () => {
|
|
138
|
+
// The whole point of the shim on this host is that / serves the hall. If the
|
|
139
|
+
// exemption were written too broadly it would stop rewriting everything.
|
|
140
|
+
const r = await get(singlePort, '/', 'demo.example.com');
|
|
141
|
+
assert.ok(r.status === 200 || r.status === 301 || r.status === 302,
|
|
142
|
+
`the hall root must still resolve (got ${r.status})`);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('SINGLE-HOST: a /docs lookalike is NOT exempt', async () => {
|
|
146
|
+
// '/docsomething' must not escape the shim — it is neither /docs nor under /docs/.
|
|
147
|
+
const r = await get(singlePort, '/docsomething', 'demo.example.com');
|
|
148
|
+
assert.notEqual(r.status, 200, 'only /docs and /docs/* are apex-only exemptions');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ---- two-host: unchanged -------------------------------------------------
|
|
152
|
+
|
|
153
|
+
test('TWO-HOST: /docs serves on the apex, as it always did', async () => {
|
|
154
|
+
const r = await get(twoPort, '/docs', 'example.com');
|
|
155
|
+
assert.equal(r.status, 200);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('TWO-HOST: /docs still does NOT serve on builders.<apex> — no smuggled change', async () => {
|
|
159
|
+
const r = await get(twoPort, '/docs', 'builders.example.com');
|
|
160
|
+
assert.equal(r.status, 404,
|
|
161
|
+
'a blanket exemption would have started serving /docs here; the fix is gated on single-host');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('TWO-HOST: module assets still serve on BOTH hosts (task 1002848 stays fixed)', async () => {
|
|
165
|
+
// /modules/ is exempt unconditionally — a different rule from the apex-only list,
|
|
166
|
+
// and this pins that the two rules did not get conflated.
|
|
167
|
+
const p = '/modules/hall-ui/public/atlas.css';
|
|
168
|
+
assert.equal((await get(twoPort, p, 'example.com')).status, 200);
|
|
169
|
+
assert.equal((await get(twoPort, p, 'builders.example.com')).status, 200);
|
|
170
|
+
});
|