@bongos/core 1.19.889 → 1.19.891
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 +39 -24
- package/docs/module-api-changelog.md +4 -0
- package/modules/grading/grader-rubric.json +17 -7
- package/modules/grading/grader-score.js +7 -1
- package/modules/grading/grader-workers/extract-json.js +7 -1
- package/modules/grading/grader-workers/index.js +43 -0
- package/modules/grading/grader-workers/reader.js +218 -0
- package/modules/grading/grader.js +47 -11
- package/modules/lifecycle/cascade.js +23 -13
- package/modules/lifecycle/tests/cascade.mjs +18 -9
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/build-cli-package.js +4 -0
- package/scripts/gds/ship-grade.js +28 -2
- package/scripts/gds/status.js +89 -13
- package/scripts/gds/version-literal-guard.js +22 -4
- package/src/module-api.js +1 -1
- package/tests/extract_json.mjs +8 -0
- package/tests/grader_reader_lens.mjs +257 -0
- package/tests/status_default_version.mjs +136 -0
- package/tests/version_literals.mjs +92 -0
|
@@ -110,7 +110,27 @@ function describeOutage(gating) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
function aggregatePanel(workerResults, meta = {}) {
|
|
113
|
-
|
|
113
|
+
// task 1003977 — THE NON-GATING PARTITION, before any gate arithmetic.
|
|
114
|
+
//
|
|
115
|
+
// A non-gating worker (today: the Reader, the advisory copy lens) contributes
|
|
116
|
+
// its scores and issues to the RECORD and nothing at all to the GATE. It is
|
|
117
|
+
// removed here, once, rather than special-cased in each of the counts below —
|
|
118
|
+
// pass/concerns/fail counts, the error and outage detection, the confidence
|
|
119
|
+
// median and every branch of the verdict all read `gateResults`.
|
|
120
|
+
//
|
|
121
|
+
// Removing it from the counts is why a Reader outage cannot reach #943's
|
|
122
|
+
// "every gating worker is missing signal" branch, and why a Reader cannot
|
|
123
|
+
// nudge the confidence median that drives the server-side gate. `N` stays the
|
|
124
|
+
// GATE's denominator so the verdict strings keep meaning what they said
|
|
125
|
+
// before ("2/4 gating worker(s) failed" counts the four that can fail).
|
|
126
|
+
//
|
|
127
|
+
// ADVISORY_WORKERS below is a DIFFERENT, weaker thing: narc is advisory in
|
|
128
|
+
// that a narc-only fail downgrades to manual-confirm. A non-gating worker
|
|
129
|
+
// never reaches even that.
|
|
130
|
+
const isNonGating = (w) => w && workers.isNonGatingKind(w.kind);
|
|
131
|
+
const nonGatingResults = workerResults.filter(isNonGating);
|
|
132
|
+
const gateResults = workerResults.filter((w) => !isNonGating(w));
|
|
133
|
+
const N = gateResults.length;
|
|
114
134
|
|
|
115
135
|
// Score axes — median across workers that reported each. Reuses the
|
|
116
136
|
// module-level median() helper (rounded to one decimal).
|
|
@@ -124,6 +144,12 @@ function aggregatePanel(workerResults, meta = {}) {
|
|
|
124
144
|
// Phase 2.2 (#421): Efficiency Monger reports efficiency_score. Same
|
|
125
145
|
// additive treatment as security_score.
|
|
126
146
|
const efficiencyScore = median(workerResults.map((w) => w.scores?.efficiency_score));
|
|
147
|
+
// task 1003977: the Reader reports copy_score. Recorded like security_score
|
|
148
|
+
// and efficiency_score before it — additive, and read from the FULL result
|
|
149
|
+
// list rather than gateResults, because the whole point is that a non-gating
|
|
150
|
+
// worker's SIGNAL lands in the record even though its verdict never reaches
|
|
151
|
+
// the gate. Null whenever no Reader ran, which is every non-copy-bearing ship.
|
|
152
|
+
const copyScore = median(workerResults.map((w) => w.scores?.copy_score));
|
|
127
153
|
|
|
128
154
|
const economyPenalty =
|
|
129
155
|
economy != null ? Math.max(0, (THRESHOLD - economy) / 2) : 0;
|
|
@@ -131,8 +157,8 @@ function aggregatePanel(workerResults, meta = {}) {
|
|
|
131
157
|
const blended = ff != null && cq != null ? r1((ff + cq) / 2) : 0;
|
|
132
158
|
|
|
133
159
|
// Verdict counts.
|
|
134
|
-
const passCount =
|
|
135
|
-
const concernsCount =
|
|
160
|
+
const passCount = gateResults.filter((w) => w.verdict === 'pass').length;
|
|
161
|
+
const concernsCount = gateResults.filter((w) => w.verdict === 'concerns').length;
|
|
136
162
|
// A crashed worker sets verdict='fail' as a DEFAULT in its computeVerdict()
|
|
137
163
|
// subprocess_error / parse_error / schema_error early returns (see each
|
|
138
164
|
// worker module) — that is MISSING SIGNAL, not a substantive finding, and
|
|
@@ -149,12 +175,12 @@ function aggregatePanel(workerResults, meta = {}) {
|
|
|
149
175
|
const hasSubstantiveIssue = (w) =>
|
|
150
176
|
(w.issues || []).some((i) => i.severity === 'blocker' || i.severity === 'major');
|
|
151
177
|
const isMissingSignal = (w) => !!w.error && !hasSubstantiveIssue(w);
|
|
152
|
-
const failCount =
|
|
178
|
+
const failCount = gateResults.filter((w) => w.verdict === 'fail' && !isMissingSignal(w)).length;
|
|
153
179
|
// NOTE (#473/#589): signals.error_count counts only isMissingSignal workers — i.e. a worker that
|
|
154
180
|
// errored AND surfaced no blocking issue. A worker that errored but still raised a blocker/major
|
|
155
181
|
// is a real finding, so it counts in fail_count (above), NOT here. This is a deliberate semantic
|
|
156
182
|
// change from the old "all w.error" meaning; keep error_count = missing-signal-only.
|
|
157
|
-
const errorCount =
|
|
183
|
+
const errorCount = gateResults.filter((w) => isMissingSignal(w)).length;
|
|
158
184
|
|
|
159
185
|
// V3.R85 (#481): the Narc (functional_fidelity) is ADVISORY in the gate —
|
|
160
186
|
// Lars's call 2026-05-30 after #303, where over-literal acceptance-criteria
|
|
@@ -188,8 +214,8 @@ function aggregatePanel(workerResults, meta = {}) {
|
|
|
188
214
|
// taken out by a grader outage, so it is not evidence either way about whether the
|
|
189
215
|
// panel ran. Task 1003574.
|
|
190
216
|
const isDeterministic = (w) => w && w.model === 'deterministic';
|
|
191
|
-
const nonAdvisory =
|
|
192
|
-
const gating = nonAdvisory.length ? nonAdvisory :
|
|
217
|
+
const nonAdvisory = gateResults.filter((w) => !isAdvisory(w));
|
|
218
|
+
const gating = nonAdvisory.length ? nonAdvisory : gateResults;
|
|
193
219
|
const gatingFail = gating.filter((w) => w.verdict === 'fail' && !isMissingSignal(w)).length;
|
|
194
220
|
const gatingError = gating.filter((w) => isMissingSignal(w)).length;
|
|
195
221
|
const gatingConcerns = gating.filter((w) => w.verdict === 'concerns').length;
|
|
@@ -228,15 +254,15 @@ function aggregatePanel(workerResults, meta = {}) {
|
|
|
228
254
|
const gatingErrorAll = subagentGating.length > 0 && subagentGating.every((w) => isMissingSignal(w));
|
|
229
255
|
// Advisory-worker outcomes — never a hard block. Uses isAdvisory so a Narc
|
|
230
256
|
// that DID raise a blocker is treated as gating (in `nonAdvisory`), not here.
|
|
231
|
-
const advisoryFail =
|
|
232
|
-
const advisoryConcerns =
|
|
257
|
+
const advisoryFail = gateResults.some((w) => isAdvisory(w) && w.verdict === 'fail' && !isMissingSignal(w));
|
|
258
|
+
const advisoryConcerns = gateResults.some((w) => isAdvisory(w) && w.verdict === 'concerns');
|
|
233
259
|
|
|
234
260
|
// Confidence — median for pass, fallthrough for concerns, 0 for fail.
|
|
235
261
|
// (Drives the server-side gate; see file header.) Computed UNROUNDED —
|
|
236
262
|
// confidence sits near the 0.4 gate cap so the module-level median()
|
|
237
263
|
// helper (which rounds via r1()) would risk nudging a borderline value
|
|
238
264
|
// across the boundary. Inlined for that one-decimal-precision reason.
|
|
239
|
-
const confVals =
|
|
265
|
+
const confVals = gateResults
|
|
240
266
|
.map((w) => w.confidence)
|
|
241
267
|
.filter((n) => Number.isFinite(n))
|
|
242
268
|
.slice()
|
|
@@ -360,7 +386,12 @@ function aggregatePanel(workerResults, meta = {}) {
|
|
|
360
386
|
|
|
361
387
|
// Build display notes.
|
|
362
388
|
const lines = [];
|
|
363
|
-
|
|
389
|
+
// N counts the GATING workers; a non-gating one is named separately so the
|
|
390
|
+
// line cannot read as an off-by-one ("panel of 4: …five kinds…").
|
|
391
|
+
const advisoryKinds = nonGatingResults.map((w) => w.kind);
|
|
392
|
+
lines.push(`Subagent grader ${VERSION} (panel of ${N}: ${gateResults.map((w) => w.kind).join(', ')}`
|
|
393
|
+
+ (advisoryKinds.length ? ` · advisory, non-gating: ${advisoryKinds.join(', ')}` : '')
|
|
394
|
+
+ `) — ${verdict}`);
|
|
364
395
|
if (ff != null) lines.push(` functional_fidelity (Narc): ${ff}/10`);
|
|
365
396
|
if (cq != null) lines.push(` code_quality (Quality): ${cq}/10`);
|
|
366
397
|
if (economy != null) lines.push(` economy (Quality, adv.): ${economy}/10`);
|
|
@@ -421,6 +452,7 @@ function aggregatePanel(workerResults, meta = {}) {
|
|
|
421
452
|
code_quality_effective: cqEffective,
|
|
422
453
|
security_score: security,
|
|
423
454
|
efficiency_score: efficiencyScore,
|
|
455
|
+
copy_score: copyScore,
|
|
424
456
|
},
|
|
425
457
|
signals: {
|
|
426
458
|
grader_kind: 'subagent-panel',
|
|
@@ -470,6 +502,10 @@ function graderUnavailableResult({ reason, note, meta = {} } = {}) {
|
|
|
470
502
|
code_quality_effective: null,
|
|
471
503
|
security_score: null,
|
|
472
504
|
efficiency_score: null,
|
|
505
|
+
// task 1003977: nulled with the rest so an outage grade's rubric_scores
|
|
506
|
+
// has the SAME SHAPE as a normal one. A key that is absent here and
|
|
507
|
+
// present there is a difference a consumer has to know about.
|
|
508
|
+
copy_score: null,
|
|
473
509
|
},
|
|
474
510
|
signals: {
|
|
475
511
|
grader_kind: 'subagent-panel',
|
|
@@ -135,23 +135,33 @@ function hasCopyOrVisualWork(task) {
|
|
|
135
135
|
// STEP 2 remakes the copy/visuals with both routes explicitly offered — R04's
|
|
136
136
|
// brief, verbatim in intent.
|
|
137
137
|
//
|
|
138
|
-
// STEP 1
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
138
|
+
// STEP 1 ASKS FOR COACHING AGAIN, because there is now a lens listening.
|
|
139
|
+
//
|
|
140
|
+
// The history, because the wording is load-bearing: STEP 1 originally said
|
|
141
|
+
// "coach the grader", which asked for something no artist could do (task
|
|
142
|
+
// 1003937, ADR 0298) — the panel's four lenses read completion, code quality,
|
|
143
|
+
// security and runtime cost, and not one read a sentence, so four consecutive
|
|
144
|
+
// reviews found the same wall. The step was rewritten to ask for the RECORD
|
|
145
|
+
// instead, and that version carried an explicit instruction not to restore the
|
|
146
|
+
// coaching wording early, because the wording is the signal for which world we
|
|
147
|
+
// are in.
|
|
148
|
+
//
|
|
149
|
+
// Task 1003977 shipped the fifth lens: an ADVISORY 'reader', scoped to
|
|
150
|
+
// copy-bearing diffs by THIS FILE'S hasCopyOrVisualWork — the same predicate
|
|
151
|
+
// that fires this cascade, so an artist review and the copy lens cover exactly
|
|
152
|
+
// the same set by construction, and widening one widens both. The voice guide
|
|
153
|
+
// it waited on is docs/voice.md (task 1003976). So there is a counterpart to
|
|
154
|
+
// coach, it has a written standard to be coached against, and the record the
|
|
155
|
+
// interim wording collected is what both were built from.
|
|
156
|
+
//
|
|
157
|
+
// What has NOT changed: the reader is non-gating and cannot hold a ship (ADR
|
|
158
|
+
// 0241 keeps the artist's judgement on the deploy), so this review is still a
|
|
159
|
+
// request triage may dismiss.
|
|
150
160
|
function artistReviewBody(task) {
|
|
151
161
|
return [
|
|
152
162
|
`This is an ARTIST REVIEW, generated automatically because task ${task.id} put copy or visuals in front of a person. It is a REQUEST, not a gate — it blocks nothing that shipped, and triage may dismiss it.`,
|
|
153
163
|
'',
|
|
154
|
-
`STEP 1 —
|
|
164
|
+
`STEP 1 — COACH THE READER. Pull how the grader judged task ${task.id} (GET /tasks/${task.id}?include=grade) and find the 'reader' row in signals.workers, with its copy_score in rubric_scores. That lens read the changed copy against docs/voice.md and cited rule ids. Say where it was RIGHT, where it was WRONG, and — the valuable one — what it did not see at all. A finding it filed that you disagree with is worth as much as one it missed: both are corrections to a lens that is listening, which is what this step could not ask for before task 1003977 shipped it (ADR 0298). If the miss is because no rule in docs/voice.md reaches it, that is a gap in the GUIDE — file it against the guide rather than expecting the lens to invent a rule at grade time. Two cases where there will be no reader row and that is not a fault: a ship that took the trivial-diff path (a single generalist shot, no panel), and one where the lens errored — it is advisory, so an outage is recorded and nothing is blocked.`,
|
|
155
165
|
'',
|
|
156
166
|
'STEP 2 — REMAKE OR TWEAK the copy/visuals if they need it. Your choice of route, both offered on purpose: OUTSIDE the AI chat (edit the surface directly and land it through the copy desk or the normal pipeline), or WITH it (draft in chat, then land). Either way it lands as a claimed, graded, shipped task like everything else — there is no live CMS (goal 1000074\'s hard rule).',
|
|
157
167
|
'',
|
|
@@ -213,15 +213,24 @@ await test('a copy/visual ship generates the two-step artist review', () => {
|
|
|
213
213
|
assert.match(spec.title, /7001/);
|
|
214
214
|
// Both steps, and both routes of step 2, are in the body.
|
|
215
215
|
//
|
|
216
|
-
// STEP 1's label tracks ADR 0298 (task 1003937)
|
|
217
|
-
// "REVIEW THE GRADER … coach it", which asked for something no
|
|
218
|
-
// do: the panel's four lenses read completion, code quality,
|
|
219
|
-
// runtime cost, so there was no copy verdict to coach.
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
216
|
+
// STEP 1's label tracks ADR 0298 (task 1003937), and it has now turned twice.
|
|
217
|
+
// It first read "REVIEW THE GRADER … coach it", which asked for something no
|
|
218
|
+
// artist could do: the panel's four lenses read completion, code quality,
|
|
219
|
+
// security and runtime cost, so there was no copy verdict to coach. It was
|
|
220
|
+
// rewritten to ask for the RECORD instead, as an explicitly INTERIM wording,
|
|
221
|
+
// pinned here so a well-meaning copy-edit could not restore the unfollowable
|
|
222
|
+
// version without reading why.
|
|
223
|
+
//
|
|
224
|
+
// Task 1003977 shipped the advisory reader lens and the voice guide it waited
|
|
225
|
+
// on (task 1003976), so the step is coaching again — and this pin moves with
|
|
226
|
+
// it. The two must turn TOGETHER: the wording is the signal for which world
|
|
227
|
+
// we are in, so a body that still said "no lens reads a sentence" after the
|
|
228
|
+
// lens landed would send every artist looking for a counterpart that is
|
|
229
|
+
// sitting right there in the grade record.
|
|
230
|
+
assert.match(spec.description, /STEP 1 — COACH THE READER/);
|
|
231
|
+
assert.match(spec.description, /docs\/voice\.md/, 'and names the written standard to coach against');
|
|
232
|
+
assert.doesNotMatch(spec.description, /none of them reads a sentence/,
|
|
233
|
+
'the interim claim is gone — it stopped being true when the reader shipped');
|
|
225
234
|
assert.match(spec.description, /ADR 0298/, 'the step names the decision that shaped it');
|
|
226
235
|
assert.match(spec.description, /STEP 2 — REMAKE OR TWEAK/);
|
|
227
236
|
assert.match(spec.description, /OUTSIDE the AI chat/);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.891",
|
|
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.891",
|
|
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.891",
|
|
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",
|
|
@@ -67,6 +67,10 @@ const FILES = [
|
|
|
67
67
|
'scripts/gds/start.js',
|
|
68
68
|
'scripts/gds/context-pack.js', // start.js renders through this
|
|
69
69
|
'scripts/gds/status.js',
|
|
70
|
+
// status.js resolves its default version at runtime instead of naming one
|
|
71
|
+
// (task 1004071) -- so the packaged CLI needs this too, or `bongos status`
|
|
72
|
+
// dies on a missing module. tests/cli_package.mjs caught exactly that.
|
|
73
|
+
'scripts/gds/version-select.js',
|
|
70
74
|
'scripts/gds/task.js',
|
|
71
75
|
'scripts/gds/recall.js',
|
|
72
76
|
'scripts/gds/cost.js',
|
|
@@ -12,6 +12,14 @@ const crypto = require('node:crypto');
|
|
|
12
12
|
const { spawn } = require('node:child_process');
|
|
13
13
|
const { cliClient } = require('./cli-lib');
|
|
14
14
|
const grader = require('../../modules/grading/grader');
|
|
15
|
+
// task 1003977: the ONE definition of "does this ship put copy in front of a
|
|
16
|
+
// person". It lives in the lifecycle module and the grading module may not
|
|
17
|
+
// require a sibling (ADR 0083), so this script — which is not a module and
|
|
18
|
+
// already requires grading directly — is the seam where the two meet. Writing a
|
|
19
|
+
// second predicate inside grading would let the at-ship reader lens and the
|
|
20
|
+
// after-the-fact artist review disagree about what they cover, and widening the
|
|
21
|
+
// set would then have to be done twice.
|
|
22
|
+
const { hasCopyOrVisualWork } = require('../../modules/lifecycle/cascade.js');
|
|
15
23
|
const { resolveBuilderModel } = require('./ship-memory-sync.js');
|
|
16
24
|
const { argvOut, gitArgvOk, gitOk, shellOk } = require('./ship-git.js');
|
|
17
25
|
const { GRADER_DIFF_CHAR_CAP, GRADER_TIMEOUT_MS, REPO_ROOT, noteServerReward } = require('./ship-state.js');
|
|
@@ -285,9 +293,16 @@ async function runSubagentGrader({ task, valueSummary, baseline, changedFiles: p
|
|
|
285
293
|
lenses_dropped: ['hacker', 'efficiency'],
|
|
286
294
|
};
|
|
287
295
|
} else {
|
|
288
|
-
|
|
296
|
+
// task 1003977: the advisory Reader rides along on copy-bearing diffs only.
|
|
297
|
+
// Where the predicate says no, `panel` is byte-identical to DEFAULT_PANEL
|
|
298
|
+
// and the ship costs exactly what it costs today.
|
|
299
|
+
const copyBearing = hasCopyOrVisualWork(task);
|
|
300
|
+
const panel = grader.WORKERS.panelFor({ copyBearing });
|
|
289
301
|
console.log(` subagent grader ${grader.VERSION}: panel of ${panel.length} workers (${panel.join(', ')}), model=${grader.selectGraderModel({ builderModel })} (builder ${builderModel || `${grader.ASSUMED_BUILDER_MODEL} assumed`}), changed_files=${changedFiles.length}, diff_chars=${diff.length}, per-worker timeout=${GRADER_TIMEOUT_MS}ms`);
|
|
290
|
-
console.log(` gate: every worker must return verdict='pass'; 'concerns' → manual-confirm fall-through; 'fail' blocks`);
|
|
302
|
+
console.log(` gate: every GATING worker must return verdict='pass'; 'concerns' → manual-confirm fall-through; 'fail' blocks`);
|
|
303
|
+
console.log(copyBearing
|
|
304
|
+
? ` copy lens: ON — this task is copy-bearing, so the advisory 'reader' reads the changed strings against docs/voice.md. It is NON-GATING: nothing it reports, and no error or timeout it hits, can stop this ship.`
|
|
305
|
+
: ` copy lens: off — not a copy-bearing task (no copy-proposal source, not artist-discipline, module not in COPY_VISUAL_MODULES), so the panel is the usual four.`);
|
|
291
306
|
if (priorRound) {
|
|
292
307
|
console.log(` delta re-grade (task 1002662): prior round's findings + builder response ride along, fenced; answered findings can only re-veto with NEW evidence`);
|
|
293
308
|
}
|
|
@@ -300,8 +315,19 @@ async function runSubagentGrader({ task, valueSummary, baseline, changedFiles: p
|
|
|
300
315
|
builderRank, // SEC #863: drives the permission-path pre-pass
|
|
301
316
|
builderModel, // task 1002659: real session model → cross-model grader pick
|
|
302
317
|
priorRound, // task 1002662: delta-aware re-grade context (panel path only)
|
|
318
|
+
workers: panel, // task 1003977: DEFAULT_PANEL, plus the advisory reader when copy-bearing
|
|
303
319
|
runOpts,
|
|
304
320
|
});
|
|
321
|
+
// A SKIPPED LENS IS RECORDED, NEVER SILENT. Without this a grade with no
|
|
322
|
+
// copy_score is indistinguishable from one where the copy was read and
|
|
323
|
+
// found clean — a missing score would read as a passing one. Same pattern
|
|
324
|
+
// and same reason as the trivial path's `lenses_dropped` above.
|
|
325
|
+
if (!copyBearing) {
|
|
326
|
+
result.signals = {
|
|
327
|
+
...(result.signals || {}),
|
|
328
|
+
reader_skipped: 'not_copy_bearing',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
305
331
|
}
|
|
306
332
|
const wallMs = Date.now() - t0;
|
|
307
333
|
// Decorate signals with the local shortstat — useful in audit-trail.
|
package/scripts/gds/status.js
CHANGED
|
@@ -19,14 +19,24 @@
|
|
|
19
19
|
//
|
|
20
20
|
// Exit codes: 0 ok · 1 network/api error · 2 bad invocation · 3 not found.
|
|
21
21
|
|
|
22
|
-
const { API_BASE, cliClient, assertInstanceMatch } = require('./cli-lib');
|
|
22
|
+
const { API_BASE, cliClient, assertInstanceMatch, cliExit } = require('./cli-lib');
|
|
23
23
|
const { resolveEnv } = require('../../src/instance-config');
|
|
24
|
+
const { resolveDefaultVersion } = require('./version-select.js');
|
|
24
25
|
|
|
25
|
-
// The active internal version
|
|
26
|
-
// --version
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
const DEFAULT_VERSION = resolveEnv('STATUS_DEFAULT_VERSION') || 'BONGOS-V1';
|
|
26
|
+
// The active internal version is RESOLVED, never named here. Override with
|
|
27
|
+
// <PREFIX>_STATUS_DEFAULT_VERSION or --version.
|
|
28
|
+
//
|
|
29
|
+
// WHY THERE IS NO LITERAL (task 1004071). This line used to read
|
|
30
|
+
// const DEFAULT_VERSION = resolveEnv('STATUS_DEFAULT_VERSION') || 'BONGOS-V1';
|
|
31
|
+
// under a comment promising "the bare /status card should default to the live
|
|
32
|
+
// version, not a sealed one" — the same fix task 1709 had already applied once,
|
|
33
|
+
// for GDS-V4 → BONGOS-V1. BONGOS-V1 then sealed, nobody bumped the literal, and
|
|
34
|
+
// /status served a shipped version's closing scoreboard (140/203 satisfied) as
|
|
35
|
+
// the project's current status. It reads as a healthy project, which is why it
|
|
36
|
+
// sat: nothing in the output says `shipped`, and /status is what a session runs
|
|
37
|
+
// precisely when it does NOT already know. Bumping the literal again would only
|
|
38
|
+
// re-arm the same bomb for whoever seals BONGOS-V2, so the rule is derived from
|
|
39
|
+
// `versions.status` instead — the selection capture.js has always used.
|
|
30
40
|
|
|
31
41
|
// Compact, status-first ordering for the "remaining: …" summaries.
|
|
32
42
|
const STATUS_ORDER = ['shipped', 'confirmed', 'completed', 'claimed', 'ready', 'blocked', 'backlog'];
|
|
@@ -53,7 +63,36 @@ function parseArgs(argv) {
|
|
|
53
63
|
else if (!criterion) criterion = a.toUpperCase();
|
|
54
64
|
else return { error: `unexpected argument: ${a}` };
|
|
55
65
|
}
|
|
56
|
-
|
|
66
|
+
// No `versionExplicit` companion flag: `version !== null` already IS that
|
|
67
|
+
// fact, and resolveVersion returns `defaulted`/`source` for anyone who needs
|
|
68
|
+
// to act on it. A second field carrying the same truth is one that can drift.
|
|
69
|
+
return { version: version || null, criterion, json, widget, hook };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Resolve which version this invocation is about. Precedence: an explicit
|
|
73
|
+
// --version (or bare token) > the env override > the live BUILDING version.
|
|
74
|
+
// Returns { version, defaulted, source }; version is NULL when nothing is
|
|
75
|
+
// building, which the caller must treat as "stop and say so", never as a guess.
|
|
76
|
+
// Deps are injectable so the precedence is unit-tested without a session.
|
|
77
|
+
async function resolveVersion(opts, deps = {}) {
|
|
78
|
+
if (opts.version) return { version: opts.version, defaulted: false, source: 'flag' };
|
|
79
|
+
const env = (deps.resolveEnv || resolveEnv)('STATUS_DEFAULT_VERSION');
|
|
80
|
+
if (env) return { version: env, defaulted: true, source: 'env' };
|
|
81
|
+
const resolve = deps.resolveDefaultVersion || resolveDefaultVersion;
|
|
82
|
+
const building = await resolve();
|
|
83
|
+
if (building) return { version: building, defaulted: true, source: 'building' };
|
|
84
|
+
return { version: null, defaulted: true, source: 'none' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// One line naming a version the USER did not, so a cross-version answer cannot
|
|
88
|
+
// pass unnoticed. `Cn` is version-scoped and the versions number independently
|
|
89
|
+
// — BONGOS-V1 C8 is `money-one-unit`, BONGOS-V2 C8 is `wa6-kernel-and-packs` —
|
|
90
|
+
// so a silently-defaulted version answers a different question than the one
|
|
91
|
+
// asked. Empty when the user named the version: they already know.
|
|
92
|
+
function defaultedVersionNote(r) {
|
|
93
|
+
if (!r || !r.defaulted || !r.version) return '';
|
|
94
|
+
const why = r.source === 'env' ? 'from STATUS_DEFAULT_VERSION' : 'the version currently building';
|
|
95
|
+
return `(no version given — showing ${r.version}, ${why}. Pass --version to pick another.)`;
|
|
57
96
|
}
|
|
58
97
|
|
|
59
98
|
async function fetchProgress(version) {
|
|
@@ -325,7 +364,38 @@ async function main() {
|
|
|
325
364
|
// apiCall/cliClient guard, so without this `bongos status` would silently print another
|
|
326
365
|
// instance's progress. Skip only in --hook mode (the automated card path stays fail-open).
|
|
327
366
|
if (!opts.hook) await assertInstanceMatch();
|
|
328
|
-
|
|
367
|
+
|
|
368
|
+
// Resolve the version before anything fetches. --hook stays FAIL-OPEN like the
|
|
369
|
+
// instance check above: the automated card path must degrade, not throw, so a
|
|
370
|
+
// lookup that cannot answer emits an empty envelope rather than killing the hook.
|
|
371
|
+
//
|
|
372
|
+
// COST: one extra sequential GET /versions before the progress fetch, and it
|
|
373
|
+
// cannot be parallelised — the id IS the second call's path. It is paid only
|
|
374
|
+
// when no version was named: --version and STATUS_DEFAULT_VERSION both
|
|
375
|
+
// short-circuit it, so an instance that minds the round trip on the
|
|
376
|
+
// session-start card path sets the env var once and pays nothing. That is the
|
|
377
|
+
// deliberate trade against the alternative, which is the literal that spent
|
|
378
|
+
// months reporting a sealed version as current.
|
|
379
|
+
const picked = await resolveVersion(opts).catch(() => ({ version: null, defaulted: true, source: 'none' }));
|
|
380
|
+
if (!picked.version) {
|
|
381
|
+
if (opts.hook) {
|
|
382
|
+
process.stdout.write(JSON.stringify({
|
|
383
|
+
widgets_enabled: false, html: '', markdown: '',
|
|
384
|
+
warnings: ['status: no version is currently `building` — nothing to report.'],
|
|
385
|
+
}) + '\n');
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
console.error('status: no version is currently `building` — cannot resolve a default.');
|
|
389
|
+
console.error(' Open one, or pass --version explicitly (e.g. --version BONGOS-V2).');
|
|
390
|
+
// cliExit, NOT process.exit: resolveVersion above just made a network round
|
|
391
|
+
// trip, and a hard exit while the request machinery is tearing down aborts
|
|
392
|
+
// natively on Windows + Node 24 (UV_HANDLE_CLOSING) — exit 127 AFTER the
|
|
393
|
+
// message prints, so a caller reads "tooling broken" instead of "no building
|
|
394
|
+
// version". The other exits in this file are pre-request or have substantial
|
|
395
|
+
// rendering between; this branch has neither. See scripts/gds/CLAUDE.md.
|
|
396
|
+
return cliExit(3);
|
|
397
|
+
}
|
|
398
|
+
const data = await fetchProgress(picked.version);
|
|
329
399
|
|
|
330
400
|
// Resolve the requested criterion (if any) once — every output mode needs it.
|
|
331
401
|
let c = null;
|
|
@@ -333,7 +403,10 @@ async function main() {
|
|
|
333
403
|
const n = Number(opts.criterion.slice(1));
|
|
334
404
|
c = data.criteria.find((x) => x.cnum === n);
|
|
335
405
|
if (!c) {
|
|
336
|
-
|
|
406
|
+
// Name the version, not just the criterion: on a DEFAULTED version this is
|
|
407
|
+
// the reader's only clue that C13 was looked up somewhere they didn't pick.
|
|
408
|
+
console.error(`status: ${picked.version} has no criterion ${opts.criterion} (it has C1–C${data.criteria.length}).`);
|
|
409
|
+
if (picked.defaulted) console.error(` ${defaultedVersionNote(picked)}`);
|
|
337
410
|
process.exit(3);
|
|
338
411
|
}
|
|
339
412
|
}
|
|
@@ -356,9 +429,12 @@ async function main() {
|
|
|
356
429
|
// else the whole version rollup).
|
|
357
430
|
if (opts.json) { console.log(JSON.stringify(c || data, null, 2)); return; }
|
|
358
431
|
|
|
359
|
-
// Default human output — the existing plain-text rollup,
|
|
360
|
-
|
|
361
|
-
|
|
432
|
+
// Default human output — the existing plain-text rollup, plus one line when
|
|
433
|
+
// the version was resolved rather than chosen (--json/--widget/--hook are
|
|
434
|
+
// machine interfaces and stay byte-identical).
|
|
435
|
+
if (c) printOne(data, c); else printAll(data);
|
|
436
|
+
const note = defaultedVersionNote(picked);
|
|
437
|
+
if (note) console.log(` ${note}\n`);
|
|
362
438
|
}
|
|
363
439
|
|
|
364
440
|
if (require.main === module) {
|
|
@@ -368,4 +444,4 @@ if (require.main === module) {
|
|
|
368
444
|
});
|
|
369
445
|
}
|
|
370
446
|
|
|
371
|
-
module.exports = { parseArgs, renderStatusWidget, renderStatusMarkdown };
|
|
447
|
+
module.exports = { parseArgs, resolveVersion, defaultedVersionNote, renderStatusWidget, renderStatusMarkdown };
|
|
@@ -35,13 +35,31 @@ const VERSION_LITERAL = String.raw`['"][A-Z][A-Z0-9]*-V\d+['"]`;
|
|
|
35
35
|
const VERSION_FIELD = String.raw`version_id|versionId|suggested_version|target_version|DEFAULT_VERSION|VERSION_ID`;
|
|
36
36
|
const ASSIGNED = new RegExp(`(?:${VERSION_FIELD})\\s*['"]?\\s*[:=][^,;)]*${VERSION_LITERAL}`);
|
|
37
37
|
const DEFAULTED = new RegExp(`--version[^\\n]*?\\|\\|\\s*${VERSION_LITERAL}`);
|
|
38
|
+
// A version field FALLING BACK to a literal through `||`. ASSIGNED cannot see
|
|
39
|
+
// this shape: its `[^,;)]*` stops at the first `)`, so any call between the `=`
|
|
40
|
+
// and the literal hides it — `DEFAULT_VERSION = resolveEnv('X') || 'BONGOS-V1'`
|
|
41
|
+
// scanned CLEAN while `DEFAULT_VERSION = 'BONGOS-V1'` was caught (task 1004071).
|
|
42
|
+
// That is the exact line that served a sealed version as /status's default for
|
|
43
|
+
// months. Kept as its own narrow pattern rather than loosening ASSIGNED to
|
|
44
|
+
// `[^;]*`, which would fire on any line merely mentioning a field and a literal
|
|
45
|
+
// — the over-broad source assertion this repo keeps relearning (learning 1000196).
|
|
46
|
+
const FALLBACK = new RegExp(`(?:${VERSION_FIELD})\\s*['"]?\\s*[:=][^;]*?\\|\\|\\s*${VERSION_LITERAL}`);
|
|
38
47
|
|
|
39
48
|
// path::reason. A justified entry, one line, like every other guard here.
|
|
49
|
+
//
|
|
50
|
+
// A REASON MAY CITE THE CODE'S SHAPE, NEVER THE DATABASE'S CONTENTS (task
|
|
51
|
+
// 1004071). status.js was exempted because "TWO versions are currently building
|
|
52
|
+
// — so 'most recent building' is not a derivable answer here." True the day it
|
|
53
|
+
// was written; false once CB-V1 sealed. Nothing re-checks prose, so the entry
|
|
54
|
+
// outlived its justification in silence and /status went on serving a SHIPPED
|
|
55
|
+
// version's closing scoreboard as the project's current status. An entry whose
|
|
56
|
+
// truth depends on rows is a timer nobody set. `version_literals.mjs` now fails
|
|
57
|
+
// CI on an entry that is outright DEAD; it cannot police one that is merely
|
|
58
|
+
// stale, which is why that judgement has to be made here, when the entry is
|
|
59
|
+
// written.
|
|
40
60
|
const VERSION_LITERAL_ALLOW = new Map([
|
|
41
61
|
['scripts/gds/grade-smoke.js',
|
|
42
62
|
'GOOD_TASK / BAD_TASK are in-memory fixtures handed to grader.gradeSubagent; they are never POSTed, so no version_closed can refuse them and the literal cannot rot into a failure.'],
|
|
43
|
-
['scripts/gds/status.js',
|
|
44
|
-
'DEFAULT_VERSION is an owner-facing dashboard default with an env override, and TWO versions are currently building — so "most recent building" is not a derivable answer here. Retargeting what /status shows by default is an owner decision, not a refactor.'],
|
|
45
63
|
]);
|
|
46
64
|
|
|
47
65
|
function isExempt(rel) {
|
|
@@ -67,7 +85,7 @@ function scanScripts(files, read) {
|
|
|
67
85
|
scanned += 1;
|
|
68
86
|
if (VERSION_LITERAL_ALLOW.has(rel)) continue;
|
|
69
87
|
stripComments(src, rel).forEach((line, i) => {
|
|
70
|
-
if (ASSIGNED.test(line) || DEFAULTED.test(line)) {
|
|
88
|
+
if (ASSIGNED.test(line) || DEFAULTED.test(line) || FALLBACK.test(line)) {
|
|
71
89
|
violations.push(
|
|
72
90
|
`${rel}:${i + 1} names a version id where one should be RESOLVED: ${line.trim().slice(0, 90)}`
|
|
73
91
|
+ ' — a literal works until that version ships, then version_closed refuses every write and the'
|
|
@@ -119,4 +137,4 @@ function main() {
|
|
|
119
137
|
|
|
120
138
|
if (require.main === module) main();
|
|
121
139
|
|
|
122
|
-
module.exports = { checkNoVersionLiterals, scanScripts, stripComments, isExempt, VERSION_LITERAL_ALLOW, ASSIGNED, DEFAULTED };
|
|
140
|
+
module.exports = { checkNoVersionLiterals, scanScripts, stripComments, isExempt, VERSION_LITERAL_ALLOW, ASSIGNED, DEFAULTED, FALLBACK };
|
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.891'; // 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');
|
package/tests/extract_json.mjs
CHANGED
|
@@ -128,11 +128,19 @@ test('nothing parseable is null', () => {
|
|
|
128
128
|
test('GRADE_SHAPE_KEYS is the exported score-key list', () => {
|
|
129
129
|
// Exported so workers and grader.js agree on what "grade-shaped" means; the
|
|
130
130
|
// preference test above depends on functional_fidelity being in this list.
|
|
131
|
+
//
|
|
132
|
+
// ONE ENTRY PER WORKER THAT REPORTS A SCORE. task 1003977 added copy_score
|
|
133
|
+
// with the reader: reader.js re-exports this extractJson, so a worker missing
|
|
134
|
+
// from this list silently loses the score-based disambiguation every other
|
|
135
|
+
// worker gets. Kept as an exact list rather than loosened, because that
|
|
136
|
+
// silence is the whole reason to pin it — a new worker must appear here in
|
|
137
|
+
// the same change that adds it.
|
|
131
138
|
assert.deepEqual(GRADE_SHAPE_KEYS, [
|
|
132
139
|
'functional_fidelity',
|
|
133
140
|
'code_quality',
|
|
134
141
|
'security_score',
|
|
135
142
|
'efficiency_score',
|
|
143
|
+
'copy_score',
|
|
136
144
|
]);
|
|
137
145
|
});
|
|
138
146
|
|