@bongos/core 1.19.331 → 1.19.333

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.bongos-core.json +163 -43
  2. package/docs/api/openapi.json +2 -2
  3. package/docs/api-reference.md +1 -1
  4. package/docs/copy-inventory.md +202 -202
  5. package/docs/copy-registry.json +384 -384
  6. package/modules/grading/grader-config.js +98 -0
  7. package/modules/grading/grader-model.js +150 -0
  8. package/modules/grading/grader-prompt.js +139 -0
  9. package/modules/grading/grader-score.js +706 -0
  10. package/modules/grading/grader-subagent.js +328 -0
  11. package/modules/grading/grader.js +5 -1287
  12. package/modules/hall-ui/public/builders.js +19 -1182
  13. package/modules/hall-ui/public/goals-page.js +11 -997
  14. package/modules/hall-ui/public/goals-render.js +1032 -0
  15. package/modules/hall-ui/public/goals.html +4 -0
  16. package/modules/hall-ui/public/hall-render.js +1211 -0
  17. package/modules/hall-ui/public/index.html +6 -0
  18. package/modules/lifecycle/routes/goal-route-authz.js +400 -0
  19. package/modules/lifecycle/routes/goals.js +1 -367
  20. package/modules/lifecycle/routes/task-route-helpers.js +219 -0
  21. package/modules/lifecycle/routes/task-write-routes.js +542 -0
  22. package/modules/lifecycle/routes/tasks.js +3 -696
  23. package/package-lock.json +2 -2
  24. package/package.json +1 -1
  25. package/public-landing/assets/cosmos.css +4 -0
  26. package/public-landing/projects.html +103 -23
  27. package/scripts/gds/box-infra.js +280 -0
  28. package/scripts/gds/box.js +1 -245
  29. package/scripts/gds/fitness-checks-identity.js +320 -0
  30. package/scripts/gds/fitness-checks-vocabulary.js +230 -0
  31. package/scripts/gds/fitness-checks-write-validation.js +144 -0
  32. package/scripts/gds/fitness-lib.js +82 -0
  33. package/scripts/gds/fitness.js +55 -668
  34. package/scripts/gds/provision-config.js +204 -0
  35. package/scripts/gds/provision-net.js +221 -0
  36. package/scripts/gds/provision-repo.js +849 -0
  37. package/scripts/gds/provision-units.js +262 -0
  38. package/scripts/gds/provision.js +4 -1395
  39. package/src/bongos/auth-admission.js +264 -0
  40. package/src/bongos/auth-config.js +151 -0
  41. package/src/bongos/auth-discord.js +198 -0
  42. package/src/bongos/auth-github.js +336 -0
  43. package/src/bongos/auth-idp.js +330 -0
  44. package/src/bongos/auth.js +5 -1100
  45. package/src/module-api.js +5 -1
  46. package/tests/auth_admission_gate.mjs +3 -1
  47. package/tests/est_advisory.mjs +3 -1
  48. package/tests/goal_advisory.mjs +3 -2
  49. package/tests/goal_criterion_confirm_ui.mjs +6 -0
  50. package/tests/goal_subtitle.mjs +5 -2
  51. package/tests/goals_card_view.mjs +10 -3
  52. package/tests/grader_generated_provenance.mjs +3 -1
  53. package/tests/hall_palette.mjs +3 -1
  54. package/tests/helpers.mjs +58 -25
  55. package/tests/lib_sh_resolution.mjs +3 -1
  56. package/tests/membership_kind_lockstep.mjs +3 -1
  57. package/tests/migration_allocation.mjs +5 -2
  58. package/tests/patch_gate_ordering.mjs +3 -1
  59. package/tests/projects_hub_pre_uat.mjs +53 -0
  60. package/tests/rank_tier_single_source.mjs +16 -3
  61. package/tests/reward_gate.mjs +3 -2
