@bongos/core 1.19.783 → 1.19.784
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 +59 -24
- 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 +2 -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
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// tests/session_accessibility.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit tests for the ACCESSIBILITY dimension of the session evaluator
|
|
4
|
+
// (task 1002516; clause 4 of criterion `accessibility-fluency-profile`) —
|
|
5
|
+
// modules/sessions/accessibility.js, which is PURE.
|
|
6
|
+
//
|
|
7
|
+
// These pin the four contracts the module's own header names, because those are
|
|
8
|
+
// what make the metric safe rather than harmful. In particular the task's "watch
|
|
9
|
+
// out" is that a metric which punishes an expert's terseness is ACTIVELY harmful,
|
|
10
|
+
// so the terseness and waiver tests below are the point of the file, not trim.
|
|
11
|
+
//
|
|
12
|
+
// Run: node tests/session_accessibility.mjs
|
|
13
|
+
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const acc = require('../modules/sessions/accessibility.js');
|
|
19
|
+
const interactionPrefs = require('../modules/builder-settings/interaction-prefs.js');
|
|
20
|
+
|
|
21
|
+
let passed = 0;
|
|
22
|
+
let failed = 0;
|
|
23
|
+
function test(name, fn) {
|
|
24
|
+
try {
|
|
25
|
+
fn();
|
|
26
|
+
passed++;
|
|
27
|
+
console.log(` ok ${name}`);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
failed++;
|
|
30
|
+
console.log(` FAIL ${name}`);
|
|
31
|
+
console.log(` ${err.message}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---- fixtures -------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
// Plain prose carrying no jargon, no definition marker, no command handoff and
|
|
38
|
+
// no framing term — so a fixture's length can be tuned without changing what any
|
|
39
|
+
// detector sees.
|
|
40
|
+
const FILLER = 'the quick brown fox jumps over the lazy dog again and again ';
|
|
41
|
+
const padTo = (text, chars) => {
|
|
42
|
+
let s = text;
|
|
43
|
+
while (s.length < chars) s += FILLER;
|
|
44
|
+
return s.slice(0, Math.max(text.length, chars));
|
|
45
|
+
};
|
|
46
|
+
const say = (text, chars) => ({ role: 'assistant', snippet: chars ? padTo(text, chars) : text });
|
|
47
|
+
|
|
48
|
+
// ---- the interaction-prefs tripwire ---------------------------------------
|
|
49
|
+
//
|
|
50
|
+
// accessibility.js may not require builder-settings (ADR 0083 forbids a module
|
|
51
|
+
// importing a sibling), so it keys on literal profile values. This test is the
|
|
52
|
+
// coupling: a rename in interaction-prefs must fail HERE rather than silently
|
|
53
|
+
// stop waiving an engineer's session.
|
|
54
|
+
|
|
55
|
+
test('tripwire: every profile value accessibility.js keys on still exists in interaction-prefs', () => {
|
|
56
|
+
for (const [field, values] of Object.entries(acc.PROFILE_VALUES)) {
|
|
57
|
+
assert.ok(interactionPrefs.FIELD_KEYS.includes(field), `interaction-prefs still has field ${field}`);
|
|
58
|
+
for (const v of values) {
|
|
59
|
+
assert.ok(interactionPrefs.isValidValue(field, v), `interaction-prefs still has ${field}=${v}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('tripwire: the defaults accessibility.js falls back to are interaction-prefs own defaults', () => {
|
|
65
|
+
for (const [field, value] of Object.entries(acc.PROFILE_DEFAULTS)) {
|
|
66
|
+
assert.equal(interactionPrefs.defaultValue(field), value, `${field} default`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ---- contract A: profile-relative, never absolute --------------------------
|
|
71
|
+
|
|
72
|
+
test('default profile scores the jargon signal (it is not waived)', () => {
|
|
73
|
+
const w = acc.waiversFor({});
|
|
74
|
+
assert.ok(!w.unexplained_jargon, 'jargon is scored for the default non-technical profile');
|
|
75
|
+
assert.ok(!w.terminal_where_ui_exists, 'terminal-first is scored for a toolchain-new profile');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('engineer waives jargon + framing; comfortable waives terminal-first', () => {
|
|
79
|
+
const eng = acc.waiversFor({ technical_level: 'engineer' });
|
|
80
|
+
assert.ok(eng.unexplained_jargon, 'engineer waives jargon');
|
|
81
|
+
assert.ok(eng.implementation_over_outcome, 'engineer waives implementation framing');
|
|
82
|
+
assert.ok(!eng.terminal_where_ui_exists, 'engineer alone does not waive terminal-first');
|
|
83
|
+
|
|
84
|
+
const comf = acc.waiversFor({ toolchain_familiarity: 'comfortable' });
|
|
85
|
+
assert.ok(comf.terminal_where_ui_exists, 'comfortable waives terminal-first');
|
|
86
|
+
assert.ok(!comf.unexplained_jargon, 'comfortable alone does not waive jargon');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('a waived signal carries the reason that waived it, not a silent absence', () => {
|
|
90
|
+
const r = acc.scoreAccessibility([say('the webhook is idempotent', 400)], {
|
|
91
|
+
profile: { technical_level: 'engineer' },
|
|
92
|
+
});
|
|
93
|
+
const jargon = r.signals.find((s) => s.type === 'unexplained_jargon');
|
|
94
|
+
assert.equal(jargon.waived, true);
|
|
95
|
+
assert.match(jargon.reason, /technical_level: engineer/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('every-signal-waived scores null, never a fabricated 10', () => {
|
|
99
|
+
const dense = say('the async closure memoize the webhook regex upsert', 300);
|
|
100
|
+
const r = acc.scoreAccessibility([dense], {
|
|
101
|
+
profile: { technical_level: 'engineer', toolchain_familiarity: 'comfortable' },
|
|
102
|
+
});
|
|
103
|
+
assert.equal(r.score, null, 'no scored signal → no score');
|
|
104
|
+
assert.equal(r.band, 'n/a');
|
|
105
|
+
assert.ok(r.signals.every((s) => s.waived), 'all three signals waived');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('an expert profile can never score WORSE than the same transcript under the default', () => {
|
|
109
|
+
const detail = [say('the middleware closure will deserialize the jsonb schema', 500)];
|
|
110
|
+
const def = acc.scoreAccessibility(detail, { profile: {} });
|
|
111
|
+
const eng = acc.scoreAccessibility(detail, {
|
|
112
|
+
profile: { technical_level: 'engineer', toolchain_familiarity: 'comfortable' },
|
|
113
|
+
});
|
|
114
|
+
assert.ok(def.score != null && def.score < 10, 'the default profile does score this down');
|
|
115
|
+
assert.equal(eng.score, null, 'the expert profile is not scored down at all');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('mixed technical level relaxes the jargon allowance rather than waiving it', () => {
|
|
119
|
+
// One jargon hit in ~500 sampled chars = a rate of 2.0 per 1000. The default
|
|
120
|
+
// free rate is 1.0 (a penalty); `mixed` doubles it to 2.0 (no penalty).
|
|
121
|
+
const detail = [say('the webhook fires on save', 500)];
|
|
122
|
+
assert.equal(acc.leniencyFor({ technical_level: 'mixed' }, 'unexplained_jargon'), 2);
|
|
123
|
+
const def = acc.scoreAccessibility(detail, { profile: {} });
|
|
124
|
+
const mixed = acc.scoreAccessibility(detail, { profile: { technical_level: 'mixed' } });
|
|
125
|
+
assert.ok(def.score < 10, 'default profile penalises the hit');
|
|
126
|
+
assert.equal(mixed.score, 10, 'mixed absorbs it inside the wider allowance');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ---- contract B: terseness is never a penalty ------------------------------
|
|
130
|
+
|
|
131
|
+
test('the score is a DENSITY: the same rate scores the same at any reply length', () => {
|
|
132
|
+
const short = [say('the webhook the closure the regex', 1000)];
|
|
133
|
+
const long = [say('the webhook the closure the regex', 1000), say('the webhook the closure the regex', 1000)];
|
|
134
|
+
const a = acc.scoreAccessibility(short, { profile: {} });
|
|
135
|
+
const b = acc.scoreAccessibility(long, { profile: {} });
|
|
136
|
+
assert.equal(a.sampled_chars * 2, b.sampled_chars, 'the fixture really is twice as long');
|
|
137
|
+
assert.equal(a.score, b.score, 'twice the prose at the same density scores identically');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('verbosity is not an input — a concise profile is scored exactly like a detailed one', () => {
|
|
141
|
+
const detail = [say('the webhook the closure the regex', 900)];
|
|
142
|
+
const concise = acc.scoreAccessibility(detail, { profile: { verbosity: 'concise' } });
|
|
143
|
+
const detailed = acc.scoreAccessibility(detail, { profile: { verbosity: 'detailed' } });
|
|
144
|
+
assert.equal(concise.score, detailed.score);
|
|
145
|
+
assert.ok(!('verbosity' in concise.profile), 'verbosity is not even carried onto the result');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ---- the detectors ---------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
test('jargon: a term with a nearby definition marker is explained, not counted', () => {
|
|
151
|
+
assert.equal(acc.countUnexplainedJargon(['the webhook fires on save']).count, 1);
|
|
152
|
+
assert.equal(acc.countUnexplainedJargon(['a webhook (a call another site makes to us) fires']).count, 0);
|
|
153
|
+
assert.equal(acc.countUnexplainedJargon(['a webhook — a call another site makes to us']).count, 0);
|
|
154
|
+
assert.equal(acc.countUnexplainedJargon(['a webhook, which means another site calls us']).count, 0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('jargon: matching is word-bounded, so ordinary English is never a hit', () => {
|
|
158
|
+
// "ORM" inside "format", "CORS" inside "scores", "seam" inside "seams" — the
|
|
159
|
+
// substring bug this guards is the difference between a metric and a nuisance.
|
|
160
|
+
assert.equal(acc.countUnexplainedJargon(['the format of the scores is fine']).count, 0);
|
|
161
|
+
assert.equal(acc.countUnexplainedJargon(['performance is normal and the form works']).count, 0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('jargon: multi-token and punctuated terms still anchor', () => {
|
|
165
|
+
assert.equal(acc.countUnexplainedJargon(['this is a race condition']).count, 1);
|
|
166
|
+
assert.equal(acc.countUnexplainedJargon(['a classic N+1 here']).count, 1);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('terminal-first: only counted when the subject HAS a click-path', () => {
|
|
170
|
+
const withUi = acc.countTerminalWhereUiExists(['run `node scripts/gds/api-key.js` to set your api key']);
|
|
171
|
+
assert.equal(withUi.count, 1);
|
|
172
|
+
assert.match(withUi.evidence, /Settings/, 'the evidence names where the button is');
|
|
173
|
+
|
|
174
|
+
// A command for something with no UI at all is the only way to do the job.
|
|
175
|
+
assert.equal(acc.countTerminalWhereUiExists(['run `npm test` to check the suite']).count, 0);
|
|
176
|
+
// Naming a UI-backed subject without handing over a command is not a finding.
|
|
177
|
+
assert.equal(acc.countTerminalWhereUiExists(['your api key is missing']).count, 0);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('framing: implementation taxonomy counts only when no outcome framing rides with it', () => {
|
|
181
|
+
assert.equal(acc.countImplementationOverOutcome(['this changes the module boundary and the data model']).count, 1);
|
|
182
|
+
assert.equal(
|
|
183
|
+
acc.countImplementationOverOutcome(['this changes the data model, so it costs about 20 minutes']).count,
|
|
184
|
+
0,
|
|
185
|
+
'naming the architecture AND what it costs is exactly right'
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ---- scoring shape ---------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
test('score is monotonic in the finding rate and clamped to [0,10]', () => {
|
|
192
|
+
const clean = acc.scoreAccessibility([say('we changed the button label', 1000)], { profile: {} });
|
|
193
|
+
const some = acc.scoreAccessibility([say('the webhook the closure the regex', 1000)], { profile: {} });
|
|
194
|
+
const awful = acc.scoreAccessibility(
|
|
195
|
+
[say('the webhook closure regex upsert jsonb middleware singleton polyfill debounce mutex', 200)],
|
|
196
|
+
{ profile: {} }
|
|
197
|
+
);
|
|
198
|
+
assert.equal(clean.score, 10);
|
|
199
|
+
assert.ok(some.score < clean.score, 'more findings score lower');
|
|
200
|
+
assert.ok(awful.score < some.score, 'many more findings score lower still');
|
|
201
|
+
assert.ok(awful.score >= 0, 'never below 0');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('no assistant prose → null score and an honest confidence, not a default pass', () => {
|
|
205
|
+
const r = acc.scoreAccessibility([], { profile: {} });
|
|
206
|
+
assert.equal(r.score, null);
|
|
207
|
+
assert.equal(r.confidence, 'none');
|
|
208
|
+
assert.equal(r.sampled_turns, 0);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('user turns are not scored — the metric judges the agent, not the builder', () => {
|
|
212
|
+
const detail = [
|
|
213
|
+
{ role: 'user', snippet: 'the webhook closure regex upsert jsonb middleware' },
|
|
214
|
+
say('we changed the button label', 1000),
|
|
215
|
+
];
|
|
216
|
+
const r = acc.scoreAccessibility(detail, { profile: {} });
|
|
217
|
+
assert.equal(r.sampled_turns, 1, 'only the assistant turn was sampled');
|
|
218
|
+
assert.equal(r.score, 10, 'the builder\'s own jargon does not count against the agent');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('a thin sample says so rather than pretending to a normal reading', () => {
|
|
222
|
+
const r = acc.scoreAccessibility([say('we changed the button label')], { profile: {} });
|
|
223
|
+
assert.ok(r.sampled_chars < acc.LOW_CONFIDENCE_CHARS);
|
|
224
|
+
assert.equal(r.confidence, 'low');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('an unknown or partial profile falls back to the accessible default, never unscored', () => {
|
|
228
|
+
const r = acc.scoreAccessibility([say('the webhook fires', 1000)], {
|
|
229
|
+
profile: { technical_level: 'wizard', toolchain_familiarity: null },
|
|
230
|
+
});
|
|
231
|
+
assert.equal(r.profile.technical_level, 'non_technical');
|
|
232
|
+
assert.equal(r.profile.toolchain_familiarity, 'new');
|
|
233
|
+
assert.ok(r.score != null, 'a junk profile is still scored, at the default posture');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('the signal spec can never penalise more than a 10 in total', () => {
|
|
237
|
+
const total = Object.values(acc.SIGNAL_SPEC).reduce((n, s) => n + s.max, 0);
|
|
238
|
+
assert.equal(total, 10, 'the three signals sum to exactly the full scale');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
console.log('');
|
|
242
|
+
console.log(`Session accessibility (clause 4) tests: ${passed} passed, ${failed} failed`);
|
|
243
|
+
if (failed > 0) process.exit(1);
|
package/tests/session_eval.mjs
CHANGED
|
@@ -331,6 +331,57 @@ test('evaluateSession: safe on empty/malformed', () => {
|
|
|
331
331
|
assert.equal(ev.evaluateSession(null).verdict, 'efficient');
|
|
332
332
|
});
|
|
333
333
|
|
|
334
|
+
// ---- accessibility dimension (task 1002516, clause 4) ----
|
|
335
|
+
//
|
|
336
|
+
// The dimension itself is tested in tests/session_accessibility.mjs; these pin
|
|
337
|
+
// the WIRING — that the evaluator prefers the score the upload route already
|
|
338
|
+
// computed, re-scores a row that predates it, and only raises a footgun when the
|
|
339
|
+
// finding is both real and profile-sanctioned-free.
|
|
340
|
+
|
|
341
|
+
test('accessibilityFor prefers the stored score over re-scoring', () => {
|
|
342
|
+
const stored = { score: 4.2, band: 'dense', signals: [], confidence: 'normal' };
|
|
343
|
+
const r = ev.accessibilityFor({ metrics: { accessibility: stored }, detail: [] });
|
|
344
|
+
assert.equal(r, stored, 'the route\'s score — it had the real profile — is used as-is');
|
|
345
|
+
assert.ok(!r.rescored);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('accessibilityFor re-scores a row that predates the dimension, and says so', () => {
|
|
349
|
+
const r = ev.accessibilityFor({ metrics: {}, detail: [{ role: 'assistant', snippet: 'we changed the button label' }] });
|
|
350
|
+
assert.equal(r.rescored, true, 'a re-derived score is marked, not passed off as the original');
|
|
351
|
+
assert.ok('score' in r);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('detectFootguns: unplain_speech fires below the 6.0 threshold', () => {
|
|
355
|
+
const acc = { score: 3.5, band: 'opaque', confidence: 'normal', signals: [{ type: 'unexplained_jargon', waived: false, penalty: 3, evidence: '"webhook" undefined' }] };
|
|
356
|
+
const out = ev.detectFootguns([], {}, { accessibility: acc });
|
|
357
|
+
const f = out.find((x) => x.type === 'unplain_speech');
|
|
358
|
+
assert.ok(f, 'the footgun fires');
|
|
359
|
+
assert.match(f.evidence, /unexplained_jargon/, 'the evidence names the worst signal');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('detectFootguns: unplain_speech never fires on a waived (null) score or a thin sample', () => {
|
|
363
|
+
// An engineer's session waives every signal and scores null — it must be
|
|
364
|
+
// impossible for it to reach this finding at all.
|
|
365
|
+
const waived = { score: null, band: 'n/a', confidence: 'normal', signals: [] };
|
|
366
|
+
assert.equal(ev.detectFootguns([], {}, { accessibility: waived }).some((x) => x.type === 'unplain_speech'), false);
|
|
367
|
+
const thin = { score: 2, band: 'opaque', confidence: 'low', signals: [] };
|
|
368
|
+
assert.equal(ev.detectFootguns([], {}, { accessibility: thin }).some((x) => x.type === 'unplain_speech'), false);
|
|
369
|
+
const fine = { score: 8, band: 'clear', confidence: 'normal', signals: [] };
|
|
370
|
+
assert.equal(ev.detectFootguns([], {}, { accessibility: fine }).some((x) => x.type === 'unplain_speech'), false);
|
|
371
|
+
assert.equal(ev.detectFootguns([], {}, {}).some((x) => x.type === 'unplain_speech'), false);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test('evaluateSession surfaces the accessibility dimension per session', () => {
|
|
375
|
+
const r = ev.evaluateSession({
|
|
376
|
+
session_id: 's1',
|
|
377
|
+
github_login: 'alice',
|
|
378
|
+
metrics: { accessibility: { score: 7.5, band: 'workable', confidence: 'normal', signals: [] } },
|
|
379
|
+
detail: [],
|
|
380
|
+
});
|
|
381
|
+
assert.equal(r.accessibility.score, 7.5);
|
|
382
|
+
assert.equal(r.builder, 'alice', 'the score is attributable to a builder');
|
|
383
|
+
});
|
|
384
|
+
|
|
334
385
|
test('detectFootguns: a null/non-object turn does not throw (regression)', () => {
|
|
335
386
|
// computeOverhead guards each turn; detectFootguns must too, or one malformed
|
|
336
387
|
// turn crashes the whole evaluation.
|
|
@@ -376,7 +427,7 @@ test('findingsByBuilder groups flagged sessions; excludes footgun-free builders'
|
|
|
376
427
|
});
|
|
377
428
|
|
|
378
429
|
test('FINDING_TEMPLATES cover every footgun class', () => {
|
|
379
|
-
for (const t of ['guess_and_retry_api', 'heavyweight_pipeline_on_trivial_change', 'stale_test_red_herring', 'repeated_tool_failures', 'empty_diff_grade', 'claim_consumed_on_fail', 'wrong_path_worktree', 'off_task_wandering']) {
|
|
430
|
+
for (const t of ['guess_and_retry_api', 'heavyweight_pipeline_on_trivial_change', 'stale_test_red_herring', 'repeated_tool_failures', 'empty_diff_grade', 'claim_consumed_on_fail', 'wrong_path_worktree', 'off_task_wandering', 'unplain_speech']) {
|
|
380
431
|
assert.ok(ev.FINDING_TEMPLATES[t] && ev.FINDING_TEMPLATES[t].title, `template for ${t}`);
|
|
381
432
|
}
|
|
382
433
|
});
|