@bongos/core 1.19.783 → 1.19.785
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 +61 -26
- package/.claude/skills/planning-session/SKILL.md +4 -0
- package/docs/adr/0291-the-accessibility-dimension-scores-the-transcript-not-the-diff.md +35 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +17 -14
- package/docs/copy-registry.json +46 -19
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/packs/ideator.md +6 -0
- package/modules/hall-ui/public/sessions.html +3 -1
- package/modules/hall-ui/public/sessions.js +33 -1
- package/modules/lifecycle/github-push.js +70 -15
- package/modules/sessions/accessibility.js +373 -0
- package/modules/sessions/db.js +11 -0
- package/modules/sessions/routes/sessions.js +35 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/artifact-audit.js +309 -0
- package/scripts/gds/session-eval.js +54 -4
- package/src/module-api.js +1 -1
- package/tests/artifact_audit.mjs +210 -0
- package/tests/auto_merge_diagnosis.mjs +313 -0
- package/tests/hall_roster_world.mjs +6 -2
- package/tests/hall_sessions_ledger.mjs +94 -0
- package/tests/session_accessibility.mjs +243 -0
- package/tests/session_eval.mjs +52 -1
package/modules/sessions/db.js
CHANGED
|
@@ -490,6 +490,11 @@ const SESSION_ORDER = Object.freeze({
|
|
|
490
490
|
tokens: 'sr.total_tokens DESC, sr.uploaded_at DESC',
|
|
491
491
|
duration: 'sr.duration_seconds DESC NULLS LAST, sr.uploaded_at DESC',
|
|
492
492
|
drachmae: 'sr.drachmae_earned DESC, sr.uploaded_at DESC',
|
|
493
|
+
// task 1002516 — WORST FIRST, like tool_errors: the sessions worth coaching are
|
|
494
|
+
// the low-scoring ones. NULLS LAST keeps the un-scorable rows (a fully-waived
|
|
495
|
+
// expert profile, or a row uploaded before the dimension existed) off the top
|
|
496
|
+
// of a ranking they have no score to earn.
|
|
497
|
+
accessibility: "(sr.metrics->'accessibility'->>'score')::numeric ASC NULLS LAST, sr.uploaded_at DESC",
|
|
493
498
|
});
|
|
494
499
|
function resolveSessionOrder(key) {
|
|
495
500
|
return Object.prototype.hasOwnProperty.call(SESSION_ORDER, key) ? SESSION_ORDER[key] : SESSION_ORDER.recent;
|
|
@@ -544,6 +549,12 @@ const SESSION_SUMMARY_COLS = `
|
|
|
544
549
|
sr.task_ids, sr.drachmae_earned, sr.total_tokens, sr.duration_seconds,
|
|
545
550
|
COALESCE((sr.metrics->>'tool_error_count')::int, 0) AS tool_errors,
|
|
546
551
|
COALESCE((sr.metrics->>'assistant_turns')::int, 0) AS assistant_turns,
|
|
552
|
+
-- task 1002516: the transcript accessibility dimension. NOT coalesced to 0 —
|
|
553
|
+
-- a missing score means "not scored" (waived, or pre-dating the dimension) and
|
|
554
|
+
-- a zero would read as the worst possible session. The band travels with it so
|
|
555
|
+
-- the ledger does not have to re-derive the thresholds in the browser.
|
|
556
|
+
(sr.metrics->'accessibility'->>'score')::numeric AS accessibility_score,
|
|
557
|
+
(sr.metrics->'accessibility'->>'band') AS accessibility_band,
|
|
547
558
|
sr.uploaded_at`;
|
|
548
559
|
|
|
549
560
|
// searchSessions — cross-builder search over the corpus, filtered by session
|
|
@@ -31,7 +31,12 @@ const express = require('express');
|
|
|
31
31
|
const api = require('../../../src/module-api');
|
|
32
32
|
const auth = api; // requireBuilder / requireRank live on the doorway surface
|
|
33
33
|
const db = require('../db');
|
|
34
|
-
|
|
34
|
+
// task 1002516 (clause 4 of `accessibility-fluency-profile`): the transcript
|
|
35
|
+
// accessibility dimension. The scorer is this module's own PURE file — it takes
|
|
36
|
+
// the interaction profile as data precisely so it does not have to require
|
|
37
|
+
// builder-settings, which ADR 0083 forbids.
|
|
38
|
+
const { scoreAccessibility } = require('../accessibility');
|
|
39
|
+
const seams = api; // resolveOptional('reward' | 'builder-settings') — kernel-mediated ports
|
|
35
40
|
const { validateOrRespond, parseId } = api;
|
|
36
41
|
|
|
37
42
|
const UPLOAD_BODY_LIMIT = '4mb';
|
|
@@ -237,6 +242,35 @@ module.exports = function buildSessionsRouter() {
|
|
|
237
242
|
const metrics = body.metrics && typeof body.metrics === 'object' ? { ...body.metrics } : {};
|
|
238
243
|
delete metrics.total_tokens;
|
|
239
244
|
delete metrics.model_usage; // promoted to its own column; avoid two copies
|
|
245
|
+
// task 1002516 — the accessibility dimension is computed HERE, server-side,
|
|
246
|
+
// for the same reason drachmae is: a score the uploader could supply is a
|
|
247
|
+
// score nobody can read. It is also the only place the builder's own
|
|
248
|
+
// interaction profile is available without another round trip — the
|
|
249
|
+
// SessionEnd hook runs under a 6-second watchdog, so asking it to fetch
|
|
250
|
+
// /me would trade a coaching metric for dropped uploads.
|
|
251
|
+
delete metrics.accessibility; // never trusted from the client
|
|
252
|
+
const settings = seams.resolveOptional('builder-settings');
|
|
253
|
+
let interactionProfile = null;
|
|
254
|
+
let profileUnavailable = false;
|
|
255
|
+
if (settings && settings.getBuilderBehaviorPrefs && settings.describeInteraction) {
|
|
256
|
+
// Best-effort: a coaching score must never be the reason an upload 500s.
|
|
257
|
+
// The failure is RECORDED ON THE ROW rather than logged — a durable flag
|
|
258
|
+
// a reader of the score can see beats a console line nobody correlates
|
|
259
|
+
// with the session it degraded.
|
|
260
|
+
try {
|
|
261
|
+
const prefs = await settings.getBuilderBehaviorPrefs(req.builder.id);
|
|
262
|
+
interactionProfile = settings.describeInteraction(prefs).effective;
|
|
263
|
+
} catch (_) {
|
|
264
|
+
profileUnavailable = true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// With the module off (or the read failed) the scorer falls back to the
|
|
268
|
+
// accessible-leaning DEFAULT profile, which is the right posture: an
|
|
269
|
+
// instance that never configured a profile has every builder on it.
|
|
270
|
+
metrics.accessibility = {
|
|
271
|
+
...scoreAccessibility(detailArr, { profile: interactionProfile }),
|
|
272
|
+
...(profileUnavailable ? { profile_unavailable: true } : {}),
|
|
273
|
+
};
|
|
240
274
|
const modelUsage = body.model_usage && typeof body.model_usage === 'object'
|
|
241
275
|
? body.model_usage : {};
|
|
242
276
|
// upsert + cost-plus token reward in ONE transaction (task 1005). The
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.785",
|
|
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.785",
|
|
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.785",
|
|
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,309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
//
|
|
4
|
+
// scripts/gds/artifact-audit.js — does the work a task CLAIMS actually exist? (task 1003200)
|
|
5
|
+
//
|
|
6
|
+
// THE GAP. `confirmed` means verified and credited, awaiting land. A task can therefore be
|
|
7
|
+
// paid for and reported as done while its artifact exists nowhere reachable. On 2026-08-23
|
|
8
|
+
// three such tasks were found BY ACCIDENT, one at a time: 1003158 (body said "BUILT, tested,
|
|
9
|
+
// 33 tests green"; branch absent from origin), 1002459 (`confirmed` — verified and credited;
|
|
10
|
+
// no branch anywhere), 1002792 (`completed`; the validator it claimed to change was
|
|
11
|
+
// untouched). Nothing compared "the task claims an artifact" against "an artifact exists".
|
|
12
|
+
//
|
|
13
|
+
// WHY THE RECONCILER IS NOT THIS. publish-reconciler.js already proves a land — but only for
|
|
14
|
+
// tasks it is sweeping at `confirmed`, and it writes its misses to a log line. It says nothing
|
|
15
|
+
// about `shipped`, which is where a false "done" is most dangerous: the credit is booked, the
|
|
16
|
+
// ledger reads finished, and no sweep will ever look again.
|
|
17
|
+
//
|
|
18
|
+
// SHAPE COPIED FROM deploy-divergence.js / strand-watch.js, deliberately: cheap, bounded,
|
|
19
|
+
// fail-open, and SILENT when the board is clean. A check that cries wolf gets ignored, and an
|
|
20
|
+
// ignored check is worse than none — which is why the narrowing below is documented step by
|
|
21
|
+
// step rather than tuned until the number looked nice.
|
|
22
|
+
//
|
|
23
|
+
// THE NARROWING (measured 2026-09-16, and every step exists because the previous one lied):
|
|
24
|
+
//
|
|
25
|
+
// 2617 tasks at shipped/confirmed/completed
|
|
26
|
+
// 650 shipped on or after the ship-assurance gate (2026-08-19, task 1002483 / ADR 0184).
|
|
27
|
+
// Before that gate a DB-only ship recorded no declaration at all, so "no artifact"
|
|
28
|
+
// is unprovable rather than suspicious — 1469 older ships would be pure noise.
|
|
29
|
+
// 33 with no COMMIT MESSAGE on main naming the task
|
|
30
|
+
// 25 …and no FILE CONTENT on main naming it either. This second source is not optional:
|
|
31
|
+
// task 1003212's fix is on main and cited in a code comment, but the commit that
|
|
32
|
+
// carried it never named the task, so a message-only index calls it stranded. That
|
|
33
|
+
// is the "landed under another branch" case, and it is why this check reads both.
|
|
34
|
+
// 3 …and which actually DECLARE a code artifact (non-empty touches[]). The other 22
|
|
35
|
+
// declare none — goal edits, claim releases, triage, deploys — and are codeless by
|
|
36
|
+
// shape, reported quietly instead of as alarms.
|
|
37
|
+
//
|
|
38
|
+
// WHAT THIS CHECK CANNOT DO, stated so nobody trusts it further than it earns. The assurance
|
|
39
|
+
// ledger (`no_artifact` + reason, ship-assurance.js) is written into the audit log, and NO API
|
|
40
|
+
// route exposes it. So "declares no artifact" here is inferred from an EMPTY touches[], not
|
|
41
|
+
// read from the declaration the ship actually made. Exposing the assurance would make the
|
|
42
|
+
// codeless bucket exact; until then it is a shape, and a deploy/ops task whose touches[]
|
|
43
|
+
// describes what a RELEASE carried (rather than what it committed) can land in `stranded`.
|
|
44
|
+
//
|
|
45
|
+
// Usage:
|
|
46
|
+
// node scripts/gds/artifact-audit.js # human-readable report
|
|
47
|
+
// node scripts/gds/artifact-audit.js --line # ONE line, or nothing; always exit 0
|
|
48
|
+
// node scripts/gds/artifact-audit.js --json # machine shape
|
|
49
|
+
// node scripts/gds/artifact-audit.js --all # include the pre-gate ships too
|
|
50
|
+
// node scripts/gds/artifact-audit.js --since YYYY-MM-DD # override the gate date
|
|
51
|
+
// node scripts/gds/artifact-audit.js --file-blocker # ALSO file findings as a blocker
|
|
52
|
+
//
|
|
53
|
+
// Exit codes: 0 always on --line (fail-open). Otherwise 0 clean, 1 strands found, 2 undetermined.
|
|
54
|
+
|
|
55
|
+
const { execFileSync } = require('node:child_process');
|
|
56
|
+
const { apiCall, arg, hasFlag } = require('./cli-lib');
|
|
57
|
+
const { TASK_LIST_MAX_LIMIT, buildTaskPageQuery } = require('../../modules/lifecycle/task-list-limits.js');
|
|
58
|
+
|
|
59
|
+
// The statuses where a claimed artifact is supposed to already exist. `shipped` is the one the
|
|
60
|
+
// reconciler never revisits, and the reason this file exists.
|
|
61
|
+
const AUDITED_STATUSES = Object.freeze(['shipped', 'confirmed', 'completed']);
|
|
62
|
+
|
|
63
|
+
// task 1002483 / ADR 0184 landed the ship-assurance gate on this date. It is the earliest point
|
|
64
|
+
// at which a no-artifact ship had to say so, and therefore the earliest point at which silence
|
|
65
|
+
// means anything at all.
|
|
66
|
+
const ASSURANCE_GATE_DATE = '2026-08-19';
|
|
67
|
+
|
|
68
|
+
// How a task id is written when something references it: `task 1003200`, `task-1003200`,
|
|
69
|
+
// `task #1003200`. Ids are 6–7 digits; anything shorter is a PR or an issue, not ours.
|
|
70
|
+
const TASK_REF = /task[ _-]#?(\d{6,7})/gi;
|
|
71
|
+
|
|
72
|
+
const GIT_MAX_BUFFER = 256 * 1024 * 1024;
|
|
73
|
+
|
|
74
|
+
// ---- pure core --------------------------------------------------------------------------
|
|
75
|
+
// Everything below this comment is IO. Everything above the next one is a pure function of
|
|
76
|
+
// (tasks, two evidence sets), which is what tests/artifact_audit.mjs drives with the three
|
|
77
|
+
// real 2026-08-23 cases as fixtures.
|
|
78
|
+
|
|
79
|
+
function taskIdsIn(text) {
|
|
80
|
+
const out = new Set();
|
|
81
|
+
for (const m of String(text || '').matchAll(TASK_REF)) out.add(m[1]);
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Does this task DECLARE a code artifact? touches[] is the task's own statement about which
|
|
86
|
+
// files its work lands in, so a non-empty one is a promise that something reached the tree.
|
|
87
|
+
function declaresArtifact(task) {
|
|
88
|
+
return Array.isArray(task.touches) && task.touches.some((p) => String(p || '').trim().length > 0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The audit. `messageIds` / `contentIds` are sets of task ids evidenced on main by a commit
|
|
92
|
+
// message and by file content respectively; keeping them separate is what lets the report say
|
|
93
|
+
// HOW a task was proven, which is the difference between a finding and an accusation.
|
|
94
|
+
function auditArtifacts(tasks, { messageIds, contentIds, gateDate = ASSURANCE_GATE_DATE } = {}) {
|
|
95
|
+
const msg = messageIds instanceof Set ? messageIds : new Set(messageIds || []);
|
|
96
|
+
const content = contentIds instanceof Set ? contentIds : new Set(contentIds || []);
|
|
97
|
+
const out = { evidenced: [], preGate: [], codeless: [], stranded: [] };
|
|
98
|
+
|
|
99
|
+
for (const t of Array.isArray(tasks) ? tasks : []) {
|
|
100
|
+
if (!t || !AUDITED_STATUSES.includes(String(t.status))) continue;
|
|
101
|
+
const id = String(t.id);
|
|
102
|
+
const row = {
|
|
103
|
+
id,
|
|
104
|
+
status: String(t.status),
|
|
105
|
+
shipped_at: t.shipped_at ? String(t.shipped_at).slice(0, 10) : null,
|
|
106
|
+
title: String(t.title || ''),
|
|
107
|
+
touches: Array.isArray(t.touches) ? t.touches : [],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Evidence first, and cheapest first. A task named by a commit message landed under its
|
|
111
|
+
// own branch; one named only in file content landed under someone else's.
|
|
112
|
+
if (msg.has(id)) { out.evidenced.push({ ...row, how: 'commit-message' }); continue; }
|
|
113
|
+
if (content.has(id)) { out.evidenced.push({ ...row, how: 'file-content' }); continue; }
|
|
114
|
+
|
|
115
|
+
// Unevidenced. Before the assurance gate, silence is not a signal — see the header.
|
|
116
|
+
if (!row.shipped_at || row.shipped_at < gateDate) { out.preGate.push(row); continue; }
|
|
117
|
+
|
|
118
|
+
(declaresArtifact(t) ? out.stranded : out.codeless).push(row);
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---- IO ---------------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
function gitLines(args) {
|
|
126
|
+
// `git grep` exits 1 when it matches nothing, which is a legitimate empty result, not a
|
|
127
|
+
// failure. Anything else is genuinely broken and must not read as "no strands".
|
|
128
|
+
try {
|
|
129
|
+
return execFileSync('git', args, { encoding: 'utf8', maxBuffer: GIT_MAX_BUFFER });
|
|
130
|
+
} catch (e) {
|
|
131
|
+
if (e && e.status === 1 && typeof e.stdout === 'string') return e.stdout;
|
|
132
|
+
throw e;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Evidence A — every task id named by a commit message reachable from origin/main.
|
|
137
|
+
function messageEvidence(ref) {
|
|
138
|
+
return taskIdsIn(gitLines(['log', ref, '--format=%s%n%b']));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Evidence B — every task id named by the CONTENT of a tracked file on origin/main. The repo's
|
|
142
|
+
// doc-comment convention (ADR 0095) cites the task that made a change, so the tree itself is a
|
|
143
|
+
// second, independent record of what landed. -I skips binaries; without it a matching byte run
|
|
144
|
+
// in an image would be counted as a citation.
|
|
145
|
+
function contentEvidence(ref) {
|
|
146
|
+
return taskIdsIn(gitLines(['grep', '-hoIE', 'task[ _-]#?[0-9]{6,7}', ref]));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Every task in an audited status, paged — the corpus is well past one page, and asking for
|
|
150
|
+
// more than the cap is a 400 (task 1003752, which is why buildTaskPageQuery exists).
|
|
151
|
+
// `call` is injected (the strand-watch.js pattern) so tests can drive the paging loop with
|
|
152
|
+
// no session and no network.
|
|
153
|
+
async function fetchAuditedTasks({ call = apiCall } = {}) {
|
|
154
|
+
const rows = [];
|
|
155
|
+
for (const status of AUDITED_STATUSES) {
|
|
156
|
+
let offset = 0;
|
|
157
|
+
for (let page = 0; page < 100; page++) {
|
|
158
|
+
const q = buildTaskPageQuery({ status, limit: TASK_LIST_MAX_LIMIT, offset });
|
|
159
|
+
const qs = new URLSearchParams(q).toString();
|
|
160
|
+
const r = await call('GET', `/api/gds/tasks?${qs}`);
|
|
161
|
+
if (!r || !r.ok) throw new Error(`GET /tasks?status=${status} → ${r && r.status}`);
|
|
162
|
+
const page_rows = (r.data && r.data.tasks) || [];
|
|
163
|
+
rows.push(...page_rows);
|
|
164
|
+
if (page_rows.length < q.limit) break;
|
|
165
|
+
offset += q.limit;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return rows;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
auditArtifacts, declaresArtifact, taskIdsIn,
|
|
173
|
+
AUDITED_STATUSES, ASSURANCE_GATE_DATE, TASK_REF,
|
|
174
|
+
messageEvidence, contentEvidence, fetchAuditedTasks,
|
|
175
|
+
blockerSourceRef, blockerBody, fileBlocker,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// ---- the human surface ------------------------------------------------------------------
|
|
179
|
+
// A journal line is where the previous three strands went to die: the reconciler DID diagnose
|
|
180
|
+
// 1002459 and wrote 'land_not_proven' down, and nobody ever read the field. So a finding goes
|
|
181
|
+
// to the Archon blocker surface, where a person actually looks.
|
|
182
|
+
//
|
|
183
|
+
// IDEMPOTENT BY THE FINDING SET, not by the run. POST /blockers dedupes on
|
|
184
|
+
// (source, source_ref), so re-running with the same findings returns 200 {skipped} instead of
|
|
185
|
+
// filing a second copy — but a CHANGED set (a new unproven task) has a different source_ref
|
|
186
|
+
// and files a fresh blocker. A check that spams is a check that gets muted.
|
|
187
|
+
function blockerSourceRef(stranded) {
|
|
188
|
+
return `unproven:${stranded.map((s) => s.id).sort().join(',')}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function blockerBody(stranded) {
|
|
192
|
+
const lines = [
|
|
193
|
+
`${stranded.length} task(s) at shipped/confirmed/completed declare a code artifact that nothing on \`main\` names — neither a commit message nor any file's content.`,
|
|
194
|
+
'',
|
|
195
|
+
'A task in these statuses has already been credited. If the work is genuinely absent, the ledger is reporting finished work that does not exist (task 1003200).',
|
|
196
|
+
'',
|
|
197
|
+
];
|
|
198
|
+
for (const s of stranded) {
|
|
199
|
+
lines.push(`- **task ${s.id}** (${s.status}, shipped ${s.shipped_at}) — ${s.title.slice(0, 90)}`);
|
|
200
|
+
lines.push(` - declares: ${s.touches.join(', ')}`);
|
|
201
|
+
}
|
|
202
|
+
lines.push('');
|
|
203
|
+
lines.push('**Confirm each by hand before acting.** A deploy/ops task often lists what a RELEASE carried rather than what it committed, and this check cannot tell those apart: the ship-assurance ledger (a no_artifact declaration and its reason) is written to the audit log and no API route exposes it, so "declares no artifact" is inferred from an empty touches[]. Exposing that declaration would make this exact.');
|
|
204
|
+
lines.push('');
|
|
205
|
+
lines.push('Re-run: node scripts/gds/artifact-audit.js');
|
|
206
|
+
return lines.join('\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function fileBlocker(stranded, { call = apiCall } = {}) {
|
|
210
|
+
if (!stranded.length) return { filed: false, reason: 'nothing to report' };
|
|
211
|
+
const r = await call('POST', '/api/gds/blockers', {
|
|
212
|
+
title: `${stranded.length} shipped task(s) claim an artifact nothing on main names`,
|
|
213
|
+
body_md: blockerBody(stranded),
|
|
214
|
+
source: 'artifact-audit',
|
|
215
|
+
source_ref: blockerSourceRef(stranded),
|
|
216
|
+
});
|
|
217
|
+
if (!r || !r.ok) return { filed: false, reason: `POST /blockers → ${r && r.status}` };
|
|
218
|
+
if (r.data && r.data.skipped) return { filed: false, reason: 'already filed for this exact finding set' };
|
|
219
|
+
return { filed: true, id: r.data && r.data.blocker && r.data.blocker.id };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---- CLI --------------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
function strandLine(s) {
|
|
225
|
+
return `Unproven artifact: task ${s.id} (${s.status}, shipped ${s.shipped_at}) claims `
|
|
226
|
+
+ `${s.touches.length} path(s) but nothing on main names it — ${s.touches.slice(0, 2).join(', ')}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function main() {
|
|
230
|
+
const asJson = hasFlag('--json');
|
|
231
|
+
const asLine = hasFlag('--line');
|
|
232
|
+
const includeAll = hasFlag('--all');
|
|
233
|
+
const gateDate = arg('--since') || ASSURANCE_GATE_DATE;
|
|
234
|
+
const wantBlocker = hasFlag('--file-blocker');
|
|
235
|
+
const ref = arg('--ref') || 'origin/main';
|
|
236
|
+
|
|
237
|
+
let result;
|
|
238
|
+
try {
|
|
239
|
+
const [tasks, messageIds, contentIds] = await Promise.all([
|
|
240
|
+
fetchAuditedTasks(),
|
|
241
|
+
Promise.resolve(messageEvidence(ref)),
|
|
242
|
+
Promise.resolve(contentEvidence(ref)),
|
|
243
|
+
]);
|
|
244
|
+
result = auditArtifacts(tasks, { messageIds, contentIds, gateDate });
|
|
245
|
+
result.scanned = tasks.length;
|
|
246
|
+
} catch (e) {
|
|
247
|
+
// Fail-open, loudly on the full report and silently on --line. A check that cannot run
|
|
248
|
+
// must never be mistaken for a check that found nothing (task 1003752's whole lesson).
|
|
249
|
+
if (asLine) return 0;
|
|
250
|
+
console.error(`artifact-audit: could not complete (${e.message})`);
|
|
251
|
+
return 2;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const { stranded, codeless, preGate, evidenced, scanned } = result;
|
|
255
|
+
|
|
256
|
+
if (asLine) {
|
|
257
|
+
if (stranded.length) {
|
|
258
|
+
console.log(
|
|
259
|
+
`${stranded.length} task(s) claim an artifact nothing on main names: `
|
|
260
|
+
+ `${stranded.slice(0, 3).map((s) => s.id).join(', ')}${stranded.length > 3 ? '…' : ''}. `
|
|
261
|
+
+ 'Run `node scripts/gds/artifact-audit.js` — a shipped task whose work never landed is credited work that does not exist.'
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (asJson) {
|
|
268
|
+
// preGate collapses to a COUNT unless --all: it is ~1456 rows that are
|
|
269
|
+
// unprovable by construction, and emitting them by default would bury the
|
|
270
|
+
// handful of rows a consumer actually has to act on.
|
|
271
|
+
console.log(JSON.stringify(includeAll ? result : { ...result, preGate: preGate.length }, null, 2));
|
|
272
|
+
return stranded.length ? 1 : 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log(
|
|
276
|
+
`artifact-audit: ${scanned} task(s) at ${AUDITED_STATUSES.join('/')} — `
|
|
277
|
+
+ `${evidenced.length} proven on ${ref}, ${preGate.length} predate the assurance gate (${gateDate}), `
|
|
278
|
+
+ `${codeless.length} declare no artifact, ${stranded.length} UNPROVEN\n`
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
for (const s of stranded) {
|
|
282
|
+
console.log(`[UNPROVEN] ${strandLine(s)}`);
|
|
283
|
+
console.log(` all paths: ${s.touches.join(', ')}`);
|
|
284
|
+
console.log(` ${s.title.slice(0, 78)}`);
|
|
285
|
+
console.log(' → confirm by hand before acting: a deploy/ops task may list what a RELEASE');
|
|
286
|
+
console.log(' carried rather than what it committed. If the work is genuinely absent, it');
|
|
287
|
+
console.log(' must be redone or the ship corrected — it was credited.\n');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (wantBlocker && stranded.length) {
|
|
291
|
+
const filed = await fileBlocker(stranded);
|
|
292
|
+
console.log(filed.filed
|
|
293
|
+
? `Filed blocker ${filed.id} on the Archon surface.`
|
|
294
|
+
: `Blocker not filed: ${filed.reason}.`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!stranded.length) {
|
|
298
|
+
console.log('Nothing unproven — every post-gate task that claims a code artifact is named on main.');
|
|
299
|
+
}
|
|
300
|
+
if (includeAll && preGate.length) {
|
|
301
|
+
console.log(`\nPre-gate (unprovable by design, ${preGate.length}): ${preGate.slice(0, 20).map((p) => p.id).join(', ')}${preGate.length > 20 ? '…' : ''}`);
|
|
302
|
+
}
|
|
303
|
+
return stranded.length ? 1 : 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (require.main === module) {
|
|
307
|
+
main().then((c) => { process.exitCode = c; })
|
|
308
|
+
.catch((e) => { console.error('artifact-audit:', e.message); process.exit(2); });
|
|
309
|
+
}
|
|
@@ -19,6 +19,13 @@
|
|
|
19
19
|
// Pure helper (no I/O) — resolves the per-rank wandering default so the
|
|
20
20
|
// off-task signal below only fires for the CONSTRAINING ranks (task 1268).
|
|
21
21
|
const wanderingPrefs = require('../../modules/builder-settings/wandering-prefs'); // carved → modules/builder-settings/ (BV1.R80 / ADR 0093 §1)
|
|
22
|
+
// task 1002516 (clause 4 of criterion `accessibility-fluency-profile`): the
|
|
23
|
+
// transcript accessibility dimension. The upload route scores it server-side and
|
|
24
|
+
// stores it on the session row; this file re-scores a row that predates the
|
|
25
|
+
// dimension so the BFG's history is not a blank column. Same sibling-read
|
|
26
|
+
// posture as wanderingPrefs above — scripts/ is outside the module boundary
|
|
27
|
+
// fitness scans (modules/sessions/CLAUDE.md, "NOT in this module").
|
|
28
|
+
const { scoreAccessibility } = require('../../modules/sessions/accessibility');
|
|
22
29
|
const { resolveDefaultVersion } = require('./version-select.js');
|
|
23
30
|
|
|
24
31
|
// ---- classification vocabularies -----------------------------------------
|
|
@@ -229,9 +236,37 @@ function detectFootguns(detail, overhead = {}, opts = {}) {
|
|
|
229
236
|
push('off_task_wandering', wander, firstWander);
|
|
230
237
|
}
|
|
231
238
|
|
|
239
|
+
// 8. unplain speech (task 1002516). The accessibility dimension is scored by
|
|
240
|
+
// modules/sessions/accessibility.js and arrives here already profile-relative
|
|
241
|
+
// — an engineer's session waives every signal and scores null, so it can
|
|
242
|
+
// never reach this branch. Two guards keep the finding worth reading:
|
|
243
|
+
// the rubric's own 6.0 threshold (below it is the 'dense'/'opaque' bands),
|
|
244
|
+
// and a sample big enough to stand behind. count is 1 because this is a
|
|
245
|
+
// property of the WHOLE session, not a tally of turns.
|
|
246
|
+
const acc = opts.accessibility;
|
|
247
|
+
if (acc && acc.score != null && acc.confidence !== 'low' && acc.score < 6) {
|
|
248
|
+
const worst = (acc.signals || [])
|
|
249
|
+
.filter((s) => !s.waived && s.penalty > 0)
|
|
250
|
+
.sort((a, b) => b.penalty - a.penalty)[0];
|
|
251
|
+
push('unplain_speech', 1, `accessibility ${acc.score}/10 (${acc.band})${worst ? ` — worst signal: ${worst.type}${worst.evidence ? `; ${worst.evidence}` : ''}` : ''}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
232
254
|
return out;
|
|
233
255
|
}
|
|
234
256
|
|
|
257
|
+
// accessibilityFor — the dimension for one session record. Prefers the score the
|
|
258
|
+
// upload route already computed (it had the builder's real profile in hand);
|
|
259
|
+
// falls back to re-scoring the stored detail for a row uploaded before the
|
|
260
|
+
// dimension existed. The fallback scores at the DEFAULT profile, which is
|
|
261
|
+
// deliberately stated on the result rather than guessed at — a historical row
|
|
262
|
+
// does not carry what the builder's settings were at the time.
|
|
263
|
+
function accessibilityFor(record) {
|
|
264
|
+
const r = record || {};
|
|
265
|
+
const stored = r.metrics && typeof r.metrics === 'object' ? r.metrics.accessibility : null;
|
|
266
|
+
if (stored && typeof stored === 'object') return stored;
|
|
267
|
+
return { ...scoreAccessibility(Array.isArray(r.detail) ? r.detail : []), rescored: true };
|
|
268
|
+
}
|
|
269
|
+
|
|
235
270
|
// verdictFor — a coarse label off the overhead ratio, for ranking + reporting.
|
|
236
271
|
function verdictFor(overheadRatio) {
|
|
237
272
|
if (overheadRatio >= 0.7) return 'wasteful';
|
|
@@ -246,7 +281,8 @@ function evaluateSession(record) {
|
|
|
246
281
|
const detail = Array.isArray(r.detail) ? r.detail : [];
|
|
247
282
|
const metrics = r.metrics && typeof r.metrics === 'object' ? r.metrics : {};
|
|
248
283
|
const overhead = computeOverhead(detail);
|
|
249
|
-
const
|
|
284
|
+
const accessibility = accessibilityFor(r);
|
|
285
|
+
const footguns = detectFootguns(detail, overhead, { rank: r.rank, accessibility });
|
|
250
286
|
return {
|
|
251
287
|
session_id: r.session_id,
|
|
252
288
|
builder: r.github_login,
|
|
@@ -268,6 +304,10 @@ function evaluateSession(record) {
|
|
|
268
304
|
buckets: overhead.buckets,
|
|
269
305
|
recovery_user_turns: overhead.recoveryUserTurns,
|
|
270
306
|
footguns,
|
|
307
|
+
// task 1002516 — the accessibility dimension, per builder (the record carries
|
|
308
|
+
// github_login + rank), scored against THIS builder's own interaction profile.
|
|
309
|
+
// Advisory: it is reported and coached, and gates nothing.
|
|
310
|
+
accessibility,
|
|
271
311
|
// Verdict is driven off the TURN-weighted ratio (task 961) so cache-dominated
|
|
272
312
|
// marathons can no longer hide their ceremony behind a huge token-work bucket.
|
|
273
313
|
verdict: verdictFor(overhead.overheadRatioTurns),
|
|
@@ -312,6 +352,10 @@ const FINDING_TEMPLATES = {
|
|
|
312
352
|
title: 'Wrong-path / worktree↔main-checkout confusion — the corpus #1 error class',
|
|
313
353
|
body: 'The session evaluator saw repeated wrong-path / wrong-worktree failures (File does not exist, EISDIR, Cannot find module, command not found: psql/timeout) — an edit or command aimed at a path/tool not present on this tree (the worktree↔main-checkout split, feedback_edit_under_worktree_path). Make the claim-time context pack push the worktree root + held paths (V4.R04), keep the worktree write guard loud (V4.R13), and ensure the box has psql/timeout on PATH.',
|
|
314
354
|
},
|
|
355
|
+
unplain_speech: {
|
|
356
|
+
title: 'Sessions read as jargon, terminal-first or implementation-framed to the builder they were pitched at (task 1002516)',
|
|
357
|
+
body: 'The session evaluator scored the accessibility dimension below the 6.0 threshold on sessions whose OWN interaction profile did not sanction it — unexplained SWE vocabulary, a terminal command where the hall already has a click-path, or a trade-off framed as architecture instead of cost, time and what the reader will see. The score is profile-relative (an engineer\'s session waives every signal and is never marked down), so a recurring finding means the accessible-by-default posture is not actually arriving. Sharpen the interaction contract the Conductor injects (modules/builder-settings/interaction-prefs.js), or the click-path table in modules/sessions/accessibility.js if a finding named a UI that does not exist.',
|
|
358
|
+
},
|
|
315
359
|
off_task_wandering: {
|
|
316
360
|
title: 'Off-task wandering — focused-rank agents offering tangents/investigations beyond the claim (task 1268)',
|
|
317
361
|
body: 'The session evaluator saw a constraining-rank builder (Xenos/Thetes, whose wandering default is Locked/Focused) offered to open a new thread beyond the claim — "want me to investigate…", "worth a closer look" — the escalation the wandering knob suppresses (the motivating phantom "credit discrepancy → want me to dig in?" derailment). If this recurs, sharpen the per-rank wandering contract (src/bongos/wandering-prefs.js) the Conductor injects, or build out the level-relative session penalty (the grade-it layer).',
|
|
@@ -388,6 +432,7 @@ module.exports = {
|
|
|
388
432
|
computeOverhead,
|
|
389
433
|
detectFootguns,
|
|
390
434
|
verdictFor,
|
|
435
|
+
accessibilityFor,
|
|
391
436
|
evaluateSession,
|
|
392
437
|
summarizeSystemicFindings,
|
|
393
438
|
findingsByBuilder,
|
|
@@ -477,8 +522,8 @@ if (require.main === module) {
|
|
|
477
522
|
'',
|
|
478
523
|
`Evaluated ${evaluated.length} candidate session(s) (search order=${order}${minKarma != null ? `, min_karma=${minKarma}` : ''}).`,
|
|
479
524
|
'',
|
|
480
|
-
'| session | builder | overhead (turn / tok) | verdict | footguns | tokens | drachmae |',
|
|
481
|
-
'
|
|
525
|
+
'| session | builder | overhead (turn / tok) | verdict | plain speech | footguns | tokens | drachmae |',
|
|
526
|
+
'|---|---|---|---|---|---|---|---|',
|
|
482
527
|
];
|
|
483
528
|
for (const e of evaluated) {
|
|
484
529
|
const fg = cell(e.footguns.map((f) => `${f.type}×${f.count}`).join(', ') || '—');
|
|
@@ -487,7 +532,12 @@ if (require.main === module) {
|
|
|
487
532
|
const oh = `${Math.round((e.overhead_ratio_turns || 0) * 100)}% / ${Math.round((e.overhead_ratio || 0) * 100)}%`;
|
|
488
533
|
// drachmae_earned alongside the overhead (task 722) so a reviewer can weigh
|
|
489
534
|
// efficiency against the reward the session produced.
|
|
490
|
-
|
|
535
|
+
// task 1002516: an em dash means NOT SCORED — the builder's own profile
|
|
536
|
+
// waived every signal — never a zero. `~` marks a score re-derived here at
|
|
537
|
+
// the default profile because the row predates the dimension.
|
|
538
|
+
const a = e.accessibility || {};
|
|
539
|
+
const acc = a.score == null ? '—' : `${a.score}/10 ${a.band}${a.rescored ? ' ~' : ''}${a.confidence === 'low' ? ' (thin)' : ''}`;
|
|
540
|
+
lines.push(`| ${cell(e.session_id ? e.session_id.slice(0, 8) : '?')} | ${cell(e.builder || '?')} | ${oh} | ${cell(e.verdict)} | ${cell(acc)} | ${fg} | ${e.total_tokens || 0} | ${e.drachmae_earned || 0} |`);
|
|
491
541
|
}
|
|
492
542
|
// Systemic findings (footgun classes seen across multiple sessions).
|
|
493
543
|
const systemic = summarizeSystemicFindings(evaluated);
|
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.785'; // 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');
|