@bongos/core 1.19.910 → 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 +47 -27
- 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/module-api-changelog.md +3 -1
- 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/hall-preview/server.js +14 -1
- package/src/module-api.js +1 -1
- package/tests/api_alias_caller_ratchet.mjs +130 -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
|
};
|
|
@@ -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');
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// tests/api_alias_caller_ratchet.mjs — the lowercase `/api/gds` CALLER ratchet
|
|
2
|
+
// (task 1004064, from idea 1001030). Mirrors tests/fitness_gds_ratchet.mjs, its
|
|
3
|
+
// case-sensitive sibling.
|
|
4
|
+
//
|
|
5
|
+
// WHAT THIS FILE IS DEFENDING AGAINST. The brief names the trap exactly: "this is a static scan;
|
|
6
|
+
// it breaks by finding nothing, and a green run looks identical to a working one." Every case
|
|
7
|
+
// below is written so that a scanner which enumerates nothing, excludes everything, or counts
|
|
8
|
+
// nothing FAILS here rather than passing quietly. The aggregation cases inject their own file
|
|
9
|
+
// list and reader, so the real exclusion + counting path runs without writing into the repo —
|
|
10
|
+
// the unit lane runs test files in parallel, and a test that edited a tracked file would break
|
|
11
|
+
// whichever suite happened to be scanning the tree at that moment.
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { test } from 'node:test';
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const scan = require(path.join(ROOT, 'scripts', 'gds', 'api-alias-caller-scan.js'));
|
|
22
|
+
|
|
23
|
+
// A reader over an in-memory tree — no repo writes.
|
|
24
|
+
const fake = (tree) => ({ files: Object.keys(tree), readFile: (rel) => tree[rel] });
|
|
25
|
+
|
|
26
|
+
test('the counter COUNTS — a file that should count, does (the trap case)', () => {
|
|
27
|
+
const r = scan.scanAliasCallers(fake({
|
|
28
|
+
'src/bongos/routes/thing.js': "await fetch(`${base}/api/gds/tasks/${id}`);\n",
|
|
29
|
+
}));
|
|
30
|
+
assert.equal(r.total, 1, 'a plain in-repo caller must be counted');
|
|
31
|
+
assert.deepEqual(Object.keys(r.byFile), ['src/bongos/routes/thing.js']);
|
|
32
|
+
assert.equal(r.scanned, 1, 'and the file must actually have been scanned');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('a collapsed enumeration is visible as scanned=0, not as a flattering total of 0', () => {
|
|
36
|
+
const empty = scan.scanAliasCallers(fake({}));
|
|
37
|
+
assert.equal(empty.total, 0);
|
|
38
|
+
assert.equal(empty.scanned, 0, 'scanned is what distinguishes "nothing to find" from "nothing looked at"');
|
|
39
|
+
|
|
40
|
+
const allExcluded = scan.scanAliasCallers(fake({
|
|
41
|
+
'tests/x.mjs': '/api/gds\n',
|
|
42
|
+
'docs/x.md': '/api/gds\n',
|
|
43
|
+
}));
|
|
44
|
+
assert.equal(allExcluded.total, 0);
|
|
45
|
+
assert.equal(allExcluded.scanned, 0, 'an every-path-excluded run is also scanned=0 — the same signal');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('the REAL scan enumerates the repo and lands on the committed baseline', () => {
|
|
49
|
+
const r = scan.scanAliasCallers();
|
|
50
|
+
assert.ok(r.scanned > 500, `the real tree must enumerate substantially (got ${r.scanned})`);
|
|
51
|
+
const baselines = JSON.parse(readFileSync(path.join(ROOT, 'config', 'fitness-baselines.json'), 'utf8'));
|
|
52
|
+
const floor = baselines.metrics.legacy_api_gds_callers;
|
|
53
|
+
assert.equal(typeof floor, 'number', 'the metric must have a baseline, or the ratchet only warns');
|
|
54
|
+
assert.ok(r.total <= floor, `caller count ${r.total} is ABOVE baseline ${floor} — a new /api/gds caller entered the tree`);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('the exclusions are real and each one is reachable', () => {
|
|
58
|
+
// Rule numbers refer to the header of api-alias-caller-scan.js.
|
|
59
|
+
for (const p of [
|
|
60
|
+
'src/bongos/api-prefix.js', // 1 the alias contract
|
|
61
|
+
'src/bongos/routes/auth.js', // 2 the pinned OAuth callbacks
|
|
62
|
+
'modules/provisioning/provisioning.js',
|
|
63
|
+
'src/bongos/uptime-poller.js', // 3 the live proof
|
|
64
|
+
'scripts/gds/smoke-gds.js',
|
|
65
|
+
'scripts/gds/gds-literal-scan.js', // 4 the rename guardrails
|
|
66
|
+
'scripts/gds/fitness-ratchets.js',
|
|
67
|
+
'scripts/gds/api-alias-caller-scan.js',
|
|
68
|
+
'tests/api_alias_exceptions.mjs', // 5 tests
|
|
69
|
+
'modules/dev-box/app/test/thing.mjs',
|
|
70
|
+
'docs/architecture.md', // 6 docs
|
|
71
|
+
'migrations/033_audit_log.sql', // 7 migrations
|
|
72
|
+
'scripts/hall-preview/fixtures/tasks.json', // 9 captured fixtures
|
|
73
|
+
'node_modules/pkg/index.js', // vendored
|
|
74
|
+
]) {
|
|
75
|
+
assert.equal(scan.isExcludedPath(p), true, `${p} must be excluded`);
|
|
76
|
+
}
|
|
77
|
+
// …and the exclusion is not a blanket that swallows the tree.
|
|
78
|
+
for (const p of [
|
|
79
|
+
'src/bongos/serve-internal.js',
|
|
80
|
+
'modules/lifecycle/abandon-leftovers.js',
|
|
81
|
+
'clients/bongos-client/index.mjs', // a .mjs that is NOT a test — a blanket \.mjs$ would blind the ratchet
|
|
82
|
+
'bin/bongos.js',
|
|
83
|
+
'CLAUDE.md',
|
|
84
|
+
]) {
|
|
85
|
+
assert.equal(scan.isExcludedPath(p), false, `${p} must NOT be excluded`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('a line ABOUT the alias is not a caller of it — and the rule is not so loose it excuses everything', () => {
|
|
90
|
+
// Excluded: names the canonical prefix too, or says what it is.
|
|
91
|
+
assert.equal(scan.isCountedLine('mount /api/bongos and /api/gds'), false);
|
|
92
|
+
assert.equal(scan.isCountedLine('// /api/gds is a permanent alias'), false);
|
|
93
|
+
assert.equal(scan.isCountedLine('// the legacy /api/gds mount'), false);
|
|
94
|
+
assert.equal(scan.isCountedLine('// /api/gds stays FOREVER'), false);
|
|
95
|
+
// Counted: a bare caller, with none of those tells.
|
|
96
|
+
assert.equal(scan.isCountedLine('const r = await fetch(`${base}/api/gds/me`);'), true);
|
|
97
|
+
assert.equal(scan.isCountedLine(' node scripts/gds/api.js GET /api/gds/tasks/1'), true);
|
|
98
|
+
// And a line with no alias at all is never counted, whatever else it says.
|
|
99
|
+
assert.equal(scan.isCountedLine('fetch(`${base}/api/bongos/me`)'), false);
|
|
100
|
+
assert.equal(scan.isCountedLine('a line about a legacy alias, forever'), false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('occurrences are counted, not lines — moving ONE of two callers on a line lowers the number', () => {
|
|
104
|
+
assert.equal(scan.countInText('f("/api/gds/a"); f("/api/gds/b");\n'), 2);
|
|
105
|
+
assert.equal(scan.countInText('f("/api/bongos/a"); f("/api/gds/b");\n'), 0, 'the canonical name on the line is the alias-commentary rule, by design');
|
|
106
|
+
assert.equal(scan.countInText('nothing here\n'), 0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('the ratchet is WIRED — computeMetrics reports the metric and compareToBaselines fails a rise', () => {
|
|
110
|
+
const ratchets = require(path.join(ROOT, 'scripts', 'gds', 'fitness-ratchets.js'));
|
|
111
|
+
const baselines = JSON.parse(readFileSync(path.join(ROOT, 'config', 'fitness-baselines.json'), 'utf8')).metrics;
|
|
112
|
+
|
|
113
|
+
// A rise is a violation…
|
|
114
|
+
const worse = ratchets.compareToBaselines(
|
|
115
|
+
{ legacy_api_gds_callers: baselines.legacy_api_gds_callers + 1 },
|
|
116
|
+
baselines,
|
|
117
|
+
);
|
|
118
|
+
assert.equal(worse.violations.length, 1, 'one more caller must RED the build');
|
|
119
|
+
assert.match(worse.violations[0], /legacy_api_gds_callers regressed/);
|
|
120
|
+
assert.match(worse.violations[0], /api-alias-caller-scan/, 'the hint must name the tool that lists the offenders');
|
|
121
|
+
|
|
122
|
+
// …a fall is a ratchet-down warning, not a failure.
|
|
123
|
+
const better = ratchets.compareToBaselines(
|
|
124
|
+
{ legacy_api_gds_callers: baselines.legacy_api_gds_callers - 1 },
|
|
125
|
+
baselines,
|
|
126
|
+
);
|
|
127
|
+
assert.equal(better.violations.length, 0);
|
|
128
|
+
assert.equal(better.warnings.length, 1);
|
|
129
|
+
assert.match(better.warnings[0], /ratchet-down available/);
|
|
130
|
+
});
|