@bongos/core 1.19.790 → 1.19.791
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 +46 -26
- package/docs/copy-inventory.md +59 -59
- package/docs/copy-registry.json +68 -68
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/hall-kit.js +7 -65
- package/modules/hall-ui/public/hall-render.js +1 -1
- package/modules/hall-ui/public/hall-visual.js +85 -0
- package/modules/hall-ui/public/index.html +1 -0
- package/modules/hall-ui/public/profile.html +1 -0
- package/modules/hall-ui/public/profile.js +2 -2
- package/modules/hall-ui/public/studio.css +1 -1
- package/modules/hall-ui/public/studio.html +1 -0
- package/modules/hall-ui/public/studio.js +4 -3
- package/modules/hall-ui/public/style.css +0 -16
- package/modules/hall-ui/public/task.html +1 -0
- package/modules/hall-ui/public/task.js +10 -13
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/accessibility-replay.js +195 -0
- package/src/module-api.js +1 -1
- package/tests/accessibility_replay.mjs +201 -0
- package/tests/fixtures/accessibility-replay.json +93 -0
- package/tests/hall_studio_world.mjs +19 -11
- package/tests/task_visuals.mjs +91 -27
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// modules/hall-ui/public/hall-visual.js — the ship-time task visual, in one place
|
|
2
|
+
// (task 1003835; the renderers themselves are tasks 1003109 and 1003829).
|
|
3
|
+
//
|
|
4
|
+
// A satellite of hall-kit.js rather than a part of it, and that is the decision
|
|
5
|
+
// this file records. These three functions lived in the kit, which is 61KB of
|
|
6
|
+
// pager/ledger/filter/chart machinery. The task record page (task.html) renders a
|
|
7
|
+
// visual and needs NOTHING else the kit holds, so converging it onto the kit's
|
|
8
|
+
// renderer would have meant loading 61KB on the hall's most-visited record page
|
|
9
|
+
// for one function — a real cost, paid on the wrong page. The alternative was to
|
|
10
|
+
// let task.js keep its own copy, which is how the hall ended up with two.
|
|
11
|
+
//
|
|
12
|
+
// So the helpers moved OUT. Any page that renders a visual loads this file; the
|
|
13
|
+
// kit no longer carries them and the pages that only use the kit no longer pay
|
|
14
|
+
// for them. The owner's call (2026-09-16) was that the kit may grow satellites
|
|
15
|
+
// like this one.
|
|
16
|
+
//
|
|
17
|
+
// Contract, identical to the kit's own: dependency-free (its own esc(), no
|
|
18
|
+
// window.OTB use) so load order only needs "before the page script";
|
|
19
|
+
// node-require-safe, so tests/task_visuals.mjs can require it with no DOM;
|
|
20
|
+
// string-template builders returning HTML, no state, no render loop.
|
|
21
|
+
//
|
|
22
|
+
// CSS: each caller supplies its own BEM base, because SIZE is the surface's
|
|
23
|
+
// decision and not this file's — the task record caps at 52vh in a record column
|
|
24
|
+
// (.tk-visual, style.css/task.css), the Studio gives the plate the card's whole
|
|
25
|
+
// width (.studio-plate). The markup, the URL guard and the escaping are shared.
|
|
26
|
+
(() => {
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
// HTML-escape for text interpolated into innerHTML.
|
|
30
|
+
function esc(s) {
|
|
31
|
+
return String(s ?? '').replace(/[&<>"']/g, (c) => ({
|
|
32
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
33
|
+
}[c]));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Same-origin, SERVER-MINTED paths only. The URL arrives from our own API, but
|
|
37
|
+
// it lands in an href and a src, so it is matched literally against the shape
|
|
38
|
+
// modules/lifecycle/task-visuals.js mints rather than trusted. Anything else
|
|
39
|
+
// renders nothing at all.
|
|
40
|
+
const VISUAL_URL_RE = /^\/api\/(gds|bongos)\/task-visuals\/task-\d+-[0-9a-f]{16}\.(png|jpg|gif|webp)$/;
|
|
41
|
+
function isTaskVisualUrl(url) {
|
|
42
|
+
return VISUAL_URL_RE.test(String(url ?? ''));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The compact thumbnail, for a FEED — where the summary is one line in a list,
|
|
46
|
+
// so the image appears small and links to the full-size file. Three feeds draw
|
|
47
|
+
// it (the hall's ship ledger, a profile's recent ships, the status dashboard).
|
|
48
|
+
// Returns '' when the row carries no visual: a task without one must never look
|
|
49
|
+
// incomplete, so nothing is drawn and there is no placeholder.
|
|
50
|
+
function shipVisualThumbHtml(row) {
|
|
51
|
+
const url = String((row && row.visual_url) ?? '');
|
|
52
|
+
if (!isTaskVisualUrl(url)) return '';
|
|
53
|
+
const alt = String((row && row.visual_alt) || `Visual for task ${(row && row.id) ?? ''}`).trim();
|
|
54
|
+
return `<a href="${esc(url)}" target="_blank" rel="noopener" title="${esc(alt)}">` +
|
|
55
|
+
`<img class="ship-visual-thumb" src="${esc(url)}" alt="${esc(alt)}" loading="lazy"></a>`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The FULL-SIZE form: the picture as the thing being looked at, with visual_alt
|
|
59
|
+
// as its caption. For a surface whose subject IS the image — the task record
|
|
60
|
+
// (.tk-visual) and the Studio's review card (.studio-plate).
|
|
61
|
+
//
|
|
62
|
+
// `className` is the caller's BEM base; it is sanitised because it reaches a
|
|
63
|
+
// class attribute. Returns '' with no visual, like the thumbnail.
|
|
64
|
+
function taskVisualFigureHtml(row, { className = 'task-visual' } = {}) {
|
|
65
|
+
const url = String((row && row.visual_url) ?? '');
|
|
66
|
+
if (!isTaskVisualUrl(url)) return '';
|
|
67
|
+
const written = String((row && row.visual_alt) || '').trim();
|
|
68
|
+
const alt = written || `Visual for task ${(row && row.id) ?? ''}`;
|
|
69
|
+
const base = String(className).replace(/[^A-Za-z0-9_-]/g, '');
|
|
70
|
+
// The caption is only drawn when the shipper WROTE one — the fallback alt is
|
|
71
|
+
// a machine sentence for a screen reader, not a caption to show a reader.
|
|
72
|
+
const caption = written ? `<figcaption class="${base}__caption">${esc(written)}</figcaption>` : '';
|
|
73
|
+
return `<figure class="${base}">` +
|
|
74
|
+
`<a href="${esc(url)}" target="_blank" rel="noopener">` +
|
|
75
|
+
`<img class="${base}__img" src="${esc(url)}" alt="${esc(alt)}" loading="lazy">` +
|
|
76
|
+
`</a>${caption}</figure>`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const api = { isTaskVisualUrl, shipVisualThumbHtml, taskVisualFigureHtml };
|
|
80
|
+
|
|
81
|
+
// Node-require-safe (the dom-utils.js / hall-kit.js pattern): assignment is the
|
|
82
|
+
// only eval-time side effect, and only when a window exists.
|
|
83
|
+
if (typeof window !== 'undefined') window.OTBVisual = api;
|
|
84
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
85
|
+
})();
|
|
@@ -110,6 +110,7 @@
|
|
|
110
110
|
<script src="/builders/box-panel.js?v=2026-06-14-domutils" defer></script>
|
|
111
111
|
<!-- task 2166: the shared component kit (window.OTBKit) — pager/ledger/rail.
|
|
112
112
|
Loaded BEFORE hall-widgets.js, whose skeletons call OTBKit.pagerBarHtml(). -->
|
|
113
|
+
<script src="/builders/hall-visual.js?v=2026-09-16-visual1" defer></script>
|
|
113
114
|
<script src="/builders/hall-kit.js" defer></script>
|
|
114
115
|
<!-- BV1.R75: the CORE hall widgets register into window.OTB's hall-widget registry.
|
|
115
116
|
Loaded BEFORE builders.js so the registry is populated when builders.js stamps
|
|
@@ -196,6 +196,7 @@
|
|
|
196
196
|
<!-- hall-kit (window.OTBKit) — profile.js renders ship-time task visuals through
|
|
197
197
|
its shared thumbnail helper (task 1003109) and builds the section rail with
|
|
198
198
|
it (task 1003489), so it must load first. -->
|
|
199
|
+
<script src="/builders/hall-visual.js?v=2026-09-16-visual1"></script>
|
|
199
200
|
<script src="/builders/hall-kit.js?v=2026-08-27-activity"></script>
|
|
200
201
|
<!-- Connections (section 6). Loaded BEFORE profile.js so window.OTBConnections
|
|
201
202
|
exists by the time renderProfile runs; profile.js guards on it anyway, so a
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
const summary = s.value_summary
|
|
177
177
|
? `<span style="display:block;margin-top:2px;font-size:12px;color:var(--ink-faint)">${escapeHtml(s.value_summary)}</span>`
|
|
178
178
|
: '';
|
|
179
|
-
const visual = window.
|
|
179
|
+
const visual = window.OTBVisual ? window.OTBVisual.shipVisualThumbHtml(s) : '';
|
|
180
180
|
return `<tr>
|
|
181
181
|
<td class="ledger__num">#${escapeHtml(String(s.id))}</td>
|
|
182
182
|
<td><a href="${taskHref(s.id)}">${escapeHtml(s.title || `Task ${s.id}`)}</a>${summary}${visual ? `<span style="display:block;margin-top:6px">${visual}</span>` : ''}</td>
|
|
@@ -473,7 +473,7 @@
|
|
|
473
473
|
const summary = t.value_summary
|
|
474
474
|
? `<span style="display:block;margin-top:2px;font-size:12px;color:var(--ink-faint)">${escapeHtml(t.value_summary)}</span>`
|
|
475
475
|
: '';
|
|
476
|
-
const visual =
|
|
476
|
+
const visual = window.OTBVisual ? window.OTBVisual.shipVisualThumbHtml(t) : '';
|
|
477
477
|
return `<tr>
|
|
478
478
|
<td class="ledger__num">#${escapeHtml(String(t.id))}</td>
|
|
479
479
|
<td><a href="${taskHref(t.id)}">${escapeHtml(t.title || `Task ${t.id}`)}</a>${summary}${visual ? `<span style="display:block;margin-top:6px">${visual}</span>` : ''}</td>
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
IS the card: an artist is being asked to LOOK at something, so it is given the
|
|
156
156
|
card's full width and as much height as a screen can spare, not the 44px
|
|
157
157
|
thumbnail a ship feed uses. Markup comes from the kit's one renderer
|
|
158
|
-
(
|
|
158
|
+
(OTBVisual.taskVisualFigureHtml); only the size is this room's decision.
|
|
159
159
|
|
|
160
160
|
max-height in vh, and MEASURED at 1440x900 rather than chosen. The size an
|
|
161
161
|
artist can judge it at wins over a zero-scroll card — that is the task's own
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
<!-- The kit, for its ONE renderer: the full-size ship-time visual figure
|
|
73
73
|
(task 1003829). The copy desk — the artist's other room — already loads it,
|
|
74
74
|
and a fourth hand-rolled <figure> is what the kit exists to stop. -->
|
|
75
|
+
<script src="/builders/hall-visual.js?v=2026-09-16-visual1"></script>
|
|
75
76
|
<script src="/builders/hall-kit.js"></script>
|
|
76
77
|
<script src="/builders/studio.js?v=2026-09-10-studio2"></script>
|
|
77
78
|
</body>
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
// ---------------------------------------------------------------------------
|
|
133
133
|
|
|
134
134
|
// THE PLATE — the reviewed work's ship-time visual, at the size the card can
|
|
135
|
-
// give it. Through the
|
|
135
|
+
// give it. Through the one renderer (OTBVisual.taskVisualFigureHtml, which
|
|
136
136
|
// carries the server-minted-URL guard and the escaping) with the Studio's own
|
|
137
137
|
// class, because the size is this surface's decision: here the picture is the
|
|
138
138
|
// subject, not an illustration beside a record.
|
|
@@ -141,8 +141,9 @@
|
|
|
141
141
|
// read, or when the kit is absent — a review with no picture must read as a
|
|
142
142
|
// review, never as an empty frame (the degrade rule).
|
|
143
143
|
function plateHtml(p) {
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
const vis = window.OTBVisual;
|
|
145
|
+
if (!p.shipped || !vis || typeof vis.taskVisualFigureHtml !== 'function') return '';
|
|
146
|
+
return vis.taskVisualFigureHtml(p.shipped, { className: 'studio-plate' });
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
// The three weights, and the one field they share. Offered only to a viewer
|
|
@@ -2841,22 +2841,6 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
|
|
|
2841
2841
|
overflow-y: auto;
|
|
2842
2842
|
}
|
|
2843
2843
|
.td-touches { font-size: 12.5px; color: var(--ink-faint); font-family: var(--font-mono); word-break: break-word; margin: 0 0 12px; }
|
|
2844
|
-
/* task 1003109: the ship-time VISUAL, rendered directly under the value summary
|
|
2845
|
-
it restates. Sized to be READ (this exists so a non-engineer can see a change),
|
|
2846
|
-
capped so a tall screenshot can't push the rest of the record off the page, and
|
|
2847
|
-
never drawn at all when a task has no visual. */
|
|
2848
|
-
.td-visual { margin: 0 0 12px; }
|
|
2849
|
-
.td-visual__img {
|
|
2850
|
-
display: block;
|
|
2851
|
-
max-width: 100%;
|
|
2852
|
-
max-height: 52vh;
|
|
2853
|
-
width: auto;
|
|
2854
|
-
height: auto;
|
|
2855
|
-
border: var(--line) solid var(--rule-soft);
|
|
2856
|
-
border-radius: var(--radius-sm);
|
|
2857
|
-
background: var(--glass);
|
|
2858
|
-
}
|
|
2859
|
-
.td-visual__caption { font-size: 12.5px; color: var(--ink-faint); margin: 6px 0 0; }
|
|
2860
2844
|
/* The compact form: a thumbnail beside a value summary in a ship FEED, where the
|
|
2861
2845
|
summary is one line in a list and a full-width image would drown it. */
|
|
2862
2846
|
.ship-visual-thumb {
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
<script src="/builders/task-detail.js?v=2026-07-03-taskdetail2" defer></script>
|
|
69
69
|
<script src="/builders/brand-apply.js?v=2026-06-19-r56" defer></script>
|
|
70
70
|
<script src="/builders/md-reader.js" defer></script>
|
|
71
|
+
<script src="/builders/hall-visual.js?v=2026-09-16-visual1" defer></script>
|
|
71
72
|
<script src="/builders/task.js?v=2026-07-03-taskpage1" defer></script>
|
|
72
73
|
</body>
|
|
73
74
|
</html>
|
|
@@ -292,20 +292,17 @@
|
|
|
292
292
|
// a detail can be opened full-size. The caption (visual_alt) doubles as the alt
|
|
293
293
|
// text, which is why it is stored: an image with no alt tells a screen reader
|
|
294
294
|
// nothing, and "a picture of the change" is exactly the audience that needs it.
|
|
295
|
+
// The shared renderer (hall-visual.js), not a copy of it — task 1003835. This
|
|
296
|
+
// page hand-rolled the same <figure> until then; the markup it produced is what
|
|
297
|
+
// OTBVisual emits, so .tk-visual's rules are unchanged. `className` keeps the
|
|
298
|
+
// sizing this page's own CSS owns (52vh inside a record column).
|
|
299
|
+
//
|
|
300
|
+
// Guarded rather than assumed: hall-visual.js is a separate <script>, so a page
|
|
301
|
+
// that forgot it should render no picture, never a broken one.
|
|
295
302
|
function taskVisualHtml(t) {
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
// trusting: anything that is not the shape our serve route mints is dropped.
|
|
300
|
-
if (!/^\/api\/(gds|bongos)\/task-visuals\/task-\d+-[0-9a-f]{16}\.(png|jpg|gif|webp)$/.test(url)) return '';
|
|
301
|
-
const alt = String(t.visual_alt || `Visual for task ${t.id}`);
|
|
302
|
-
const caption = t.visual_alt
|
|
303
|
-
? `<figcaption class="tk-visual__caption">${escapeHtml(t.visual_alt)}</figcaption>`
|
|
304
|
-
: '';
|
|
305
|
-
return `<figure class="tk-visual">` +
|
|
306
|
-
`<a href="${escapeHtml(url)}" target="_blank" rel="noopener">` +
|
|
307
|
-
`<img class="tk-visual__img" src="${escapeHtml(url)}" alt="${escapeHtml(alt)}" loading="lazy">` +
|
|
308
|
-
`</a>${caption}</figure>`;
|
|
303
|
+
const vis = window.OTBVisual;
|
|
304
|
+
if (!vis || typeof vis.taskVisualFigureHtml !== 'function') return '';
|
|
305
|
+
return vis.taskVisualFigureHtml(t, { className: 'tk-visual' });
|
|
309
306
|
}
|
|
310
307
|
|
|
311
308
|
async function copyText(text) {
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.791",
|
|
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.791",
|
|
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.791",
|
|
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,195 @@
|
|
|
1
|
+
// scripts/gds/accessibility-replay.js — the two-profile replay regression for the
|
|
2
|
+
// accessibility dimension (task 1002517; clauses 5 and 8 of criterion
|
|
3
|
+
// `accessibility-fluency-profile`, which named this run as its OWN proof).
|
|
4
|
+
//
|
|
5
|
+
// WHAT IT PROVES, and why it is not already covered.
|
|
6
|
+
// tests/session_accessibility.mjs pins the scorer's contracts against synthetic
|
|
7
|
+
// fixtures it builds itself — a test of the arithmetic. It cannot answer the
|
|
8
|
+
// criterion's actual question, which is about PROSE: does the dimension reward a
|
|
9
|
+
// reply a newcomer can use, and does it leave an expert's reply alone? So this
|
|
10
|
+
// harness takes a FIXED set of eight questions a newcomer really asks, each the
|
|
11
|
+
// documented trigger of a surface that exists, answered twice — once in each
|
|
12
|
+
// register — and scores both runs.
|
|
13
|
+
//
|
|
14
|
+
// Four legs. Each one fails a different way of being wrong:
|
|
15
|
+
//
|
|
16
|
+
// 1. FLOOR the non-technical run, under the non-technical profile,
|
|
17
|
+
// clears FLOOR. The dimension recognises accessible prose.
|
|
18
|
+
// 2. NO PENALTY the engineer run, under the engineer profile, is not
|
|
19
|
+
// scored at all — every signal waived, score null.
|
|
20
|
+
// 3. DISCRIMINATION the SAME engineer prose under the NON-TECHNICAL profile
|
|
21
|
+
// falls BELOW FLOOR, with every detector registering hits.
|
|
22
|
+
// Without this leg 1 proves nothing — a scorer that returned
|
|
23
|
+
// 10 for everything, or three detectors that had gone blind,
|
|
24
|
+
// would pass leg 1 and look identical to a working one.
|
|
25
|
+
// 4. LENGTH `doubleRun` says the same prose twice. Chars double, hits
|
|
26
|
+
// double, the rate is unchanged, so the score must be
|
|
27
|
+
// IDENTICAL. That is the metric reading density rather than
|
|
28
|
+
// volume, asserted against real prose.
|
|
29
|
+
//
|
|
30
|
+
// WHERE TERSENESS PROTECTION ACTUALLY COMES FROM, since the task's "watch out"
|
|
31
|
+
// is a metric that punishes an expert's brevity. It is leg 2 — the WAIVER — not
|
|
32
|
+
// the per-1000-character rates. The rates make the score independent of how long
|
|
33
|
+
// an answer is at a fixed density (leg 4); they do NOT make it independent of
|
|
34
|
+
// density, and they should not: three undefined terms in three hundred
|
|
35
|
+
// characters really is denser than three in three thousand. What protects the
|
|
36
|
+
// expert is that their profile removes those two signals from the scoring set
|
|
37
|
+
// entirely. `terseVariant` — every turn cut to its first sentence, a genuinely
|
|
38
|
+
// shorter and thinner answer — is checked too, asserting only that answering
|
|
39
|
+
// more briefly never COSTS points; it is a guard, not the mechanism.
|
|
40
|
+
//
|
|
41
|
+
// One honest asymmetry this surfaced and the corpus now records: under
|
|
42
|
+
// `technical_level: engineer` with `toolchain_familiarity: new`, the engineer
|
|
43
|
+
// run still scores, because terminal-where-a-click-path-exists is waived by the
|
|
44
|
+
// TOOLCHAIN setting, not the technical one. "I think in code" and "I live in a
|
|
45
|
+
// terminal" are two different answers, and the scorer is right to keep them
|
|
46
|
+
// apart. tests/accessibility_replay.mjs pins that pairing so a future widening
|
|
47
|
+
// of the engineer waiver has to be a deliberate edit.
|
|
48
|
+
//
|
|
49
|
+
// The corpus is data — tests/fixtures/accessibility-replay.json — so adding a
|
|
50
|
+
// prompt is an edit to JSON, not to code.
|
|
51
|
+
//
|
|
52
|
+
// Run: node scripts/gds/accessibility-replay.js
|
|
53
|
+
// node scripts/gds/accessibility-replay.js --json
|
|
54
|
+
|
|
55
|
+
'use strict';
|
|
56
|
+
|
|
57
|
+
const fs = require('node:fs');
|
|
58
|
+
const path = require('node:path');
|
|
59
|
+
|
|
60
|
+
const { scoreAccessibility, bandFor } = require('../../modules/sessions/accessibility.js');
|
|
61
|
+
|
|
62
|
+
const CORPUS_PATH = path.join(__dirname, '..', '..', 'tests', 'fixtures', 'accessibility-replay.json');
|
|
63
|
+
|
|
64
|
+
// FLOOR — the score a non-technical run has to clear. It is bandFor()'s
|
|
65
|
+
// 'workable' boundary and deliberately not a second independent number: the
|
|
66
|
+
// scorer already draws the line between prose a newcomer can work with and prose
|
|
67
|
+
// that is merely dense, and inventing a different one here would let the two
|
|
68
|
+
// drift. tests/accessibility_replay.mjs asserts they stay tied.
|
|
69
|
+
const FLOOR = 6.0;
|
|
70
|
+
|
|
71
|
+
function loadCorpus(file = CORPUS_PATH) {
|
|
72
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// terseVariant — each turn cut to its first sentence: a shorter, thinner answer.
|
|
76
|
+
// Mechanical on purpose. A hand-written "terse version" in the corpus would be a
|
|
77
|
+
// version tuned until it passed; taking the first sentence of prose written for
|
|
78
|
+
// another purpose is a variant nobody got to tune.
|
|
79
|
+
function terseVariant(turns) {
|
|
80
|
+
return turns.map((t) => {
|
|
81
|
+
const m = /^[^.!?]*[.!?]/.exec(t);
|
|
82
|
+
return m ? m[0] : t;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// doubleRun — the same prose said twice. Every count doubles and so does the
|
|
87
|
+
// character total, so every rate is unchanged and the score must come back
|
|
88
|
+
// EXACTLY equal. This is the length-independence property at prose altitude: a
|
|
89
|
+
// scorer that had drifted to absolute counts would double its penalties here.
|
|
90
|
+
function doubleRun(turns) {
|
|
91
|
+
return [...turns, ...turns];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// asDetail — a run's replies as the per-turn digest shape scoreAccessibility
|
|
95
|
+
// reads. The user turns are included because a real digest has them and
|
|
96
|
+
// assistantProse must keep ignoring them; if it ever stopped, the prompts would
|
|
97
|
+
// start scoring the builder's own words and every number here would move.
|
|
98
|
+
function asDetail(corpus, replies, { transform } = {}) {
|
|
99
|
+
const detail = [];
|
|
100
|
+
let i = 0;
|
|
101
|
+
for (const { id, prompt } of corpus.prompts) {
|
|
102
|
+
detail.push({ i: i++, role: 'user', snippet: prompt });
|
|
103
|
+
const turns = replies[id] || [];
|
|
104
|
+
for (const snippet of transform ? transform(turns) : turns) {
|
|
105
|
+
detail.push({ i: i++, role: 'assistant', snippet });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return detail;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const TRANSFORMS = { terse: terseVariant, doubled: doubleRun };
|
|
112
|
+
|
|
113
|
+
// replay — score one named run under one profile. `profile` defaults to the
|
|
114
|
+
// run's own declared profile, which is the pairing the criterion asks about;
|
|
115
|
+
// passing another is how leg 3 reads engineer prose as a newcomer would.
|
|
116
|
+
function replay(corpus, runName, { profile, variant } = {}) {
|
|
117
|
+
const run = corpus.runs[runName];
|
|
118
|
+
if (!run) throw new Error(`no such run in the corpus: ${runName}`);
|
|
119
|
+
const transform = variant ? TRANSFORMS[variant] : undefined;
|
|
120
|
+
if (variant && !transform) throw new Error(`no such variant: ${variant}`);
|
|
121
|
+
const effective = profile || run.profile;
|
|
122
|
+
const result = scoreAccessibility(asDetail(corpus, run.replies, { transform }), { profile: effective });
|
|
123
|
+
return { run: runName, variant: variant || 'full', profile: effective, result };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// The partial engineer profile — thinks in code, new to the toolchain. Named
|
|
127
|
+
// here because it is the pairing that shows the two waivers are independent.
|
|
128
|
+
const ENGINEER_NEW_TO_TOOLCHAIN = Object.freeze({ technical_level: 'engineer', toolchain_familiarity: 'new' });
|
|
129
|
+
|
|
130
|
+
function rows(corpus) {
|
|
131
|
+
const newcomer = corpus.runs.non_technical.profile;
|
|
132
|
+
return [
|
|
133
|
+
['non-technical prose, non-technical profile', replay(corpus, 'non_technical')],
|
|
134
|
+
['engineer prose, engineer profile', replay(corpus, 'engineer')],
|
|
135
|
+
['engineer prose, NON-TECHNICAL profile', replay(corpus, 'engineer', { profile: newcomer })],
|
|
136
|
+
['engineer prose, engineer but new to the terminal', replay(corpus, 'engineer', { profile: ENGINEER_NEW_TO_TOOLCHAIN })],
|
|
137
|
+
['non-technical prose, said twice', replay(corpus, 'non_technical', { variant: 'doubled' })],
|
|
138
|
+
['non-technical prose, first sentences only', replay(corpus, 'non_technical', { variant: 'terse' })],
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function fmt(n) {
|
|
143
|
+
return n == null ? '—' : n.toFixed(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function formatReport(corpus) {
|
|
147
|
+
const lines = [`accessibility replay — ${corpus.prompts.length} fixed beginner prompts, floor ${FLOOR.toFixed(1)}`, ''];
|
|
148
|
+
for (const [label, r] of rows(corpus)) {
|
|
149
|
+
const { score, band, sampled_chars: chars, sampled_turns: turns } = r.result;
|
|
150
|
+
const waived = r.result.signals.filter((s) => s.waived).length;
|
|
151
|
+
lines.push(
|
|
152
|
+
` ${label.padEnd(48)} ${fmt(score).padStart(5)} ${band.padEnd(9)} ` +
|
|
153
|
+
`${String(turns).padStart(3)} turns ${String(chars).padStart(5)} chars ${waived}/3 waived`
|
|
154
|
+
);
|
|
155
|
+
for (const s of r.result.signals) {
|
|
156
|
+
if (s.waived) continue;
|
|
157
|
+
if (!s.count) continue;
|
|
158
|
+
lines.push(
|
|
159
|
+
` ${s.type.padEnd(28)} ${String(s.count).padStart(2)} hits rate ${s.rate.toFixed(2)}/1k −${s.penalty.toFixed(2)}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return lines.join('\n');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// main — the CLI. `write` is injectable so the test can exercise both output
|
|
167
|
+
// paths without printing a screenful of JSON into the test log.
|
|
168
|
+
function main(argv = process.argv.slice(2), write = (s) => process.stdout.write(s)) {
|
|
169
|
+
const corpus = loadCorpus();
|
|
170
|
+
if (argv.includes('--json')) {
|
|
171
|
+
const out = { floor: FLOOR };
|
|
172
|
+
for (const [label, r] of rows(corpus)) out[label] = r.result;
|
|
173
|
+
write(`${JSON.stringify(out, null, 2)}\n`);
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
write(`${formatReport(corpus)}\n`);
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
FLOOR,
|
|
182
|
+
CORPUS_PATH,
|
|
183
|
+
ENGINEER_NEW_TO_TOOLCHAIN,
|
|
184
|
+
loadCorpus,
|
|
185
|
+
terseVariant,
|
|
186
|
+
doubleRun,
|
|
187
|
+
asDetail,
|
|
188
|
+
replay,
|
|
189
|
+
rows,
|
|
190
|
+
formatReport,
|
|
191
|
+
bandFor,
|
|
192
|
+
main,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
if (require.main === module) process.exit(main());
|
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.791'; // 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');
|