@bongos/core 1.19.332 → 1.19.334
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 +173 -43
- package/docs/adr/0187-oauth-handshake-cookie-lifetime.md +84 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +2 -2
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +202 -202
- package/docs/copy-registry.json +384 -384
- package/modules/grading/grader-config.js +98 -0
- package/modules/grading/grader-model.js +150 -0
- package/modules/grading/grader-prompt.js +139 -0
- package/modules/grading/grader-score.js +706 -0
- package/modules/grading/grader-subagent.js +328 -0
- package/modules/grading/grader.js +5 -1287
- package/modules/hall-ui/public/builders.js +19 -1182
- package/modules/hall-ui/public/goals-page.js +11 -997
- package/modules/hall-ui/public/goals-render.js +1032 -0
- package/modules/hall-ui/public/goals.html +4 -0
- package/modules/hall-ui/public/hall-render.js +1211 -0
- package/modules/hall-ui/public/index.html +6 -0
- package/modules/lifecycle/routes/goal-route-authz.js +400 -0
- package/modules/lifecycle/routes/goals.js +1 -367
- package/modules/lifecycle/routes/task-route-helpers.js +219 -0
- package/modules/lifecycle/routes/task-write-routes.js +542 -0
- package/modules/lifecycle/routes/tasks.js +3 -696
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/box-infra.js +280 -0
- package/scripts/gds/box.js +1 -245
- package/scripts/gds/fitness-checks-identity.js +320 -0
- package/scripts/gds/fitness-checks-vocabulary.js +230 -0
- package/scripts/gds/fitness-checks-write-validation.js +144 -0
- package/scripts/gds/fitness-lib.js +82 -0
- package/scripts/gds/fitness.js +55 -668
- package/scripts/gds/provision-config.js +204 -0
- package/scripts/gds/provision-net.js +221 -0
- package/scripts/gds/provision-repo.js +849 -0
- package/scripts/gds/provision-units.js +262 -0
- package/scripts/gds/provision.js +4 -1395
- package/src/bongos/auth-admission.js +264 -0
- package/src/bongos/auth-config.js +151 -0
- package/src/bongos/auth-discord.js +198 -0
- package/src/bongos/auth-github.js +336 -0
- package/src/bongos/auth-idp.js +330 -0
- package/src/bongos/auth.js +5 -1100
- package/src/bongos/routes/auth.js +54 -10
- package/src/module-api.js +5 -1
- package/tests/auth_admission_gate.mjs +3 -1
- package/tests/auth_web_recovery.mjs +63 -0
- package/tests/est_advisory.mjs +3 -1
- package/tests/goal_advisory.mjs +3 -2
- package/tests/goal_criterion_confirm_ui.mjs +6 -0
- package/tests/goal_subtitle.mjs +5 -2
- package/tests/goals_card_view.mjs +10 -3
- package/tests/grader_generated_provenance.mjs +3 -1
- package/tests/hall_palette.mjs +3 -1
- package/tests/helpers.mjs +58 -25
- package/tests/lib_sh_resolution.mjs +3 -1
- package/tests/membership_kind_lockstep.mjs +3 -1
- package/tests/migration_allocation.mjs +5 -2
- package/tests/patch_gate_ordering.mjs +3 -1
- package/tests/rank_tier_single_source.mjs +16 -3
- package/tests/reward_gate.mjs +3 -2
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// scripts/gds/fitness-checks-identity.js — the checks that keep the portable core
|
|
2
|
+
// free of any one host's identity: kernel prompts (BV1.R76), the neutral config
|
|
3
|
+
// packs (BV1.R102) and the design contract.
|
|
4
|
+
//
|
|
5
|
+
// Carved out of scripts/gds/fitness.js by task 1003206 (goal 1000079, the R03
|
|
6
|
+
// de-monolith plan) to bring that file under the 1,500-line ratchet. A STRUCTURAL
|
|
7
|
+
// move: the lines below are fitness.js's originals unchanged, and fitness.js
|
|
8
|
+
// imports every name straight back, so the CHECKS registry and the module's
|
|
9
|
+
// public surface (which the tests read heavily) are untouched.
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const { CORE_ROOT, MODULE_ROOTS, REPO_ROOT, rel } = require('./fitness-lib.js');
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Check 13 — portable code carries no hardcoded host-brand identity (BV1.R76 + task 1705)
|
|
19
|
+
// ============================================================================
|
|
20
|
+
//
|
|
21
|
+
// The portable core (src/bongos/**) and every feature MODULE (modules/**) must read
|
|
22
|
+
// the instance's product/world name + domains from branding() / window.__BRANDING__,
|
|
23
|
+
// never a baked-in "Off the Boats" / "Amazonprimea" / apex domain (ADR 0062 §3/§5;
|
|
24
|
+
// the branding contract). R76 shipped this as an 8-FILE allowlist over the kernel
|
|
25
|
+
// LLM-prompt headers; task 1705 WIDENS it to a directory-wide scan, so ANY new brand
|
|
26
|
+
// leak in portable code reds the build — not just the nine files someone remembered
|
|
27
|
+
// to list. (The 2026-07-02 separation audit found five leaks the old allowlist missed:
|
|
28
|
+
// the rank/graduation broadcasts, the auth error-page title, the apex-origin default,
|
|
29
|
+
// and ~124 served-UI strings.) Comments are stripped first (block /* */, HTML
|
|
30
|
+
// <!-- -->, and JS // line comments via a `://`-safe strip so a URL is never
|
|
31
|
+
// truncated) because the de-brand rationale legitimately names the old literals in
|
|
32
|
+
// prose. Instance-injected placeholders ({{worldName}} etc.) carry no literal, so
|
|
33
|
+
// they pass untouched.
|
|
34
|
+
//
|
|
35
|
+
// EXCLUDED — the small instance-1 / ops allowlist (task 1705):
|
|
36
|
+
// - modules/game/** — the INSTANCE'S OWN PRODUCT. A product's identity is
|
|
37
|
+
// instance-specific by definition (a vanilla instance ships a different product,
|
|
38
|
+
// or none); it is not portable-platform code the branding pack can neutralize.
|
|
39
|
+
// A documented carve-out, not an oversight.
|
|
40
|
+
// - generated / build-output dirs (no authored source).
|
|
41
|
+
const PROMPT_IDENTITY_LITERALS = [
|
|
42
|
+
/off the boats/i,
|
|
43
|
+
/amazonprimea/i,
|
|
44
|
+
];
|
|
45
|
+
// Scan roots (recursive) + kernel-prompt CLIs that live outside them.
|
|
46
|
+
// scripts/gds joined at task 1002427: the builder CLI was UNSCANNED, which is how
|
|
47
|
+
// paste-token.js kept a hardcoded 'https://amazonprimea.com' long after the task
|
|
48
|
+
// 1002201 sweep replaced that literal everywhere else. The straggler surfaced as a
|
|
49
|
+
// 401 that locked a builder out of re-authenticating — a valid token verified
|
|
50
|
+
// against the wrong host — and nothing in CI could see it (idea 1000564).
|
|
51
|
+
// Widened scripts/gds → ALL of scripts/ at task 1002756: migrate.sh's hardcoded
|
|
52
|
+
// founder DB default (task 1002755) survived every gate because the detector never
|
|
53
|
+
// saw the file — a gate that cannot see the file class the bug lives in is worse
|
|
54
|
+
// than no gate, because it is trusted.
|
|
55
|
+
const IDENTITY_SCAN_ROOTS = ['src/bongos', 'modules', 'scripts'];
|
|
56
|
+
const IDENTITY_SCAN_EXTRA_FILES = [];
|
|
57
|
+
// PERMANENT, COUNTED carve-outs for the scripts/gds root — {file: allowed hits}.
|
|
58
|
+
//
|
|
59
|
+
// Widening the root (task 1002427) surfaced 23 files. Task 1002428 swept them:
|
|
60
|
+
// every one whose literal was a real runtime bug is FIXED (a wrong instance URL
|
|
61
|
+
// printed to a builder, an `amazonprimea-*` User-Agent that should come from
|
|
62
|
+
// branding().userAgent(), a hardcoded product name in user-facing copy). What is
|
|
63
|
+
// left below is NOT a backlog — each entry is a literal that is CORRECT as
|
|
64
|
+
// written, so this list is permanent rather than a ratchet-to-zero.
|
|
65
|
+
//
|
|
66
|
+
// The COUNT is still enforced, which is the point: an entry may not grow. A file
|
|
67
|
+
// here that gains a NEW hardcoded host fails the build exactly like an unlisted
|
|
68
|
+
// one, and any file NOT listed must have zero hits. Getting better never fails.
|
|
69
|
+
//
|
|
70
|
+
// Why each is legitimate:
|
|
71
|
+
// * box.js — REAL Cloudflare/DNS zone names for the operator plane
|
|
72
|
+
// (`dev.<host>`, the terminal zone). These are infrastructure identifiers,
|
|
73
|
+
// not an instance URL; routing them through branding would be wrong. (Its
|
|
74
|
+
// two genuine API-base defaults WERE fixed in 1002428.)
|
|
75
|
+
// * publish-manifest.js — the literal FILENAMES of two real committed docs
|
|
76
|
+
// (docs/recipes/amazonprimea-decouple-*.md). Renaming the strings without
|
|
77
|
+
// renaming the files would break the publish manifest.
|
|
78
|
+
// * backfill-sessions.js / version-close.js — one-time operator runbook text
|
|
79
|
+
// naming the actual prod DB + a historical local directory. Describing what
|
|
80
|
+
// an operator must literally type is the correct content.
|
|
81
|
+
// * seed-*.js — the hits are PROSE INSIDE seeded task descriptions: historical
|
|
82
|
+
// records of what those tasks said when they were filed. Rewriting them would
|
|
83
|
+
// falsify the record; deleting the files would destroy it. They are inert
|
|
84
|
+
// (idempotent on source_ref) and referenced only by generated manifests.
|
|
85
|
+
// * the four *.sh entries — surfaced when `.sh` joined the extension set (task
|
|
86
|
+
// 1002567). Each is a HOST-PINNED OPS script, not portable code: they name the
|
|
87
|
+
// live box's actual Postgres database/role and its actual /etc secrets dir, and
|
|
88
|
+
// every one of those literals is already only the DEFAULT behind an env override
|
|
89
|
+
// (GDS_BACKUP_DB, PROD_DB/STAGING_DB, PG_SOCKET). Same rationale as box.js's real
|
|
90
|
+
// DNS zones — an infrastructure identifier, not an instance URL, and the systemd
|
|
91
|
+
// units on that box invoke these by path. Re-pointing an instance's backups is
|
|
92
|
+
// deployment work, not a branding read. Their COUNT is still frozen here, so a
|
|
93
|
+
// NEW literal in any of them fails exactly like one in an unlisted file.
|
|
94
|
+
// * fitness-checks-identity.js / package-core.js — THE DETECTORS THEMSELVES: Check 13's regexes
|
|
95
|
+
// and package-core's PRIOR_INSTANCE_FLOOR name the brand because hunting it is
|
|
96
|
+
// their job. * seed-v3-tasks.js — the seeded-task-prose rationale above. All
|
|
97
|
+
// three passed pre-1002756 only by the old stripper's accidents (phantom
|
|
98
|
+
// blocks + regex-blindness); now they are seen and counted.
|
|
99
|
+
const IDENTITY_SCAN_ALLOWED = {
|
|
100
|
+
// task 1003206 moved Check 13's own regexes out of fitness.js into THIS file
|
|
101
|
+
// when the monolith was split, so the detector's self-carve-out moved with them.
|
|
102
|
+
'scripts/gds/fitness-checks-identity.js': 2,
|
|
103
|
+
'scripts/gds/package-core.js': 1,
|
|
104
|
+
'scripts/gds/seed-v3-tasks.js': 14,
|
|
105
|
+
// audit-replay.sh + db-backup-{nightly,offsite}.sh were carved out here as
|
|
106
|
+
// "host-pinned ops"; task 1002757 RETIRED that justification for them — each
|
|
107
|
+
// now resolves its database from the branding pack (the migrate.sh pattern) and
|
|
108
|
+
// fails loudly rather than defaulting, so their counts ratcheted to zero and the
|
|
109
|
+
// entries are gone. seed-staging.sh keeps its carve-out on the OPPOSITE
|
|
110
|
+
// rationale: its literal is a fail-CLOSED safety interlock (the script wipes
|
|
111
|
+
// STAGING_DB and refuses unless the name matches exactly), so on any other
|
|
112
|
+
// instance it declines to run instead of destroying the wrong database.
|
|
113
|
+
'scripts/gds/seed-staging.sh': 4,
|
|
114
|
+
'scripts/gds/backfill-sessions.js': 2,
|
|
115
|
+
'scripts/gds/box.js': 3,
|
|
116
|
+
'scripts/gds/publish-manifest.js': 2,
|
|
117
|
+
'scripts/gds/seed-bongos-coreB-tranche1-tasks.js': 1,
|
|
118
|
+
'scripts/gds/seed-bongos-coreB-tranche2-tasks.js': 1,
|
|
119
|
+
'scripts/gds/seed-bongos-goal-hierarchy-tasks.js': 1,
|
|
120
|
+
'scripts/gds/seed-bongos-tasks.js': 1,
|
|
121
|
+
'scripts/gds/seed-gds-v4-tasks.js': 1,
|
|
122
|
+
'scripts/gds/seed-medusa-tasks.js': 7,
|
|
123
|
+
'scripts/gds/seed-module-upstreaming.js': 1,
|
|
124
|
+
'scripts/gds/seed-productization-tasks.js': 6,
|
|
125
|
+
'scripts/gds/seed-provisioning-tasks.js': 2,
|
|
126
|
+
'scripts/gds/version-close.js': 1,
|
|
127
|
+
};
|
|
128
|
+
// `.sh` joined at task 1002567: the extension set was `.js/.html/.css`, so NO shell
|
|
129
|
+
// script had ever been scanned — which is how scripts/gds/lib.sh kept a hardcoded
|
|
130
|
+
// founder host as its API-base default (and a hardcoded ~/.config/otb token path)
|
|
131
|
+
// through the entire task 1002201 sweep, leaving all five bash smokes dead on any
|
|
132
|
+
// instance with a different configDir. The guard could not see the file class the
|
|
133
|
+
// bug lived in.
|
|
134
|
+
const IDENTITY_SCAN_EXTS = new Set(['.js', '.html', '.css', '.sh']);
|
|
135
|
+
// Path-prefix carve-outs (see the header note). Kept SMALL and documented.
|
|
136
|
+
const IDENTITY_SCAN_EXCLUDE = ['modules/game/'];
|
|
137
|
+
// Directories with no authored source to gate.
|
|
138
|
+
const IDENTITY_SCAN_SKIP_DIRS = new Set(['node_modules', '.git', 'generated', 'iterations', 'references']);
|
|
139
|
+
|
|
140
|
+
// Comment stripping lives in identity-strip.js (task 1002756: extracted + replaced
|
|
141
|
+
// the two-regex JS strip, which a `/*` inside a `//` comment could OPEN — it
|
|
142
|
+
// silently hid ~330 provision.js lines from this gate). Re-exported for the tests.
|
|
143
|
+
const { stripCommentsForIdentity } = require('./identity-strip');
|
|
144
|
+
|
|
145
|
+
// Pure: scan one file's comment-stripped source for a hardcoded host-brand literal.
|
|
146
|
+
// Returns violation strings. Exported for the "actually catches a re-hardcode" test.
|
|
147
|
+
function scanPromptIdentity(relFile, text, ext = path.extname(relFile)) {
|
|
148
|
+
const violations = [];
|
|
149
|
+
stripCommentsForIdentity(text, ext).split('\n').forEach((raw, i) => {
|
|
150
|
+
if (!raw.trim()) return;
|
|
151
|
+
for (const re of PROMPT_IDENTITY_LITERALS) {
|
|
152
|
+
const m = raw.match(re);
|
|
153
|
+
if (m) {
|
|
154
|
+
violations.push(`${relFile}:${i + 1} hardcodes the host-brand identity "${m[0]}" — read it from branding()/window.__BRANDING__ (identity/domains/currency), or use a {{worldName}}-style token, so a non-OTB instance renders ITS OWN brand (ADR 0062 §3/§5; BV1.R76 / task 1705).`);
|
|
155
|
+
break; // one hit per line is enough
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return violations;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Recursively collect scannable files under one root, honoring the skip-dir + carve-out lists.
|
|
163
|
+
function collectIdentityFiles(absRoot, relRoot) {
|
|
164
|
+
const out = [];
|
|
165
|
+
let entries;
|
|
166
|
+
try { entries = fs.readdirSync(absRoot, { withFileTypes: true }); }
|
|
167
|
+
catch { return out; }
|
|
168
|
+
for (const e of entries) {
|
|
169
|
+
const rel = relRoot ? `${relRoot}/${e.name}` : e.name;
|
|
170
|
+
if (e.isDirectory()) {
|
|
171
|
+
if (IDENTITY_SCAN_SKIP_DIRS.has(e.name)) continue;
|
|
172
|
+
if (IDENTITY_SCAN_EXCLUDE.some((p) => `${rel}/`.startsWith(p))) continue;
|
|
173
|
+
out.push(...collectIdentityFiles(path.join(absRoot, e.name), rel));
|
|
174
|
+
} else if (IDENTITY_SCAN_EXTS.has(path.extname(e.name))
|
|
175
|
+
&& !IDENTITY_SCAN_EXCLUDE.some((p) => rel.startsWith(p))) {
|
|
176
|
+
out.push(rel);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function checkKernelPromptsNoHardcodedIdentity() {
|
|
183
|
+
const violations = [];
|
|
184
|
+
let scanned = 0;
|
|
185
|
+
// Collect {abs, rel}: src/bongos is CORE; modules ship from BOTH roots (ADR 0108 §Consequences #4).
|
|
186
|
+
// Dedup by repo-relative path so a shared checkout never scans a file twice, and read each file
|
|
187
|
+
// from the root it was actually found under (a module may live in the instance repo post-split).
|
|
188
|
+
const seen = new Set();
|
|
189
|
+
const entries = [];
|
|
190
|
+
const addFrom = (rootAbs, sub) => {
|
|
191
|
+
for (const relFromRoot of collectIdentityFiles(path.join(rootAbs, sub), sub)) {
|
|
192
|
+
if (seen.has(relFromRoot)) continue;
|
|
193
|
+
seen.add(relFromRoot);
|
|
194
|
+
entries.push({ abs: path.join(rootAbs, relFromRoot), rel: relFromRoot });
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
for (const root of IDENTITY_SCAN_ROOTS) {
|
|
198
|
+
for (const rootAbs of (root === 'modules' ? MODULE_ROOTS : [CORE_ROOT])) addFrom(rootAbs, root);
|
|
199
|
+
}
|
|
200
|
+
for (const extra of IDENTITY_SCAN_EXTRA_FILES) {
|
|
201
|
+
if (!seen.has(extra)) { seen.add(extra); entries.push({ abs: path.join(CORE_ROOT, extra), rel: extra }); }
|
|
202
|
+
}
|
|
203
|
+
let ratcheted = 0;
|
|
204
|
+
for (const { abs, rel: relFile } of entries) {
|
|
205
|
+
let text;
|
|
206
|
+
try { text = fs.readFileSync(abs, 'utf8'); }
|
|
207
|
+
catch { continue; } // a file moved/carved away is not a failure of this gate
|
|
208
|
+
scanned += 1;
|
|
209
|
+
const found = scanPromptIdentity(relFile, text);
|
|
210
|
+
// Permanently allowed at a COUNT (task 1002428): the listed literals are
|
|
211
|
+
// correct as written, but an entry may not GROW — a new hardcode in one of
|
|
212
|
+
// these files fails exactly like one in an unlisted file.
|
|
213
|
+
const allowed = IDENTITY_SCAN_ALLOWED[relFile];
|
|
214
|
+
if (allowed !== undefined) {
|
|
215
|
+
if (found.length > allowed) {
|
|
216
|
+
violations.push(
|
|
217
|
+
`${relFile} now hardcodes the host-brand identity ${found.length} time(s), above its allowed ${allowed} — ` +
|
|
218
|
+
`the carve-out covers the EXISTING literals (see IDENTITY_SCAN_ALLOWED), not new ones. ` +
|
|
219
|
+
`Read the host from branding()/window.__BRANDING__ instead (task 1002428).`
|
|
220
|
+
);
|
|
221
|
+
} else {
|
|
222
|
+
ratcheted += 1;
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
violations.push(...found);
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
name: 'portable code carries no hardcoded host identity (BV1.R76 / task 1705)',
|
|
230
|
+
ok: violations.length === 0,
|
|
231
|
+
hardFail: violations.length > 0,
|
|
232
|
+
violations,
|
|
233
|
+
warnings: [],
|
|
234
|
+
note: `${scanned} portable source file(s) scanned (.js/.html/.css/.sh under src/bongos/** + modules/** + scripts/** (whole root, task 1002756), minus the instance-1 product + generated dirs) — none may hardcode the host brand; identity comes from branding()/window.__BRANDING__ (ADR 0062 §3/§5). ${ratcheted} scripts/gds file(s) hold documented permanent carve-outs (real DNS zones, real doc filenames, operator runbook text, historical seeded prose) capped at a count they may not exceed — task 1002428.`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ============================================================================
|
|
239
|
+
// Check 15b — core-shipped NEUTRAL config packs carry no host identity (BV1.R102)
|
|
240
|
+
// ============================================================================
|
|
241
|
+
//
|
|
242
|
+
// Sibling of Check 15 for the CONFIG class. IDENTITY_SCAN_EXTS is js/html/css, so
|
|
243
|
+
// the scan above never opens a .json — yet config/*.neutral.json is precisely what
|
|
244
|
+
// a vanilla instance boots from (branding R76, governance R96). A host name leaking
|
|
245
|
+
// into a neutral pack ships one instance's brand to EVERY fresh instance, and until
|
|
246
|
+
// this check nothing in CI could see it.
|
|
247
|
+
//
|
|
248
|
+
// Only the NEUTRAL packs are scanned, deliberately: an instance pack (config/
|
|
249
|
+
// branding.json, config/government.json) is host content that legitimately names
|
|
250
|
+
// its host, and is boundary-excluded from the published core anyway.
|
|
251
|
+
function checkNeutralConfigsNoHostIdentity() {
|
|
252
|
+
const violations = [];
|
|
253
|
+
const dir = path.join(CORE_ROOT, 'config');
|
|
254
|
+
let names = [];
|
|
255
|
+
try { names = fs.readdirSync(dir).filter((n) => n.endsWith('.neutral.json')).sort(); }
|
|
256
|
+
catch { /* no config/ dir in this root — nothing to gate */ }
|
|
257
|
+
for (const name of names) {
|
|
258
|
+
const relFile = `config/${name}`;
|
|
259
|
+
let text;
|
|
260
|
+
try { text = fs.readFileSync(path.join(dir, name), 'utf8'); }
|
|
261
|
+
catch { continue; } // a pack that moved away is not a failure of this gate
|
|
262
|
+
violations.push(...scanPromptIdentity(relFile, text, '.json'));
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
name: 'neutral config packs carry no host identity (BV1.R102)',
|
|
266
|
+
ok: violations.length === 0,
|
|
267
|
+
hardFail: violations.length > 0,
|
|
268
|
+
violations,
|
|
269
|
+
warnings: [],
|
|
270
|
+
note: `${names.length} core-shipped config/*.neutral.json pack(s) scanned — a vanilla instance boots from these alone, so none may name a host brand. Instance packs (config/<name>.json) are host content and are deliberately NOT scanned.`,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---- design contract (ADR 0081 / task 1377) --------------------------------
|
|
275
|
+
|
|
276
|
+
function checkDesignContract() {
|
|
277
|
+
const { spawnSync } = require('node:child_process');
|
|
278
|
+
const validatorPath = path.join(REPO_ROOT, 'scripts', 'gds', 'validate-design.js');
|
|
279
|
+
const adaptersDir = path.join(REPO_ROOT, 'src', 'ui', 'adapters');
|
|
280
|
+
const hasAdapters = (() => {
|
|
281
|
+
try { return fs.readdirSync(adaptersDir).some((e) => fs.statSync(path.join(adaptersDir, e)).isDirectory()); }
|
|
282
|
+
catch { return false; }
|
|
283
|
+
})();
|
|
284
|
+
|
|
285
|
+
const result = spawnSync(process.execPath, [validatorPath, '--json'], { encoding: 'utf8' });
|
|
286
|
+
let report = { ok: true, errors: [], warnings: [] };
|
|
287
|
+
try { report = JSON.parse(result.stdout || '{}'); } catch { /* fall through with defaults */ }
|
|
288
|
+
if (result.status !== 0 && !report.errors.length) {
|
|
289
|
+
report.errors.push(result.stderr ? result.stderr.trim() : 'validate-design.js exited non-zero');
|
|
290
|
+
report.ok = false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
name: 'design contract valid (docs/design-contract.md / ADR 0081)',
|
|
295
|
+
ok: report.ok,
|
|
296
|
+
hardFail: !report.ok && hasAdapters,
|
|
297
|
+
violations: report.errors || [],
|
|
298
|
+
warnings: report.warnings || [],
|
|
299
|
+
note: hasAdapters
|
|
300
|
+
? 'Hard-fail: adapters present — contract must be valid. Run: node scripts/gds/validate-design.js'
|
|
301
|
+
: 'Warn-only until first adapter lands (task 1378 / task 1379). Run: node scripts/gds/validate-design.js',
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
module.exports = {
|
|
307
|
+
IDENTITY_SCAN_ALLOWED,
|
|
308
|
+
IDENTITY_SCAN_EXCLUDE,
|
|
309
|
+
IDENTITY_SCAN_EXTRA_FILES,
|
|
310
|
+
IDENTITY_SCAN_EXTS,
|
|
311
|
+
IDENTITY_SCAN_ROOTS,
|
|
312
|
+
IDENTITY_SCAN_SKIP_DIRS,
|
|
313
|
+
PROMPT_IDENTITY_LITERALS,
|
|
314
|
+
checkDesignContract,
|
|
315
|
+
checkKernelPromptsNoHardcodedIdentity,
|
|
316
|
+
checkNeutralConfigsNoHostIdentity,
|
|
317
|
+
collectIdentityFiles,
|
|
318
|
+
scanPromptIdentity,
|
|
319
|
+
stripCommentsForIdentity,
|
|
320
|
+
};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// scripts/gds/fitness-checks-vocabulary.js — Checks 19 and 19b: "role" names a
|
|
2
|
+
// craft, never a rank (ADR 0174), and a membership is a membership_kind (ADR 0177).
|
|
3
|
+
//
|
|
4
|
+
// Carved out of scripts/gds/fitness.js by task 1003206 (goal 1000079, the R03
|
|
5
|
+
// de-monolith plan) to bring that file under the 1,500-line ratchet. A STRUCTURAL
|
|
6
|
+
// move: the lines below are fitness.js's originals unchanged, and fitness.js
|
|
7
|
+
// imports every name straight back, so the CHECKS registry and the module's
|
|
8
|
+
// public surface (which the tests read heavily) are untouched.
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
const { CORE_ROOT, rel } = require('./fitness-lib.js');
|
|
15
|
+
|
|
16
|
+
const VOCAB_ALLOW = [
|
|
17
|
+
{
|
|
18
|
+
file: 'modules/government/config.js',
|
|
19
|
+
match: 'roles',
|
|
20
|
+
why: 'the legacy INSTANCE-config key fallback. The core ships `ranks`, but an '
|
|
21
|
+
+ 'external self-hosted instance may still have a config/government.json '
|
|
22
|
+
+ 'authored as `roles`; reading it is what stops their templates vanishing '
|
|
23
|
+
+ 'on upgrade. Removing this line would break them silently.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
file: 'modules/government/config.js',
|
|
27
|
+
match: 'governance_role_labels',
|
|
28
|
+
why: 'the legacy ENV-VAR fallback. <PREFIX>_GOVERNMENT_RANK_LABELS is the current '
|
|
29
|
+
+ 'name, but an operator sets this in their own shell or env file, outside this '
|
|
30
|
+
+ 'repo, so the old spelling is still read. Dropping it would silently stop '
|
|
31
|
+
+ 'honouring a label override someone already configured.',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
file: 'modules/government/board.js',
|
|
35
|
+
match: 'governancepasscredit',
|
|
36
|
+
why: 'the R02 pass-event contract (docs/specs/board-room-pass-event-contract.md, agreed '
|
|
37
|
+
+ 'cross-goal 2026-08-17) names the economy-side award awardIdeaGovernancePassCredit, '
|
|
38
|
+
+ 'matching the credit_log reason family idea.credit.governance_pass:<id> that streams '
|
|
39
|
+
+ '1-3 share. The board calls the CONSUMER\'s agreed method name — renaming it here '
|
|
40
|
+
+ 'would break the contract, and the reason strings are durable ledger rows. Scoped to '
|
|
41
|
+
+ 'the method name, so a stray "governance" elsewhere in board.js still reds the build.',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
file: 'modules/government/module.json',
|
|
45
|
+
match: 'role',
|
|
46
|
+
why: 'the manifest states the rule itself — "The word `role` is deliberately '
|
|
47
|
+
+ 'absent here". Meta-commentary about the ban has to be able to name it.',
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// A line CITING a historical artifact by its real name: an ADR filename, the
|
|
52
|
+
// docs/governance/ evidence folder, or one of the already-applied
|
|
53
|
+
// governance_NNN migrations. These names are permanent (see the header), so a
|
|
54
|
+
// reference to one is a citation, not drift.
|
|
55
|
+
const VOCAB_HISTORICAL_REF = /docs\/adr\/|docs\/governance\/|docs\/session-logs\/|governance_0\d\d/;
|
|
56
|
+
|
|
57
|
+
const VOCAB_SURFACE = [
|
|
58
|
+
'modules/government',
|
|
59
|
+
'modules/hall-ui/public/government.js',
|
|
60
|
+
'modules/hall-ui/public/government.html',
|
|
61
|
+
'modules/hall-ui/public/government.css',
|
|
62
|
+
];
|
|
63
|
+
const VOCAB_EXTS = new Set(['.js', '.json', '.html', '.css']);
|
|
64
|
+
const VOCAB_BANNED = [
|
|
65
|
+
{ re: /\brole/i, label: 'role', hint: '"role" is reserved for the crafts (Artist / Ideator / Builder). On this surface the noun is RANK.' },
|
|
66
|
+
{ re: /governance/i, label: 'governance', hint: 'the pre-ADR-0174 name of this surface. It is the GOVERNMENT now.' },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
function vocabFilesToScan() {
|
|
70
|
+
const out = [];
|
|
71
|
+
const walk = (abs, rel) => {
|
|
72
|
+
let st;
|
|
73
|
+
try { st = fs.statSync(abs); } catch { return; }
|
|
74
|
+
if (st.isDirectory()) {
|
|
75
|
+
// the historical migrations keep their names + bodies (see header)
|
|
76
|
+
if (rel.endsWith('/migrations')) return;
|
|
77
|
+
for (const e of fs.readdirSync(abs)) walk(path.join(abs, e), `${rel}/${e}`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (VOCAB_EXTS.has(path.extname(abs))) out.push({ abs, rel });
|
|
81
|
+
};
|
|
82
|
+
for (const s of VOCAB_SURFACE) walk(path.join(CORE_ROOT, s), s);
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Check 19b — the FOURTH and FIFTH "role": a membership is not a craft either ─
|
|
87
|
+
//
|
|
88
|
+
// task 1003013 / ADR 0177. ADR 0174 reserved `role` for the three crafts and R07
|
|
89
|
+
// (checkVocabularyWall above) enforces it on the GOVERNMENT surface — deliberately
|
|
90
|
+
// only there, because `builders.system_role` is a core machine-identity column and
|
|
91
|
+
// Discord's API owns its own "role" noun. Goal membership was a fourth,
|
|
92
|
+
// independent use of the word (`goal_members.role` = 'lead' | 'member'), which is
|
|
93
|
+
// exactly why it survived the rename: the rule was silent on it, as designed.
|
|
94
|
+
//
|
|
95
|
+
// So this is a SECOND narrow wall rather than a widened first one — same reason the
|
|
96
|
+
// first was scoped: the honest unit is a SURFACE, not the repo. Two surfaces carry
|
|
97
|
+
// a membership, and both now say `membership_kind`:
|
|
98
|
+
// • GOAL membership (`goal_members`, task 1003013) — the routes and pure gate that
|
|
99
|
+
// decide it, the hall page that renders it, and (line-scoped) the membership SQL
|
|
100
|
+
// in modules/lifecycle/db.js;
|
|
101
|
+
// • PROJECT membership on the hub (`platform_identity_project_memberships`, task
|
|
102
|
+
// 1003090) — whose old VALUE 'builder' was itself a craft name. Its check-in
|
|
103
|
+
// route keeps reading a legacy `role` field for the cross-version federation
|
|
104
|
+
// window (other instances upgrade on their own schedule), so the lines that
|
|
105
|
+
// handle that legacy field cite the task and are exempt like any other citation.
|
|
106
|
+
//
|
|
107
|
+
// Naming the rule stays legal — a line that cites ADR 0174 / ADR 0177 / task
|
|
108
|
+
// 1003013 is meta-commentary about the ban, the same principle as
|
|
109
|
+
// VOCAB_HISTORICAL_REF above. `role="…"` is stripped first: that is HTML's ARIA
|
|
110
|
+
// attribute, a different word spelled the same.
|
|
111
|
+
const GOAL_VOCAB_SURFACE = [
|
|
112
|
+
'modules/lifecycle/routes/goals.js',
|
|
113
|
+
'modules/lifecycle/goal-authz.js',
|
|
114
|
+
'modules/lifecycle/routes/done-when.js',
|
|
115
|
+
'modules/hall-ui/public/goals-page.js',
|
|
116
|
+
'modules/hall-ui/public/goals.html',
|
|
117
|
+
'modules/hall-ui/public/goals.css',
|
|
118
|
+
// The FIFTH use (task 1003090): the hub's coarse project membership.
|
|
119
|
+
'modules/platform-identity/platform-identity.js',
|
|
120
|
+
'modules/platform-identity/routes/my-projects.js',
|
|
121
|
+
'modules/hall-ui/public/my-projects.js',
|
|
122
|
+
];
|
|
123
|
+
// modules/lifecycle/db.js is 5k+ lines and legitimately says "role" about ranks
|
|
124
|
+
// (the rank-sync port, builders.system_role). Scanning it whole would need an
|
|
125
|
+
// allowlist longer than the rule; scanning the LINES THAT NAME THE TABLE catches
|
|
126
|
+
// the only thing that can regress here — a membership query reaching for the old
|
|
127
|
+
// column — with no false positives to excuse.
|
|
128
|
+
// task 1003204 (goal 1000079) split that 5k-line file into db.js (a re-export
|
|
129
|
+
// facade) + twelve db-*.js units, so the membership SQL is no longer in db.js at
|
|
130
|
+
// all. Enumerate the family from disk rather than naming one file: a line-scoped
|
|
131
|
+
// scan costs nothing on the units that hold no membership SQL, and a future
|
|
132
|
+
// re-carve cannot silently disarm the rule by moving the queries again.
|
|
133
|
+
const LIFECYCLE_DB_FILES = (() => {
|
|
134
|
+
const dir = path.join(CORE_ROOT, 'modules', 'lifecycle');
|
|
135
|
+
try {
|
|
136
|
+
return fs.readdirSync(dir)
|
|
137
|
+
.filter((f) => f === 'db.js' || (f.startsWith('db-') && f.endsWith('.js')))
|
|
138
|
+
.sort()
|
|
139
|
+
.map((f) => `modules/lifecycle/${f}`);
|
|
140
|
+
} catch { return ['modules/lifecycle/db.js']; }
|
|
141
|
+
})();
|
|
142
|
+
const GOAL_VOCAB_LINE_SCOPED = [
|
|
143
|
+
...LIFECYCLE_DB_FILES.map((file) => ({ file, mustMention: /goal_members/ })),
|
|
144
|
+
// sso.js is the federation surface: only the lines that name the membership table
|
|
145
|
+
// or its upsert are in scope, so the file's other server-to-server prose stays out.
|
|
146
|
+
{ file: 'modules/platform-identity/routes/sso.js', mustMention: /project_memberships|upsertMembership/ },
|
|
147
|
+
];
|
|
148
|
+
const GOAL_VOCAB_CITATION = /ADR 017[47]|task 1003013|task 1003090/;
|
|
149
|
+
|
|
150
|
+
function checkGoalMembershipVocabulary() {
|
|
151
|
+
const violations = [];
|
|
152
|
+
let scanned = 0;
|
|
153
|
+
const scanFile = (rel, lineFilter = null) => {
|
|
154
|
+
const abs = path.join(CORE_ROOT, rel);
|
|
155
|
+
let src;
|
|
156
|
+
try { src = fs.readFileSync(abs, 'utf8'); } catch { return; }
|
|
157
|
+
scanned += 1;
|
|
158
|
+
src.split('\n').forEach((line, i) => {
|
|
159
|
+
if (lineFilter && !lineFilter.test(line)) return;
|
|
160
|
+
const scanLine = line.replace(/\srole\s*=\s*("[^"]*"|'[^']*')/g, ' ');
|
|
161
|
+
if (!/\brole/i.test(scanLine)) return;
|
|
162
|
+
if (GOAL_VOCAB_CITATION.test(scanLine)) return;
|
|
163
|
+
violations.push(`${rel}:${i + 1}: "role" — a membership is a 'membership_kind' (ADR 0174 reserves "role" for the crafts; ADR 0177). → ${line.trim().slice(0, 90)}`);
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
for (const rel of GOAL_VOCAB_SURFACE) scanFile(rel);
|
|
167
|
+
for (const { file, mustMention } of GOAL_VOCAB_LINE_SCOPED) scanFile(file, mustMention);
|
|
168
|
+
return {
|
|
169
|
+
name: 'a membership is a "membership_kind", never a role (ADR 0177)',
|
|
170
|
+
ok: violations.length === 0,
|
|
171
|
+
hardFail: violations.length > 0,
|
|
172
|
+
violations,
|
|
173
|
+
warnings: [],
|
|
174
|
+
note: `${scanned} membership-surface file(s) scanned (${GOAL_VOCAB_SURFACE.length} whole — goal + project membership — plus `
|
|
175
|
+
+ `${GOAL_VOCAB_LINE_SCOPED.length} line-scoped to the lines naming the membership tables); no allowlist — `
|
|
176
|
+
+ 'a line that CITES the rule is the only exemption.',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function checkVocabularyWall() {
|
|
181
|
+
const violations = [];
|
|
182
|
+
let scanned = 0;
|
|
183
|
+
for (const { abs, rel } of vocabFilesToScan()) {
|
|
184
|
+
scanned += 1;
|
|
185
|
+
const lines = fs.readFileSync(abs, 'utf8').split('\n');
|
|
186
|
+
lines.forEach((line, i) => {
|
|
187
|
+
// `role="…"` is HTML's own ARIA attribute — a different word that happens to
|
|
188
|
+
// be spelled the same. Strip attributes before matching rather than
|
|
189
|
+
// allowlisting each occurrence: this is a property of HTML, not of any one file.
|
|
190
|
+
const scanLine = line.replace(/\srole\s*=\s*("[^"]*"|'[^']*')/g, ' ');
|
|
191
|
+
for (const b of VOCAB_BANNED) {
|
|
192
|
+
if (!b.re.test(scanLine)) continue;
|
|
193
|
+
// A REFERENCE to a historical artifact by its real name is not a
|
|
194
|
+
// vocabulary violation — it is a citation. The artifacts themselves are
|
|
195
|
+
// excluded by construction (see the header); naming one has to be legal
|
|
196
|
+
// too, or the code could not point at the migration that created its own
|
|
197
|
+
// tables. Same principle, applied to the line rather than the file.
|
|
198
|
+
if (VOCAB_HISTORICAL_REF.test(scanLine)) continue;
|
|
199
|
+
const excused = VOCAB_ALLOW.some((a) => a.file === rel && scanLine.toLowerCase().includes(a.match));
|
|
200
|
+
if (excused) continue;
|
|
201
|
+
violations.push(`${rel}:${i + 1}: "${b.label}" — ${b.hint} → ${line.trim().slice(0, 90)}`);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
name: 'the word "role" means a craft, never a rank (ADR 0174)',
|
|
207
|
+
ok: violations.length === 0,
|
|
208
|
+
hardFail: violations.length > 0,
|
|
209
|
+
violations,
|
|
210
|
+
warnings: [],
|
|
211
|
+
note: `${scanned} government-surface file(s) scanned for "role"/"governance"; `
|
|
212
|
+
+ `${VOCAB_ALLOW.length} documented allowlist entr(y|ies). Historical migrations + docs/adr are excluded by construction, not suppressed.`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
module.exports = {
|
|
218
|
+
GOAL_VOCAB_CITATION,
|
|
219
|
+
GOAL_VOCAB_LINE_SCOPED,
|
|
220
|
+
GOAL_VOCAB_SURFACE,
|
|
221
|
+
LIFECYCLE_DB_FILES,
|
|
222
|
+
VOCAB_ALLOW,
|
|
223
|
+
VOCAB_BANNED,
|
|
224
|
+
VOCAB_EXTS,
|
|
225
|
+
VOCAB_HISTORICAL_REF,
|
|
226
|
+
VOCAB_SURFACE,
|
|
227
|
+
checkGoalMembershipVocabulary,
|
|
228
|
+
checkVocabularyWall,
|
|
229
|
+
vocabFilesToScan,
|
|
230
|
+
};
|