@bongos/core 1.19.909 → 1.19.911
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 +52 -32
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +62 -2
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +25 -25
- package/docs/copy-registry.json +29 -29
- package/docs/file-map.md +3 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/ideas.css +25 -0
- package/modules/hall-ui/public/ideas.html +12 -0
- package/modules/hall-ui/public/ideas.js +115 -2
- package/modules/ideas/landing-preview.js +180 -0
- package/modules/ideas/routes/inbox.js +47 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/api-alias-caller-scan.js +213 -0
- package/scripts/gds/fitness-ratchets.js +31 -0
- package/scripts/gds/fitness.js +1 -1
- package/scripts/gds/gen-file-map.js +96 -2
- package/scripts/gds/ship-check.js +1 -1
- package/scripts/hall-preview/server.js +14 -1
- package/src/module-api.js +1 -1
- package/tests/api_alias_caller_ratchet.mjs +130 -0
- package/tests/file_map.mjs +59 -0
- package/tests/idea_landing_preview.mjs +337 -0
- package/tests/idea_spark_hall.mjs +183 -0
|
@@ -52,6 +52,10 @@ const { completenessScore, weakestSectionPayload, scoreFieldValues } = require('
|
|
|
52
52
|
// The trusted tier, spelled once (task 1003197 (D)) — routing.js owns METIC_PLUS
|
|
53
53
|
// and already names 'criterion-proposal' in NEVER_ROUTE_KINDS.
|
|
54
54
|
const { mayProposeCriterion, METIC_PLUS } = require('../routing');
|
|
55
|
+
// "Gravity" — the landing verdict read BEFORE the write (task 1004083, BV2.R02).
|
|
56
|
+
// Read-only and never-throwing; it calls the same matrix the capture calls so the
|
|
57
|
+
// composer can state the outcome instead of implying one. See ../landing-preview.js.
|
|
58
|
+
const { previewLanding } = require('../landing-preview');
|
|
55
59
|
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
56
60
|
const log = api.logger('ideas');
|
|
57
61
|
const { validateOrRespond, LIMITS, parseId } = api;
|
|
@@ -514,6 +518,49 @@ module.exports = function buildInboxRouter() {
|
|
|
514
518
|
const { score, fields } = scoreFieldValues(req.body.field_values, templateKey ? { templateKey } : undefined);
|
|
515
519
|
return res.json({ score, fields, template_key: templateKey || null });
|
|
516
520
|
}, { errorCode: 'score_failed' }));
|
|
521
|
+
// POST /inbox/preview-landing — GRAVITY, MADE VISIBLE (task 1004083, BV2.R02).
|
|
522
|
+
//
|
|
523
|
+
// What pressing "File it" would do, without doing it. The composer draws one
|
|
524
|
+
// line from this so a filer can see which goal will catch the thought, change
|
|
525
|
+
// it, or deliberately let it float — the criterion's done-when, read literally.
|
|
526
|
+
//
|
|
527
|
+
// A DRY RUN IN THE SHAPE OF /inbox/score DIRECTLY ABOVE: same POST-with-a-body
|
|
528
|
+
// reason (the draft is the input, not an id), same "a half-formed draft is a
|
|
529
|
+
// legal state" contract. It differs in one way worth naming, because the file
|
|
530
|
+
// header above claims /score is pure: this one READS (the goal row, the
|
|
531
|
+
// building version). It still WRITES NOTHING — see ../landing-preview.js, which
|
|
532
|
+
// calls `currentBuildingVersionId` and deliberately never `ensureMaintenanceGoal`,
|
|
533
|
+
// so previewing a homeless bug cannot mint a goal as a side effect of typing.
|
|
534
|
+
//
|
|
535
|
+
// THE RANK AND THE VECTOR ARE NOT READ FROM THE BODY, exactly as POST /inbox
|
|
536
|
+
// refuses to read them: the rank comes off the authenticated builder and the
|
|
537
|
+
// vector is hardcoded to 'api'. A preview is only worth having if it is the
|
|
538
|
+
// same question the capture will be asked — one that answered a rank the filer
|
|
539
|
+
// does not hold would teach them a landing they cannot have.
|
|
540
|
+
//
|
|
541
|
+
// NEVER A 4xx FOR A DRAFT. Only the input SHAPE is validated; an unknown kind,
|
|
542
|
+
// an absent title, a goal that vanished all come back as `outcome: null`
|
|
543
|
+
// ("nothing to say yet"), because a composer redrawing on every keystroke must
|
|
544
|
+
// degrade to silence rather than to a red error under a half-typed thought.
|
|
545
|
+
//
|
|
546
|
+
// rank: any-builder — it answers only about the caller's own prospective
|
|
547
|
+
// filing, and filing itself is open to every rank.
|
|
548
|
+
router.post('/inbox/preview-landing', api.requireBuilder, api.asyncHandler('POST /inbox/preview-landing', async (req, res) => {
|
|
549
|
+
if (validateOrRespond(req, res, {
|
|
550
|
+
goal_id: { type: 'integer', min: 1 },
|
|
551
|
+
kind: { type: 'string', maxLength: 64 },
|
|
552
|
+
grade: { type: 'string', maxLength: 16 },
|
|
553
|
+
})) return;
|
|
554
|
+
const body = req.body || {};
|
|
555
|
+
const verdict = await previewLanding({
|
|
556
|
+
goalId: body.goal_id ?? null,
|
|
557
|
+
kind: body.kind ?? null,
|
|
558
|
+
grade: body.grade ?? null,
|
|
559
|
+
filerRank: req.builder?.rank ?? null,
|
|
560
|
+
vector: 'api',
|
|
561
|
+
});
|
|
562
|
+
return res.json(verdict);
|
|
563
|
+
}, { errorCode: 'preview_landing_failed' }));
|
|
517
564
|
// GET /inbox/sparks — THE SPARK QUEUE (task 1002921 / BV1.R145, goal 1000062).
|
|
518
565
|
//
|
|
519
566
|
// Big Idea Sparks awaiting development: the ideator craft's pickup surface,
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.911",
|
|
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.911",
|
|
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.911",
|
|
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,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// api-alias-caller-scan.js — the detector behind the lowercase `/api/gds` CALLER ratchet
|
|
4
|
+
// (task 1004064, from idea 1001030; the sibling of gds-literal-scan.js).
|
|
5
|
+
//
|
|
6
|
+
// WHY THIS EXISTS. Task 1003707 (criterion C3e of goal 1000073) moved 665 in-repo callers off the
|
|
7
|
+
// permanent `/api/gds` alias onto the canonical `/api/bongos`. The ratchet that already guards the
|
|
8
|
+
// rename, `legacy_gds_literals`, is CASE-SENSITIVE on the shouting acronym — deliberately, because
|
|
9
|
+
// `GDS` is the word a newcomer trips over. So that metric read 1135 before the sweep and 1135
|
|
10
|
+
// after it, and it was right to: the alias is spelled in lowercase.
|
|
11
|
+
//
|
|
12
|
+
// The consequence is the gap this file closes. The lowercase spelling is invisible to CI in BOTH
|
|
13
|
+
// directions: the 665-caller sweep earned no credit, and — the half that matters — a caller written
|
|
14
|
+
// against `/api/gds` tomorrow moves no number and reds no build. 1003707's own session log says it
|
|
15
|
+
// plainly: "the 665 will quietly grow back". `tests/api_alias_exceptions.mjs` pins the callers that
|
|
16
|
+
// were deliberately LEFT behind and why, but it polices the exceptions, not new offenders. That is
|
|
17
|
+
// a ratchet's job.
|
|
18
|
+
//
|
|
19
|
+
// WHY A COUNT AND NOT ZERO. Every remaining occurrence has a reason, and most of them are reasons
|
|
20
|
+
// that will never expire — the OAuth callbacks are registered in GitHub's and Discord's app
|
|
21
|
+
// settings, the uptime poller is the one production caller proving the alias still answers, and
|
|
22
|
+
// tests are what keep it exercised in CI. A zero-rule would demand deleting things that must not be
|
|
23
|
+
// deleted. So this holds the line the same way its sibling does: the number may fall, never rise.
|
|
24
|
+
//
|
|
25
|
+
// WHAT IS NEVER COUNTED, and why each one. The exclusion list is not invented here — it is
|
|
26
|
+
// 1003707's own worked-out list, transcribed from docs/session-logs/2026-09-16-task-1003707-api-gds-callers.md
|
|
27
|
+
// so that a future move has to argue with a stated reason rather than with a leftover:
|
|
28
|
+
//
|
|
29
|
+
// 1. THE ALIAS CONTRACT (path-excluded). api-prefix.js and api-path-404.js ARE the alias; they
|
|
30
|
+
// cannot name it less often than they do.
|
|
31
|
+
// 2. THE PINNED OAUTH CALLBACKS (path-excluded). routes/auth.js, provisioning.js and the scripts
|
|
32
|
+
// + skills that register the same callback for a new instance. These paths live in the GitHub
|
|
33
|
+
// and Discord app settings; a repo-only change silently breaks sign-in.
|
|
34
|
+
// 3. THE LIVE PROOF (path-excluded). uptime-poller.js and smoke-gds.js exercise the alias on
|
|
35
|
+
// purpose — if nothing calls it, its eventual breakage is found by a user rather than by us.
|
|
36
|
+
// Note fitness-ratchets.js belongs here too: it is the GATE, and its builder-facing hint has
|
|
37
|
+
// to spell the alias out to tell anyone what they tripped over.
|
|
38
|
+
// 4. THE RENAME GUARDRAILS (path-excluded). rename-history-check.js, rename-ledger-prose.js,
|
|
39
|
+
// gds-literal-scan.js, codemod-rename-src-gds.js, fitness-ratchets.js and THIS FILE name the
|
|
40
|
+
// old path because that is what they are for. Same principle as GDS_RULE_FILES next door: a
|
|
41
|
+
// gate that cannot state its own rule cannot explain itself.
|
|
42
|
+
// 5. tests/ (path-excluded). Tests are what keeps the alias exercised in CI. Moving them off it
|
|
43
|
+
// would retire the alias by accident.
|
|
44
|
+
// 6. docs/ (path-excluded) — a DELIBERATE SCOPE CALL, not an oversight. It is the largest
|
|
45
|
+
// remaining bucket (~320) and it is where rename-history-check's per-file floors live, so a
|
|
46
|
+
// docs pass has to separate the record of what we used to call it from instructions that
|
|
47
|
+
// still teach the old spelling. 1004064's own brief says to treat docs as out of scope and
|
|
48
|
+
// file it rather than widen the task; filed as the follow-up named in that task's session log.
|
|
49
|
+
// 7. MIGRATIONS + THE FROZEN RECORD (path-excluded). Same rule as the sibling detector: an
|
|
50
|
+
// applied migration renamed is an applied migration re-run, and a rewritten ADR destroys the
|
|
51
|
+
// evidence that the words changed.
|
|
52
|
+
// 8. A LINE THAT IS ABOUT THE ALIAS (line-excluded). Any line that ALSO names `/api/bongos`, or
|
|
53
|
+
// carries the words alias / legacy / permanent / forever, is commentary on the alias rather
|
|
54
|
+
// than a caller of it. This is the same rule 1003707's codemod used to decide what to leave,
|
|
55
|
+
// so the ratchet and the sweep agree by construction.
|
|
56
|
+
// 9. CAPTURED FIXTURES (path-excluded). scripts/hall-preview/fixtures/*.json are snapshots of
|
|
57
|
+
// real API responses; rewriting one would make the fixture misreport what the server returned.
|
|
58
|
+
//
|
|
59
|
+
// Usage:
|
|
60
|
+
// node scripts/gds/api-alias-caller-scan.js # count + the top offending files
|
|
61
|
+
// node scripts/gds/api-alias-caller-scan.js --files # every file with its count
|
|
62
|
+
// node scripts/gds/api-alias-caller-scan.js --json
|
|
63
|
+
|
|
64
|
+
const fs = require('node:fs');
|
|
65
|
+
const path = require('node:path');
|
|
66
|
+
const { execFileSync } = require('node:child_process');
|
|
67
|
+
|
|
68
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
69
|
+
|
|
70
|
+
// The alias, lowercase — the whole point of this detector, since its sibling is case-sensitive on
|
|
71
|
+
// the shouting form and therefore blind to exactly this spelling.
|
|
72
|
+
const ALIAS = '/api/gds';
|
|
73
|
+
const ALIAS_RE = /\/api\/gds/g;
|
|
74
|
+
|
|
75
|
+
// The canonical prefix. A line naming BOTH is commentary on the alias, not a caller of it.
|
|
76
|
+
const CANONICAL = '/api/bongos';
|
|
77
|
+
|
|
78
|
+
// Path exclusions, matched against the repo-relative path (forward slashes), at any depth.
|
|
79
|
+
// Every entry maps to a numbered reason in the header above — keep them in step.
|
|
80
|
+
const ALIAS_EXCLUDED_PATHS = [
|
|
81
|
+
// 1. the alias contract
|
|
82
|
+
/^src\/bongos\/api-prefix\.js$/,
|
|
83
|
+
/^src\/bongos\/api-path-404\.js$/,
|
|
84
|
+
// 2. the pinned OAuth callbacks + everything that registers them for a new instance
|
|
85
|
+
/^src\/bongos\/routes\/auth\.js$/,
|
|
86
|
+
/^modules\/provisioning\/provisioning\.js$/,
|
|
87
|
+
/^scripts\/gds\/(onboard|provision-net|dev|login)\.js$/,
|
|
88
|
+
/^modules\/lifecycle\/kickoff-checklist\.js$/,
|
|
89
|
+
/^modules\/public-landing\/public\/projects\.html$/,
|
|
90
|
+
/^\.claude\/skills\/new-project\/SKILL\.md$/,
|
|
91
|
+
// 3. the live proof that the alias still answers
|
|
92
|
+
/^src\/bongos\/uptime-poller\.js$/,
|
|
93
|
+
/^scripts\/gds\/smoke-gds\.js$/,
|
|
94
|
+
// 4. the rename guardrails — they name the old path on purpose
|
|
95
|
+
/^scripts\/gds\/(rename-history-check|rename-ledger-prose|gds-literal-scan|codemod-rename-src-gds|api-alias-caller-scan|fitness-ratchets)\.js$/,
|
|
96
|
+
/^config\/(fitness-baselines|rename-history-baseline)\.json$/,
|
|
97
|
+
// 5. tests keep the alias exercised in CI — the repo's own two spellings, `tests/` at the root
|
|
98
|
+
// and a module's `tests/` or `test/` dir. Deliberately NOT a blanket `\.mjs$`: three .mjs
|
|
99
|
+
// files are not tests (clients/bongos-client/index.mjs and its example, scripts/status-probe.mjs)
|
|
100
|
+
// and the client is exactly the kind of caller this ratchet exists to police.
|
|
101
|
+
/(^|\/)tests?\//,
|
|
102
|
+
// 6. docs are a separate pass (scope call, see the header)
|
|
103
|
+
/(^|\/)docs\//,
|
|
104
|
+
// 7. migrations + the frozen record
|
|
105
|
+
/(^|\/)migrations\//,
|
|
106
|
+
/(^|\/)limitations\//,
|
|
107
|
+
// 9. captured API-response fixtures
|
|
108
|
+
/(^|\/)fixtures\//,
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
// Vendored / generated / third-party trees — the same exclusion the sibling detector applies.
|
|
112
|
+
const ALIAS_EXCLUDED_TREES = /(^|\/)(node_modules|public-docs)\/|\.min\.js$|(^|\/)vendor\//;
|
|
113
|
+
|
|
114
|
+
// A line that is ABOUT the alias rather than a caller of it. Rule 8 above.
|
|
115
|
+
const ALIAS_COMMENTARY_RE = /\balias\b|\blegacy\b|\bpermanent\b|\bforever\b/i;
|
|
116
|
+
|
|
117
|
+
function isExcludedPath(relPath) {
|
|
118
|
+
const p = relPath.replace(/\\/g, '/');
|
|
119
|
+
if (ALIAS_EXCLUDED_TREES.test(p)) return true;
|
|
120
|
+
return ALIAS_EXCLUDED_PATHS.some((re) => re.test(p));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// isCountedLine — a line naming the alias that is a CALLER rather than commentary. Exported so the
|
|
124
|
+
// test and any future sweep share ONE decision (the gds-literal-scan precedent: a gate whose
|
|
125
|
+
// detector drifts from its sweep is worse than no gate).
|
|
126
|
+
function isCountedLine(line) {
|
|
127
|
+
if (!line.includes(ALIAS)) return false;
|
|
128
|
+
if (line.includes(CANONICAL)) return false;
|
|
129
|
+
if (ALIAS_COMMENTARY_RE.test(line)) return false;
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// countInText — alias occurrences on counted lines only. A line can legitimately carry more than
|
|
134
|
+
// one (two fetches, a usage banner), so occurrences are counted rather than lines: that is what
|
|
135
|
+
// makes the number fall when a caller is actually moved.
|
|
136
|
+
function countInText(text) {
|
|
137
|
+
let n = 0;
|
|
138
|
+
for (const line of String(text).split(/\r?\n/)) {
|
|
139
|
+
if (!isCountedLine(line)) continue;
|
|
140
|
+
n += (line.match(ALIAS_RE) || []).length;
|
|
141
|
+
}
|
|
142
|
+
return n;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function trackedFiles() {
|
|
146
|
+
const out = execFileSync('git', ['ls-files'], { cwd: ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
147
|
+
return out.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// scanAliasCallers — { total, byFile, scanned } across every tracked, countable file.
|
|
151
|
+
//
|
|
152
|
+
// `scanned` is the NON-VACUITY channel, and it is the whole reason this function returns it. A
|
|
153
|
+
// static scan breaks by finding nothing, and a green run looks identical to a working one: if
|
|
154
|
+
// `git ls-files` returns nothing, or every path is excluded, this reports total 0 and a caller
|
|
155
|
+
// that reads only `total` cannot tell a clean tree from a broken scanner. The ratchet and the test
|
|
156
|
+
// both assert `scanned > 0`, so an enumeration that collapses fails LOUDLY instead of passing.
|
|
157
|
+
//
|
|
158
|
+
// `files` and `readFile` are injectable for tests, so the real aggregation — exclusions, the
|
|
159
|
+
// commentary rule, per-file totals — is exercised without writing into the repo. The unit lane runs
|
|
160
|
+
// test files in parallel; a test that edited a tracked file would break whichever suite happened to
|
|
161
|
+
// be scanning the tree at that moment.
|
|
162
|
+
function scanAliasCallers({ files = null, readFile = null } = {}) {
|
|
163
|
+
const read = readFile || ((relPath) => fs.readFileSync(path.join(ROOT, relPath), 'utf8'));
|
|
164
|
+
const byFile = {};
|
|
165
|
+
let total = 0;
|
|
166
|
+
let scanned = 0;
|
|
167
|
+
for (const relPath of files || trackedFiles()) {
|
|
168
|
+
if (isExcludedPath(relPath)) continue;
|
|
169
|
+
let text;
|
|
170
|
+
try {
|
|
171
|
+
text = read(relPath);
|
|
172
|
+
} catch {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (text.includes(String.fromCharCode(0))) continue; // binary (a NUL byte)
|
|
176
|
+
scanned += 1;
|
|
177
|
+
const n = countInText(text);
|
|
178
|
+
if (n) {
|
|
179
|
+
byFile[relPath] = n;
|
|
180
|
+
total += n;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { total, byFile, scanned };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function main() {
|
|
187
|
+
const { total, byFile, scanned } = scanAliasCallers();
|
|
188
|
+
const ranked = Object.entries(byFile).sort((a, b) => b[1] - a[1]);
|
|
189
|
+
if (process.argv.includes('--json')) {
|
|
190
|
+
process.stdout.write(`${JSON.stringify({ total, byFile, scanned }, null, 2)}\n`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const show = process.argv.includes('--files') ? ranked : ranked.slice(0, 20);
|
|
194
|
+
console.log(`in-repo /api/gds callers: ${total} across ${ranked.length} file(s) (${scanned} file(s) scanned)`);
|
|
195
|
+
console.log('(the alias contract, the pinned OAuth callbacks, the uptime poller + smoke, the rename guardrails,');
|
|
196
|
+
console.log(' tests/, docs/, migrations/ and captured fixtures are excluded by construction; so is any line that');
|
|
197
|
+
console.log(' also names /api/bongos or carries the words alias / legacy / permanent / forever)');
|
|
198
|
+
console.log('');
|
|
199
|
+
for (const [f, n] of show) console.log(` ${String(n).padStart(5)} ${f}`);
|
|
200
|
+
if (show.length < ranked.length) console.log(` … and ${ranked.length - show.length} more file(s) — pass --files for all`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (require.main === module) main();
|
|
204
|
+
|
|
205
|
+
// Only what a consumer actually calls. The constants above stay module-private on purpose: an
|
|
206
|
+
// export nobody imports reads as dead code to knip and costs a ratchet point for nothing, and the
|
|
207
|
+
// rules are meant to be read here rather than re-implemented by a caller.
|
|
208
|
+
module.exports = {
|
|
209
|
+
isExcludedPath,
|
|
210
|
+
isCountedLine,
|
|
211
|
+
countInText,
|
|
212
|
+
scanAliasCallers,
|
|
213
|
+
};
|
|
@@ -347,6 +347,15 @@ const METRIC_HINTS = {
|
|
|
347
347
|
+ 'Write Bongos instead. Run `node scripts/gds/gds-literal-scan.js` to see every counted file. '
|
|
348
348
|
+ 'Version ids (GDS-V##), docs/adr, docs/session-logs, docs/audits, limitations and migrations '
|
|
349
349
|
+ 'are excluded by construction — if your literal is history, it belongs in one of those.',
|
|
350
|
+
legacy_api_gds_callers:
|
|
351
|
+
' — a NEW in-repo caller was written against the permanent `/api/gds` alias (task 1004064, '
|
|
352
|
+
+ 'idea 1001030). Ask for `/api/bongos` instead: the alias exists for callers we CANNOT update '
|
|
353
|
+
+ '(shipped Dev Box binaries, live dev boxes), not for code in this repo. Run '
|
|
354
|
+
+ '`node scripts/gds/api-alias-caller-scan.js --files` to see every counted line. The alias '
|
|
355
|
+
+ 'contract, the pinned OAuth callbacks, the uptime poller + smoke, the rename guardrails, '
|
|
356
|
+
+ 'tests/, docs/, migrations/ and captured fixtures are excluded by construction — and so is '
|
|
357
|
+
+ 'any line that also names /api/bongos or carries the words alias / legacy / permanent / forever, '
|
|
358
|
+
+ 'so if your line is ABOUT the alias rather than a call to it, say so on the line.',
|
|
350
359
|
};
|
|
351
360
|
|
|
352
361
|
function compareToBaselines(metrics, baselines) {
|
|
@@ -379,6 +388,27 @@ function gdsMetrics() {
|
|
|
379
388
|
return { legacy_gds_literals: scanGdsLiterals().total };
|
|
380
389
|
}
|
|
381
390
|
|
|
391
|
+
// legacy_api_gds_callers — the LOWERCASE half of the same rename (task 1004064, idea 1001030).
|
|
392
|
+
// `legacy_gds_literals` above is case-sensitive on the shouting acronym, so it read 1135 both
|
|
393
|
+
// before and after task 1003707 moved 665 in-repo callers off `/api/gds` — correct for what it
|
|
394
|
+
// measures, and exactly why a caller written against the alias tomorrow moves no number and reds
|
|
395
|
+
// no build. This counts that spelling instead. Detector + the reason for every exclusion:
|
|
396
|
+
// scripts/gds/api-alias-caller-scan.js.
|
|
397
|
+
//
|
|
398
|
+
// It FAILS CLOSED on a collapsed enumeration. A static scan breaks by finding nothing, and a
|
|
399
|
+
// green run looks identical to a working one — so if the scanner scanned no files at all, the
|
|
400
|
+
// metric is OMITTED with a reason on stderr rather than reported as a flattering 0 that would
|
|
401
|
+
// invite someone to ratchet the baseline down to it.
|
|
402
|
+
function apiAliasCallerMetrics() {
|
|
403
|
+
const { scanAliasCallers } = require('./api-alias-caller-scan.js');
|
|
404
|
+
const { total, scanned } = scanAliasCallers();
|
|
405
|
+
if (!scanned) {
|
|
406
|
+
process.stderr.write('fitness-ratchets: legacy_api_gds_callers NOT MEASURED — the caller scan enumerated ZERO files, which is a scan defect, not a clean tree\n');
|
|
407
|
+
return {};
|
|
408
|
+
}
|
|
409
|
+
return { legacy_api_gds_callers: total };
|
|
410
|
+
}
|
|
411
|
+
|
|
382
412
|
// skill_listing_chars — the resident cost of the skill listing (every name +
|
|
383
413
|
// description, both skill homes). Ratcheted rather than merely warned about
|
|
384
414
|
// because this number has now been trimmed three times (tasks 1003548, 1003587,
|
|
@@ -411,6 +441,7 @@ function computeMetrics({ withDocs = true } = {}) {
|
|
|
411
441
|
require_cycle_count: countCycles(requireGraph(contents)),
|
|
412
442
|
console_call_count: consoleCallCount(contents),
|
|
413
443
|
...gdsMetrics(),
|
|
444
|
+
...apiAliasCallerMetrics(),
|
|
414
445
|
...skillListingMetrics(),
|
|
415
446
|
...(withDocs ? docMetrics() : {}),
|
|
416
447
|
};
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -225,7 +225,7 @@ function checkGeneratedArtifactsFresh() {
|
|
|
225
225
|
try {
|
|
226
226
|
execFileSync(process.execPath, [path.join(__dirname, 'gen-file-map.js'), '--check'], { cwd: REPO_ROOT, stdio: 'pipe' });
|
|
227
227
|
} catch {
|
|
228
|
-
violations.push('docs/file-map.md generated sections (.claude/skills/, .claude/scheduled-tasks/) are STALE
|
|
228
|
+
violations.push('docs/file-map.md generated sections (.claude/skills/, .claude/scheduled-tasks/) are STALE, a skill/routine has no note in docs/file-map.notes.json, or a directory under modules/ has no file-map entry at all — run `node scripts/gds/gen-file-map.js --check` for the exact list, then fix and commit (ADR 0066; module coverage: task 1004066).');
|
|
229
229
|
}
|
|
230
230
|
try {
|
|
231
231
|
// Also structurally validates the spec (exit 1 on invalid OpenAPI 3.1) beyond the drift check (exit 2). Task 1918.
|
|
@@ -16,6 +16,16 @@
|
|
|
16
16
|
// • every skill/routine dir on disk MUST have a note → a new skill can't land undescribed;
|
|
17
17
|
// • every note MUST map to a dir that exists → the map can't carry ghosts;
|
|
18
18
|
// • the rendered block MUST match what's committed → no stale tree.
|
|
19
|
+
// MODULE COVERAGE (task 1004066). The same honor system that let a skill land unmapped let a whole
|
|
20
|
+
// MODULE land unmapped: `modules/agents`, `modules/government` and `modules/specialities` were all
|
|
21
|
+
// on disk with nothing in the map, and the two that got fixed before this got fixed BY HAND — which
|
|
22
|
+
// is the point, since hand-patching a drifting map does not stop the drift. The modules/ block is
|
|
23
|
+
// not generated (each entry is a paragraph of prose no directory walk could write, and generating
|
|
24
|
+
// the fenced repo tree is the expensive option this task ruled out), so what is enforced is the
|
|
25
|
+
// cheap half: a set comparison asserting every directory under modules/ is documented SOMEWHERE in
|
|
26
|
+
// the map — either a `modules/<key>/…` path reference or a `<key>/` entry inside the modules block.
|
|
27
|
+
// A new module now fails --check until someone writes its line.
|
|
28
|
+
//
|
|
19
29
|
// scripts/gds/ is deliberately NOT generated: it is a curated highlight reel of ~20 of ~70 files
|
|
20
30
|
// (generating it would bloat it with low-signal entries), and it sits mid-tree inside the big
|
|
21
31
|
// fenced repo tree where HTML-comment markers can't live cleanly. It stays hand-curated.
|
|
@@ -51,6 +61,13 @@ const SECTIONS = [
|
|
|
51
61
|
{ id: 'scheduled-tasks', dir: '.claude/scheduled-tasks', notesKey: 'scheduled-tasks' },
|
|
52
62
|
];
|
|
53
63
|
|
|
64
|
+
// modules/ is NOT generated (task 1004066): each entry is a paragraph of prose no directory walk
|
|
65
|
+
// could write, and generating it would force the whole fenced repo tree through a generator —
|
|
66
|
+
// explicitly out of scope. What IS enforceable is COVERAGE: every module on disk must be documented
|
|
67
|
+
// somewhere in the map. That set comparison is the cheap half of the same contract the skills
|
|
68
|
+
// section already carries, and it is what stops a whole vertical slice landing unmapped.
|
|
69
|
+
const MODULES_DIR = 'modules';
|
|
70
|
+
|
|
54
71
|
const begin = (id) => `<!-- BEGIN GENERATED FILE-MAP ${id} (scripts/gds/gen-file-map.js — do not hand-edit; notes live in docs/file-map.notes.json) -->`;
|
|
55
72
|
const end = (id) => `<!-- END GENERATED FILE-MAP ${id} -->`;
|
|
56
73
|
|
|
@@ -107,6 +124,72 @@ function buildModel() {
|
|
|
107
124
|
return SECTIONS.map((s) => buildSection(s, notes));
|
|
108
125
|
}
|
|
109
126
|
|
|
127
|
+
// ── module coverage (task 1004066) ────────────────────────────────────────────
|
|
128
|
+
// A module counts as documented when the map either references its PATH (`modules/<key>/…`
|
|
129
|
+
// anywhere) or carries a `<key>/` entry line inside the `modules/` tree block. Both forms are in
|
|
130
|
+
// use today, so the check accepts both rather than inventing a third the map would fail on.
|
|
131
|
+
|
|
132
|
+
// Both matchers CAPTURE the whole path segment rather than testing for a given key. That is what
|
|
133
|
+
// makes the prefix hazard structural instead of something a lookahead has to remember: a mention of
|
|
134
|
+
// `modules/agents-extra` yields the key `agents-extra`, so it can never vouch for `agents`. It also
|
|
135
|
+
// means the map is scanned ONCE per run instead of twice per module.
|
|
136
|
+
const PATH_REF_RE = /modules\/([A-Za-z0-9_.-]+)/g;
|
|
137
|
+
const ENTRY_RE = /[├└]──\s+([A-Za-z0-9_.-]+)\//g;
|
|
138
|
+
|
|
139
|
+
// The slice of the fenced repo tree between the `├── modules/` header and the next sibling at the
|
|
140
|
+
// same depth. Scoping the entry search here matters: `.claude/skills/` lists dirs in the same
|
|
141
|
+
// `├── name/` shape, so an unscoped match would let a SKILL named after a module vouch for it.
|
|
142
|
+
function modulesBlock(mapText) {
|
|
143
|
+
const lines = String(mapText).split(/\r?\n/);
|
|
144
|
+
const start = lines.findIndex((l) => new RegExp(`^[├└]── ${MODULES_DIR}/`).test(l));
|
|
145
|
+
if (start === -1) return '';
|
|
146
|
+
let stop = lines.length;
|
|
147
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
148
|
+
if (/^[├└]── /.test(lines[i])) { stop = i; break; }
|
|
149
|
+
}
|
|
150
|
+
return lines.slice(start, stop).join('\n');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Every module key the map documents: any `modules/<key>` path reference anywhere, plus any
|
|
154
|
+
// `<key>/` entry line inside the modules block. Both forms are in use today, so both count.
|
|
155
|
+
function documentedModuleKeys(mapText, block) {
|
|
156
|
+
const keys = new Set();
|
|
157
|
+
for (const m of String(mapText).matchAll(PATH_REF_RE)) keys.add(m[1]);
|
|
158
|
+
for (const m of String(block).matchAll(ENTRY_RE)) keys.add(m[1]);
|
|
159
|
+
return keys;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The single-key form — the readable one to assert against.
|
|
163
|
+
function isModuleDocumented(mapText, block, key) {
|
|
164
|
+
return documentedModuleKeys(mapText, block).has(key);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Every immediate subdirectory of modules/, or [] when the tree carries no modules/ at all (a
|
|
168
|
+
// consumer instance that enabled none — nothing to document).
|
|
169
|
+
function listModules() {
|
|
170
|
+
return listSubdirs(path.join(REPO_ROOT, MODULES_DIR));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The set comparison. Returns { onDisk, undocumented, defects } — `defects` is the non-vacuity
|
|
174
|
+
// channel: this check breaks by comparing two empty sets and reporting nothing, so a state that
|
|
175
|
+
// makes the comparison meaningless (modules on disk but no readable map, or no modules/ block to
|
|
176
|
+
// read entries out of) is reported as a SCAN DEFECT rather than passing quietly.
|
|
177
|
+
function moduleCoverage(mapText) {
|
|
178
|
+
const onDisk = listModules();
|
|
179
|
+
const defects = [];
|
|
180
|
+
if (!onDisk.length) return { onDisk, undocumented: [], defects };
|
|
181
|
+
const text = typeof mapText === 'string' ? mapText : (fs.existsSync(MAP_PATH) ? fs.readFileSync(MAP_PATH, 'utf8') : '');
|
|
182
|
+
if (!text.trim()) {
|
|
183
|
+
defects.push(`${onDisk.length} module(s) on disk but docs/file-map.md is missing or empty — the module-coverage check cannot run`);
|
|
184
|
+
return { onDisk, undocumented: [], defects };
|
|
185
|
+
}
|
|
186
|
+
const block = modulesBlock(text);
|
|
187
|
+
if (!block) defects.push('docs/file-map.md has no `modules/` block in its repo tree — module entries cannot be read (path references are still checked)');
|
|
188
|
+
const documented = documentedModuleKeys(text, block);
|
|
189
|
+
const undocumented = onDisk.filter((key) => !documented.has(key));
|
|
190
|
+
return { onDisk, undocumented, defects };
|
|
191
|
+
}
|
|
192
|
+
|
|
110
193
|
// Render one section's fenced tree block (the bytes that live between its markers). Arrows are
|
|
111
194
|
// column-aligned to the longest label, matching the hand-formatted look of the rest of the map.
|
|
112
195
|
function renderSection(model) {
|
|
@@ -144,6 +227,7 @@ function computeWrites() {
|
|
|
144
227
|
const stale = model.flatMap((m) => m.stale);
|
|
145
228
|
const writes = [];
|
|
146
229
|
let markersMissing = [];
|
|
230
|
+
let mapText = '';
|
|
147
231
|
if (fs.existsSync(MAP_PATH)) {
|
|
148
232
|
let cur = fs.readFileSync(MAP_PATH, 'utf8');
|
|
149
233
|
let next = cur;
|
|
@@ -152,8 +236,10 @@ function computeWrites() {
|
|
|
152
236
|
next = injectSection(next, m);
|
|
153
237
|
}
|
|
154
238
|
if (!sameContent(next, cur)) writes.push({ full: MAP_PATH, content: next, rel: 'docs/file-map.md' });
|
|
239
|
+
mapText = next; // coverage reads the POST-generation text, so a regen never changes the verdict
|
|
155
240
|
}
|
|
156
|
-
|
|
241
|
+
const modules = moduleCoverage(mapText);
|
|
242
|
+
return { model, writes, missing, stale, markersMissing, modules };
|
|
157
243
|
}
|
|
158
244
|
|
|
159
245
|
function main() {
|
|
@@ -162,7 +248,7 @@ function main() {
|
|
|
162
248
|
process.stdout.write(JSON.stringify(buildModel(), null, 2) + '\n');
|
|
163
249
|
return;
|
|
164
250
|
}
|
|
165
|
-
const { writes, missing, stale, markersMissing } = computeWrites();
|
|
251
|
+
const { writes, missing, stale, markersMissing, modules } = computeWrites();
|
|
166
252
|
|
|
167
253
|
// A missing note (dir without a note) or a stale note (note without a dir) is a hard failure in
|
|
168
254
|
// BOTH modes: in --check it fails CI; in write mode we still report it (and render a visible
|
|
@@ -171,6 +257,8 @@ function main() {
|
|
|
171
257
|
if (missing.length) problems.push(`notes MISSING for: ${missing.join(', ')} — add a one-line note to docs/file-map.notes.json`);
|
|
172
258
|
if (stale.length) problems.push(`notes are STALE (no such dir) — remove from docs/file-map.notes.json: ${stale.join(', ')}`);
|
|
173
259
|
if (markersMissing.length) problems.push(`docs/file-map.md is missing generated markers for: ${markersMissing.join(', ')}`);
|
|
260
|
+
if (modules.undocumented.length) problems.push(`modules with NO docs/file-map.md entry: ${modules.undocumented.map((k) => `${MODULES_DIR}/${k}`).join(', ')} — add an entry to the modules/ block of docs/file-map.md (task 1004066)`);
|
|
261
|
+
for (const d of modules.defects) problems.push(`module-coverage SCAN DEFECT — ${d}`);
|
|
174
262
|
|
|
175
263
|
if (checkOnly) {
|
|
176
264
|
if (writes.length || problems.length) {
|
|
@@ -197,6 +285,12 @@ if (require.main === module) main();
|
|
|
197
285
|
module.exports = {
|
|
198
286
|
materializedNames,
|
|
199
287
|
SECTIONS,
|
|
288
|
+
MODULES_DIR,
|
|
289
|
+
listModules,
|
|
290
|
+
modulesBlock,
|
|
291
|
+
documentedModuleKeys,
|
|
292
|
+
isModuleDocumented,
|
|
293
|
+
moduleCoverage,
|
|
200
294
|
begin,
|
|
201
295
|
end,
|
|
202
296
|
buildModel,
|
|
@@ -27,7 +27,7 @@ const STEPS = [
|
|
|
27
27
|
{ key: 'fitness', label: 'fitness.js (architecture + budgets + freshness gate)', argv: ['scripts/gds/fitness.js'], fix: 'read the ✗ lines above; each names its fix' },
|
|
28
28
|
{ key: 'repo-map', label: 'nested CLAUDE.md symbol blocks fresh', argv: ['scripts/gds/gen-repo-map.js', '--check-block'], fix: 'node scripts/gds/gen-repo-map.js' },
|
|
29
29
|
{ key: 'session-index', label: 'CLAUDE.md §13 session snippet fresh', argv: ['scripts/gds/gen-session-index.js', '--check-block'], fix: 'node scripts/gds/gen-session-index.js' },
|
|
30
|
-
{ key: 'file-map', label: 'docs/file-map.md skills/routines sections fresh', argv: ['scripts/gds/gen-file-map.js', '--check'], fix: 'node scripts/gds/gen-file-map.js (+ a note in docs/file-map.notes.json for a new skill)' },
|
|
30
|
+
{ key: 'file-map', label: 'docs/file-map.md skills/routines sections fresh + every module documented', argv: ['scripts/gds/gen-file-map.js', '--check'], fix: 'node scripts/gds/gen-file-map.js (+ a note in docs/file-map.notes.json for a new skill, or a modules/ entry in docs/file-map.md for a new module)' },
|
|
31
31
|
{ key: 'api-docs', label: 'docs/api/openapi.json + api-reference fresh', argv: ['scripts/gds/gen-api-docs.js', '--check'], fix: 'node scripts/gds/gen-api-docs.js' },
|
|
32
32
|
{ key: 'copy-registry', label: 'docs/copy-registry.json fresh', argv: ['scripts/gds/copy-inventory.js', '--check'], fix: 'node scripts/gds/copy-inventory.js', optional: true },
|
|
33
33
|
{ key: 'linkify', label: 'no bare #NNN task refs in committed markdown', argv: ['scripts/gds/linkify-refs.js', '--check'], fix: 'node scripts/gds/linkify-refs.js', optional: true },
|
|
@@ -231,7 +231,20 @@ app.use((req, res, next) => {
|
|
|
231
231
|
return res.end(png);
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
// READ-SHAPED POSTS. A handful of routes are POSTs only because the INPUT is a
|
|
235
|
+
// draft rather than an id — they write nothing. Serving them the 501 below made
|
|
236
|
+
// their surfaces permanently un-demoable here: the page renders its degraded
|
|
237
|
+
// state forever, which is the "a page that only ever renders its empty state
|
|
238
|
+
// cannot be redesigned by looking" trap modules/hall-ui/CLAUDE.md names. They
|
|
239
|
+
// fall through to the ordinary fixture lookup instead.
|
|
240
|
+
//
|
|
241
|
+
// AN ALLOWLIST, and it stays short. Anything absent is still refused, so a real
|
|
242
|
+
// write cannot reach the harness by growing a comment — adding a member means
|
|
243
|
+
// asserting that route writes nothing, which is checkable at its handler.
|
|
244
|
+
const READ_SHAPED_POSTS = new Set([
|
|
245
|
+
'inbox/preview-landing', // the landing verdict for a draft (task 1004083)
|
|
246
|
+
]);
|
|
247
|
+
if (req.method !== 'GET' && !READ_SHAPED_POSTS.has(apiPath)) {
|
|
235
248
|
return res.status(501).json({ error: { code: 'preview_readonly', message: 'the preview harness serves fixtures; writes are not wired' } });
|
|
236
249
|
}
|
|
237
250
|
// Your own account wins over the checked-in snapshot; a me-scoped read that
|
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.911'; // 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');
|