@@ -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
+ };
@@ -0,0 +1,144 @@
1
+ // scripts/gds/fitness-checks-write-validation.js — Check 18 (BV1 R12 / task 1999,
2
+ // ADR 0118): every body-reading write route validates its body.
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 genApiDocs = require('./gen-api-docs'); // R12 (#1999): reuse the route model (readsBody/validated) that drives the OpenAPI spec
13
+
14
+ // ============================================================================
15
+ // Check 18 — every write route validates its body (BV1 R12 / task 1999 / ADR 0118)
16
+ // ============================================================================
17
+ //
18
+ // Criterion api-validation: "the build fails on an unvalidated write route." R11
19
+ // (#1998) gave every body-reading write route a strict validateOrRespond schema
20
+ // (code-first, spec-driven — ADR 0118); this LOCKS that in. Reusing the SAME
21
+ // route model that generates the OpenAPI spec (gen-api-docs.extractModel —
22
+ // readsBody + validated), a POST/PUT/PATCH/DELETE that reads req.body must carry
23
+ // a validateOrRespond()/validate(req.body) schema. Hard-zero (no ratchet — the
24
+ // R11 sweep already reached 100%): a NEW write route that reads a body but
25
+ // declares no schema fails the build immediately, so unknown/typo'd fields can
26
+ // never again be silently dropped.
27
+ //
28
+ // EXEMPT — the builder-settings /me/*-prefs routes. Their bodies are DYNAMIC-KEY
29
+ // maps ({ "<skill|sound|event-id>": <value> }, or a single-key toggle) that a
30
+ // FIXED field schema cannot express; they validate via their module lib's
31
+ // validatePatch()/validate() (which already rejects unknown keys with a 422). A
32
+ // fixed strict schema would wrongly reject the valid dynamic keys — so these are
33
+ // validated-by-lib, not by validateOrRespond. Documented per-route below; see
34
+ // learning #133 / ADR 0118. Adding a route here is a deliberate, reviewable act.
35
+ const WRITE_VALIDATION_EXEMPT = new Set([
36
+ 'PATCH /me/skill-prefs', // { "<skill>": "<model>"|null } — dynamic keys; skillPrefs.validatePatch
37
+ 'PATCH /me/sound-prefs', // { "<sound>": <bool>|null } — dynamic keys; soundPrefs.validatePatch
38
+ 'PATCH /me/event-sound-prefs', // { "<event>": "<variant>"|null } — dynamic keys; eventSoundPrefs.validatePatch
39
+ 'PATCH /me/wandering', // { wandering: 0..3|null } — rank-capped; wanderingPrefs.validatePatch
40
+ 'PATCH /me/interaction', // interaction profile — interactionPrefs.validatePatch
41
+ 'PATCH /me/model-allocation', // per-slot tiers — modelAllocPrefs.validatePatch
42
+ 'PATCH /me/render', // { widgets: <bool>|null } — renderPrefs.validatePatch
43
+ 'PATCH /me/display-name', // { display_name } — displayName.validate
44
+ ]);
45
+
46
+ // KNOWN GAPS, NOT EXEMPTIONS (task 1002551 / BV1.R109b → tracked by task 1002552).
47
+ //
48
+ // These three are NOT validated-by-lib and do NOT belong in WRITE_VALIDATION_EXEMPT
49
+ // above — they are genuine ADR 0118 gaps that must grow a validateOrRespond schema.
50
+ // They surface here only because R109b taught route discovery to see MULTI-LINE
51
+ // `router.<method>(` declarations: this check reuses gen-api-docs.extractModel(), so
52
+ // the same regex hole that hid them from the rank auditor hid them from Check 18 too.
53
+ // They are pre-existing (never validated), not a regression.
54
+ //
55
+ // They are recorded rather than fixed because modules/*/routes/*.js was being migrated
56
+ // concurrently by BV1.R105, so editing those files would have collided. This is the
57
+ // Check-17 ratchet posture: a NAMED, non-growing baseline. The list may only ever
58
+ // SHRINK — a new unvalidated write route is not on it and still hard-fails the build,
59
+ // and a stale entry (one that HAS been fixed) also fails, so the list cannot rot.
60
+ // Delete this constant when task 1002552 lands and Check 18 returns to hard-zero.
61
+ // EMPTY as of BV1.R109c (task 1002552) — all three original entries now validate.
62
+ // Kept (rather than deleted) as the ratchet itself: an empty named baseline states
63
+ // the invariant is hard-zero, and any future entry has to be added deliberately.
64
+ const WRITE_VALIDATION_BASELINE = new Set([]);
65
+
66
+ function scanUnvalidatedWriteRoutes(model = genApiDocs.extractModel()) {
67
+ const unvalidated = [];
68
+ const baselineSeen = new Set();
69
+ for (const r of model) {
70
+ if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(r.method)) continue;
71
+ // Only routes that actually read a body have an input surface to validate;
72
+ // a write that ignores req.body has nothing to declare.
73
+ if (!r.body || !r.body.readsBody || r.body.validated) continue;
74
+ const key = `${r.method} ${r.route}`;
75
+ if (WRITE_VALIDATION_EXEMPT.has(key)) continue;
76
+ if (WRITE_VALIDATION_BASELINE.has(key)) { baselineSeen.add(key); continue; }
77
+ unvalidated.push(`${key} [${r.file}]`);
78
+ }
79
+ // A baseline entry that no longer reports as unvalidated has been FIXED — it must be
80
+ // removed, or the list would quietly license a future regression on that same route.
81
+ for (const key of WRITE_VALIDATION_BASELINE) {
82
+ if (!baselineSeen.has(key)) {
83
+ unvalidated.push(`${key} [STALE BASELINE — this route now validates; remove it from WRITE_VALIDATION_BASELINE in scripts/gds/fitness.js]`);
84
+ }
85
+ }
86
+ return unvalidated;
87
+ }
88
+
89
+ function checkWriteRoutesValidated() {
90
+ const unvalidated = scanUnvalidatedWriteRoutes();
91
+ const violations = [];
92
+ if (unvalidated.length) {
93
+ violations.push(
94
+ `${unvalidated.length} write route(s) read req.body but declare NO validateOrRespond/validate schema (ADR 0118 / R11 requires one so unknown fields are rejected via the ADR-0116 envelope): ${unvalidated.join('; ')}. Add a strict validateOrRespond(req, res, { … }) listing every field the handler reads (see learning #133). A dynamic-key body validated by a module lib belongs in WRITE_VALIDATION_EXEMPT with a one-line reason.`
95
+ );
96
+ }
97
+ return {
98
+ name: 'every write route validates its body (ADR 0118 / R12)',
99
+ ok: violations.length === 0,
100
+ hardFail: violations.length > 0,
101
+ violations,
102
+ warnings: [],
103
+ note: `body-reading POST/PUT/PATCH/DELETE routes must call validateOrRespond/validate (${WRITE_VALIDATION_EXEMPT.size} dynamic-key prefs routes validate by lib; ${WRITE_VALIDATION_BASELINE.size} known gaps tracked by task 1002552, list may only shrink); ${unvalidated.length} unvalidated.`,
104
+ };
105
+ }
106
+
107
+
108
+ // ── Check 19 — the ADR 0174 vocabulary wall (goal 1000068 R07, task 1003008) ──
109
+ //
110
+ // "Role" means ONE thing in this codebase: a craft — Artist, Ideator, or Builder.
111
+ // The permission system is the GOVERNMENT and the positions in it are RANKS.
112
+ // Before ADR 0174 the same word named both, and goal 1000066 was about to claim
113
+ // it for the crafts while modules/governance/ still used it for the ladder. This
114
+ // check is what stops that collision reforming the first time someone copies an
115
+ // old file.
116
+ //
117
+ // TWO RULES, both scoped to the GOVERNMENT SURFACE — not the whole repo. A repo-
118
+ // wide ban would be wrong: `builders.system_role` is a core column for machine
119
+ // identities, Discord's API owns its own "role" noun, and the crafts are supposed
120
+ // to use the word.
121
+ //
122
+ // 1. no "role" under modules/government/ or on the Government hall page
123
+ // 2. no "governance" there either — the old name of the same surface
124
+ //
125
+ // EXCLUDED BY CONSTRUCTION (not by allowlist):
126
+ // • modules/government/migrations/governance_*.sql — the already-applied
127
+ // migrations keep their filenames forever (schema_migrations is stem-keyed;
128
+ // renaming one re-runs it, and governance_002 is a seed/backfill). Their
129
+ // bodies necessarily name the pre-rename tables they created.
130
+ // • docs/adr/, docs/session-logs/, docs/governance/ — the historical record.
131
+ // An ADR describes a decision at a point in time; rewriting it to match
132
+ // today's vocabulary would erase the evidence that the words changed.
133
+ //
134
+ // The ALLOWLIST below is deliberately tiny and each entry carries its reason. A
135
+ // rule that passes because everything is suppressed reads as evidence while
136
+ // proving nothing — if this list grows, that is the signal to finish the rename,
137
+ // not to add a line.
138
+
139
+ module.exports = {
140
+ WRITE_VALIDATION_BASELINE,
141
+ WRITE_VALIDATION_EXEMPT,
142
+ checkWriteRoutesValidated,
143
+ scanUnvalidatedWriteRoutes,
144
+ };
@@ -0,0 +1,82 @@
1
+ // scripts/gds/fitness-lib.js — the two roots every architecture check walks, and
2
+ // the walkers themselves (ADR 0108 §1: core tree vs instance tree).
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 { resolveCoreRoot, resolveInstanceRoot } = require('../../src/instance-config'); // ADR 0108 §1: configurable-root
15
+
16
+
17
+ // ADR 0108 §Consequences #4 (configurable-root W6 / task 1886) — the guardrail walks TWO trees.
18
+ // CORE code + core-owned docs (src/, server.js, scripts/, the src/bongos + scripts/gds nested docs)
19
+ // live under the core PACKAGE; INSTANCE content (the root CLAUDE.md, the .claude/ methodology
20
+ // surface, instance modules + migrations) lives under the host REPO. Modules ship from BOTH.
21
+ // Today both resolve to the same checkout, so every walk below is byte-identical; once the core
22
+ // installs from node_modules they diverge and each check inspects the CORRECT tree. A NEW
23
+ // hardcoded `path.resolve(__dirname,'..','..')` here reintroduces the bug this check exists to stop.
24
+ const CORE_ROOT = resolveCoreRoot();
25
+ const INSTANCE_ROOT = resolveInstanceRoot();
26
+ // REPO_ROOT retained as an alias to the core root for the many purely-core walks below (src/bongos,
27
+ // KERNEL_FILES, scope map, …) — those keep their exact behavior. Instance-owned reads route
28
+ // through INSTANCE_ROOT explicitly, and module reads through MODULE_ROOTS.
29
+ const REPO_ROOT = CORE_ROOT;
30
+ // Deduped module search roots — a module can ship from the core package OR the instance repo (the
31
+ // loader errors on a cross-root key collision, so a key resolves to exactly one root). Deduped so
32
+ // today's single checkout scans each module ONCE (no double-counting).
33
+ const MODULE_ROOTS = [...new Set([CORE_ROOT, INSTANCE_ROOT])];
34
+
35
+ // ---- shared fs helpers -----------------------------------------------------
36
+
37
+ function walkJs(absRoot, out = []) {
38
+ let st;
39
+ try { st = fs.statSync(absRoot); } catch { return out; }
40
+ if (st.isFile()) { if (absRoot.endsWith('.js')) out.push(absRoot); return out; }
41
+ for (const ent of fs.readdirSync(absRoot, { withFileTypes: true })) {
42
+ if (ent.name === 'node_modules' || ent.name === '.git') continue;
43
+ walkJs(path.join(absRoot, ent.name), out);
44
+ }
45
+ return out;
46
+ }
47
+ // repo-relative POSIX path for an abs file under EITHER root (core or instance). The classifier
48
+ // regexes throughout this file key on these relative strings, which are identical whichever tree
49
+ // a file lives in — so only the WALK roots change under the split, not the classification.
50
+ const rel = (abs) => {
51
+ for (const base of [CORE_ROOT, INSTANCE_ROOT]) {
52
+ const r = path.relative(base, abs);
53
+ if (r && !r.startsWith('..') && !path.isAbsolute(r)) return r.split(path.sep).join('/');
54
+ }
55
+ return path.relative(CORE_ROOT, abs).split(path.sep).join('/');
56
+ };
57
+ // Walk .js under `subpath` (e.g. 'modules') across ALL module roots, deduped by repo-relative
58
+ // path so the same file is never scanned twice when the roots coincide (today).
59
+ function walkJsAllRoots(subpath, roots = MODULE_ROOTS) {
60
+ const seen = new Set();
61
+ const out = [];
62
+ for (const root of roots) {
63
+ for (const abs of walkJs(path.join(root, subpath))) {
64
+ const r = rel(abs);
65
+ if (seen.has(r)) continue;
66
+ seen.add(r);
67
+ out.push(abs);
68
+ }
69
+ }
70
+ return out;
71
+ }
72
+
73
+
74
+ module.exports = {
75
+ CORE_ROOT,
76
+ INSTANCE_ROOT,
77
+ MODULE_ROOTS,
78
+ REPO_ROOT,
79
+ rel,
80
+ walkJs,
81
+ walkJsAllRoots,
82
+ };