@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
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.891",
|
|
6
|
+
"core_contract": "1.19.891",
|
|
7
|
+
"source_commit": "e306e6ce6ad80a4e9259cea5c81326a60ff5e7ab",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-19T21:
|
|
9
|
+
"built_at": "2026-09-19T21:50:27.729Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 497,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2272,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2795,
|
|
20
|
+
"tree_sha256": "7d4974b5899a8d6ee86520b56479fb42f5256e4596ff21d29df8fd6925518fec",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2607,7 +2607,7 @@
|
|
|
2607
2607
|
{
|
|
2608
2608
|
"path": "docs/module-api-changelog.md",
|
|
2609
2609
|
"mode": "0000644",
|
|
2610
|
-
"sha256": "
|
|
2610
|
+
"sha256": "d6bb312d40e0a4532550ea1faa19b5ae80e09a4dab01035513f8967aecb88f2e"
|
|
2611
2611
|
},
|
|
2612
2612
|
{
|
|
2613
2613
|
"path": "docs/modules-contract.md",
|
|
@@ -4442,12 +4442,12 @@
|
|
|
4442
4442
|
{
|
|
4443
4443
|
"path": "modules/grading/grader-rubric.json",
|
|
4444
4444
|
"mode": "0000644",
|
|
4445
|
-
"sha256": "
|
|
4445
|
+
"sha256": "de0ca17fedb33bd54d73759e14e6813cd9f2c8c85ef2180e49c54794c7a63a3d"
|
|
4446
4446
|
},
|
|
4447
4447
|
{
|
|
4448
4448
|
"path": "modules/grading/grader-score.js",
|
|
4449
4449
|
"mode": "0000644",
|
|
4450
|
-
"sha256": "
|
|
4450
|
+
"sha256": "e0f665b760e2b299a7aa9098e1ab967aa330ff20f7430e0aa23d9c7c57aa0d70"
|
|
4451
4451
|
},
|
|
4452
4452
|
{
|
|
4453
4453
|
"path": "modules/grading/grader-subagent.js",
|
|
@@ -4462,7 +4462,7 @@
|
|
|
4462
4462
|
{
|
|
4463
4463
|
"path": "modules/grading/grader-workers/extract-json.js",
|
|
4464
4464
|
"mode": "0000644",
|
|
4465
|
-
"sha256": "
|
|
4465
|
+
"sha256": "b359c904c68ae8b36ac6518c79bcebf251abcf7aaef3dae4aff4a6e2175e7914"
|
|
4466
4466
|
},
|
|
4467
4467
|
{
|
|
4468
4468
|
"path": "modules/grading/grader-workers/fence.js",
|
|
@@ -4477,7 +4477,7 @@
|
|
|
4477
4477
|
{
|
|
4478
4478
|
"path": "modules/grading/grader-workers/index.js",
|
|
4479
4479
|
"mode": "0000644",
|
|
4480
|
-
"sha256": "
|
|
4480
|
+
"sha256": "ec8046b24550ef81c26b8a421c684360873f9eaed0ec3a7e95b8546aa1e45d6f"
|
|
4481
4481
|
},
|
|
4482
4482
|
{
|
|
4483
4483
|
"path": "modules/grading/grader-workers/narc.js",
|
|
@@ -4489,6 +4489,11 @@
|
|
|
4489
4489
|
"mode": "0000644",
|
|
4490
4490
|
"sha256": "1ad616532dc2c240bf94992db17b12f1334e3e74667c402d997c03c2c8df1566"
|
|
4491
4491
|
},
|
|
4492
|
+
{
|
|
4493
|
+
"path": "modules/grading/grader-workers/reader.js",
|
|
4494
|
+
"mode": "0000644",
|
|
4495
|
+
"sha256": "b7fdf28a76914c708b628a20ee90278ed29ad1e4c886b4e4f0edb2a2515d4d2c"
|
|
4496
|
+
},
|
|
4492
4497
|
{
|
|
4493
4498
|
"path": "modules/grading/grader-workers/rubric.js",
|
|
4494
4499
|
"mode": "0000644",
|
|
@@ -4507,7 +4512,7 @@
|
|
|
4507
4512
|
{
|
|
4508
4513
|
"path": "modules/grading/grader.js",
|
|
4509
4514
|
"mode": "0000644",
|
|
4510
|
-
"sha256": "
|
|
4515
|
+
"sha256": "1234bb3c9938fd4cf71c39e4660d69c56a27de667ee7ff09b0f865374b6beba4"
|
|
4511
4516
|
},
|
|
4512
4517
|
{
|
|
4513
4518
|
"path": "modules/grading/module.json",
|
|
@@ -5367,7 +5372,7 @@
|
|
|
5367
5372
|
{
|
|
5368
5373
|
"path": "modules/lifecycle/cascade.js",
|
|
5369
5374
|
"mode": "0000644",
|
|
5370
|
-
"sha256": "
|
|
5375
|
+
"sha256": "b168a58926ee0506d3fba4f448c797b8611bc8ae0dfeca322086f22769e7ae35"
|
|
5371
5376
|
},
|
|
5372
5377
|
{
|
|
5373
5378
|
"path": "modules/lifecycle/check-rollup.js",
|
|
@@ -5797,7 +5802,7 @@
|
|
|
5797
5802
|
{
|
|
5798
5803
|
"path": "modules/lifecycle/tests/cascade.mjs",
|
|
5799
5804
|
"mode": "0000644",
|
|
5800
|
-
"sha256": "
|
|
5805
|
+
"sha256": "9439accf7648ef22000c75f3e315695c806969eb5a908275d7db33e5491499f8"
|
|
5801
5806
|
},
|
|
5802
5807
|
{
|
|
5803
5808
|
"path": "modules/lifecycle/version-autoclose.js",
|
|
@@ -7702,12 +7707,12 @@
|
|
|
7702
7707
|
{
|
|
7703
7708
|
"path": "package-lock.json",
|
|
7704
7709
|
"mode": "0000644",
|
|
7705
|
-
"sha256": "
|
|
7710
|
+
"sha256": "17d7b0573d81b759182725bbb1be90a3bf1da889cdef0ef97510e3bb0e263932"
|
|
7706
7711
|
},
|
|
7707
7712
|
{
|
|
7708
7713
|
"path": "package.json",
|
|
7709
7714
|
"mode": "0000644",
|
|
7710
|
-
"sha256": "
|
|
7715
|
+
"sha256": "90befa8ce40948a6ce667f2690343e2489b3ea33c08b4d6d1e2daee5227e9c9a"
|
|
7711
7716
|
},
|
|
7712
7717
|
{
|
|
7713
7718
|
"path": "public-docs/index.html",
|
|
@@ -8012,7 +8017,7 @@
|
|
|
8012
8017
|
{
|
|
8013
8018
|
"path": "scripts/gds/build-cli-package.js",
|
|
8014
8019
|
"mode": "0000644",
|
|
8015
|
-
"sha256": "
|
|
8020
|
+
"sha256": "ca0566c509fb1ea81e7c874e2676bbcb965803509c0289c577bc8f15791e6f5c"
|
|
8016
8021
|
},
|
|
8017
8022
|
{
|
|
8018
8023
|
"path": "scripts/gds/bump-version.js",
|
|
@@ -9062,7 +9067,7 @@
|
|
|
9062
9067
|
{
|
|
9063
9068
|
"path": "scripts/gds/ship-grade.js",
|
|
9064
9069
|
"mode": "0000644",
|
|
9065
|
-
"sha256": "
|
|
9070
|
+
"sha256": "b1fb38635433b579fbb38d65e92b9db226cccb753866ab7a2a15227796be66cd"
|
|
9066
9071
|
},
|
|
9067
9072
|
{
|
|
9068
9073
|
"path": "scripts/gds/ship-honesty.js",
|
|
@@ -9202,7 +9207,7 @@
|
|
|
9202
9207
|
{
|
|
9203
9208
|
"path": "scripts/gds/status.js",
|
|
9204
9209
|
"mode": "0000644",
|
|
9205
|
-
"sha256": "
|
|
9210
|
+
"sha256": "191b49e9ccbcfc7b855d834535ad5e9de0970d6e91126c7a0591e7790d196a89"
|
|
9206
9211
|
},
|
|
9207
9212
|
{
|
|
9208
9213
|
"path": "scripts/gds/strand-watch.js",
|
|
@@ -9277,7 +9282,7 @@
|
|
|
9277
9282
|
{
|
|
9278
9283
|
"path": "scripts/gds/version-literal-guard.js",
|
|
9279
9284
|
"mode": "0000644",
|
|
9280
|
-
"sha256": "
|
|
9285
|
+
"sha256": "2c0e605876762f8c6607af49374f56bc0717a1aad08c3ffdbbae383ffaf1d9dd"
|
|
9281
9286
|
},
|
|
9282
9287
|
{
|
|
9283
9288
|
"path": "scripts/gds/version-select.js",
|
|
@@ -9712,7 +9717,7 @@
|
|
|
9712
9717
|
{
|
|
9713
9718
|
"path": "src/module-api.js",
|
|
9714
9719
|
"mode": "0000644",
|
|
9715
|
-
"sha256": "
|
|
9720
|
+
"sha256": "4dd8bdfcb69d7c1f7a1443175bea5379ed60daec5d19ec846a14732db59559fc"
|
|
9716
9721
|
},
|
|
9717
9722
|
{
|
|
9718
9723
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10962,7 +10967,7 @@
|
|
|
10962
10967
|
{
|
|
10963
10968
|
"path": "tests/extract_json.mjs",
|
|
10964
10969
|
"mode": "0000644",
|
|
10965
|
-
"sha256": "
|
|
10970
|
+
"sha256": "74f8d8852b8949dc55ac38dd6f31da85bb55aae1215a146213495db898515086"
|
|
10966
10971
|
},
|
|
10967
10972
|
{
|
|
10968
10973
|
"path": "tests/figma_adapter.mjs",
|
|
@@ -11504,6 +11509,11 @@
|
|
|
11504
11509
|
"mode": "0000644",
|
|
11505
11510
|
"sha256": "cbe81b9176d6ffdd59ea8d6955e30aab9bf6d13bd15144fd79d126455aa0e1e3"
|
|
11506
11511
|
},
|
|
11512
|
+
{
|
|
11513
|
+
"path": "tests/grader_reader_lens.mjs",
|
|
11514
|
+
"mode": "0000644",
|
|
11515
|
+
"sha256": "93e24012252c08798e3017ce9a7130ce9ece4e5e07456892b3632ab12f44ad47"
|
|
11516
|
+
},
|
|
11507
11517
|
{
|
|
11508
11518
|
"path": "tests/grader_regrade_delta.mjs",
|
|
11509
11519
|
"mode": "0000644",
|
|
@@ -13579,6 +13589,11 @@
|
|
|
13579
13589
|
"mode": "0000644",
|
|
13580
13590
|
"sha256": "4b92c96ae496736b0f44182e64d42a24f108cb51fbeb5cf7cd5814ad336b14f6"
|
|
13581
13591
|
},
|
|
13592
|
+
{
|
|
13593
|
+
"path": "tests/status_default_version.mjs",
|
|
13594
|
+
"mode": "0000644",
|
|
13595
|
+
"sha256": "9a80ed43b685ea537944886a1a4a02d35580c109bd777373e30b63afd9aa73df"
|
|
13596
|
+
},
|
|
13582
13597
|
{
|
|
13583
13598
|
"path": "tests/status_mirror_sync.mjs",
|
|
13584
13599
|
"mode": "0000644",
|
|
@@ -13867,7 +13882,7 @@
|
|
|
13867
13882
|
{
|
|
13868
13883
|
"path": "tests/version_literals.mjs",
|
|
13869
13884
|
"mode": "0000644",
|
|
13870
|
-
"sha256": "
|
|
13885
|
+
"sha256": "3b706518920bc48e1bcd5a0185a5829a179fa0e73a6ed491320327d2cb7c8c68"
|
|
13871
13886
|
},
|
|
13872
13887
|
{
|
|
13873
13888
|
"path": "tests/version_override_visibility.mjs",
|
|
@@ -2252,5 +2252,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2252
2252
|
landed since 1.19.887 with no explicit bump. run 35468948342. (task 1002620)
|
|
2253
2253
|
1.19.889 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2254
2254
|
landed since 1.19.888 with no explicit bump. run 35470239834. (task 1002620)
|
|
2255
|
+
1.19.890 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2256
|
+
landed since 1.19.889 with no explicit bump. run 35470624284. (task 1002620)
|
|
2257
|
+
1.19.891 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2258
|
+
landed since 1.19.890 with no explicit bump. run 35471575521. (task 1002620)
|
|
2255
2259
|
---------------------------------------------------------------------------
|
|
2256
2260
|
```
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
"assumed_builder_model": "claude-fable-5",
|
|
13
13
|
"model_refresh_note": "task 1002659 (2026-08-11): pins refreshed to the Claude 5 family. Assumed builder = claude-fable-5 (the owner-top-model default per CLAUDE.md §2); default grader = claude-sonnet-5 (cross-model per ADR 0024, cost-parity with the prior sonnet-4-6 posture — opus-5 grades the sonnet/haiku builders where the grader should be the stronger model). selectGraderModel no longer throws on an unknown builder id: it warns loudly and falls back to default_model (the #117 throw stranded ships whose builder id lacked an opus/sonnet/haiku substring — exactly what claude-fable-5 is).",
|
|
14
14
|
"active_panel": ["narc", "quality", "hacker", "efficiency"],
|
|
15
|
-
"worker_model_tiers": { "quality": "claude-sonnet-5" },
|
|
16
|
-
"worker_model_tiers_note": "task 1002660: per-worker cost tiering. Quality was 41% of panel spend with 75% nit findings — its cq/economy lens ran on the haiku tier. task 1003318: moved to claude-sonnet-5 — haiku 4.5's 200k window overflows on any diff over ~150 KB (`Prompt is too long`, deterministic; tasks 1003297 and 1003298 each lost three ~$5 panel runs to it) and a Quality worker that never starts is missing signal on a GATING lens. For fable/opus builders the panel model is already claude-sonnet-5, so the tier coincides with default_model and acts as a floor — a future default_model bump must not pin Quality back onto a smaller window; it stays a live cost lever only for haiku builders (whose panel runs on opus-5). workerModelFor refuses an override that lands on the builder's own model family (the ADR 0024 cross-model guarantee outranks the cost lever); gate semantics unchanged — Quality's verdict still gates identically.",
|
|
15
|
+
"worker_model_tiers": { "quality": "claude-sonnet-5", "reader": "claude-sonnet-5" },
|
|
16
|
+
"worker_model_tiers_note": "task 1002660: per-worker cost tiering. Quality was 41% of panel spend with 75% nit findings — its cq/economy lens ran on the haiku tier. task 1003318: moved to claude-sonnet-5 — haiku 4.5's 200k window overflows on any diff over ~150 KB (`Prompt is too long`, deterministic; tasks 1003297 and 1003298 each lost three ~$5 panel runs to it) and a Quality worker that never starts is missing signal on a GATING lens. For fable/opus builders the panel model is already claude-sonnet-5, so the tier coincides with default_model and acts as a floor — a future default_model bump must not pin Quality back onto a smaller window; it stays a live cost lever only for haiku builders (whose panel runs on opus-5). workerModelFor refuses an override that lands on the builder's own model family (the ADR 0024 cross-model guarantee outranks the cost lever); gate semantics unchanged — Quality's verdict still gates identically. task 1003977: the READER is pinned to claude-sonnet-5 for the same task-1003318 reason — it is Read-heavy by construction (the voice guide, then the render paths that decide whether a string is user-facing), and a small window fails deterministically with PROMPT_TOO_LONG. For a NON-GATING worker that means silently reading no copy at all on exactly the biggest copy diffs, with nothing blocking to reveal it.",
|
|
17
17
|
"fallthrough_confidence": 0.3,
|
|
18
18
|
"low_confidence_threshold": 0.4,
|
|
19
19
|
"rationale": "GDS-V3 task #410 (MAS Phase 2.0), 2026-05-29. Supersedes v3 (single-prompt 3-shot generalist). Phase 1 calibration showed minimal between-shot disagreement on the same prompt — the strict gate fired correctly but the diversity signal was weak. v4 trades sampling-variance for lens-specialization: replace 3 identical-prompt shots with N workers (each a distinct prompt focused on one axis). Phase 2.0 ships 2 of the 3 planned workers (narc for completion, quality for cq + advisory economy) — keeps the gate at the same strictness while the framework calibrates. Phase 2.1 adds hacker (security; wraps /builder-redteam as a CI gate). Phase 2.2 adds efficiency_monger.",
|
|
20
20
|
"worker_contract": {
|
|
21
|
-
"kind": "string — worker registry key (narc | quality | hacker | efficiency)",
|
|
21
|
+
"kind": "string — worker registry key (narc | quality | hacker | efficiency | reader)",
|
|
22
22
|
"name": "string — display name",
|
|
23
23
|
"specialty": "string — one-line description of what this worker judges",
|
|
24
|
-
"verdict": "'pass' | 'concerns' | 'fail' — per-worker verdict, sub-classification of the panel gate",
|
|
24
|
+
"verdict": "'pass' | 'concerns' | 'fail' — per-worker verdict, sub-classification of the panel gate; plus 'advisory', the ONLY verdict a NON-GATING worker emits (task 1003977 — see active_workers.reader and panel_gate.non_gating). Mirrored by WORKER_VERDICTS in grader-score.js, which hasPanelEvidence checks with an `every`.",
|
|
25
25
|
"scores": "{ functional_fidelity?, code_quality?, economy? } — only the axes this worker reports",
|
|
26
26
|
"issues": "[{ severity, file, summary }] — concrete problems found",
|
|
27
27
|
"notes_md": "short markdown summary",
|
|
@@ -31,12 +31,22 @@
|
|
|
31
31
|
"error": "null | 'parse_error' | 'schema_error' | 'SUBAGENT_*' — set when the subagent comms failed; verdict will be 'fail'"
|
|
32
32
|
},
|
|
33
33
|
"panel_gate": {
|
|
34
|
-
"fail": "ANY worker.verdict='fail' OR worker.error != null → overall passed=false, no advance to confirmed.",
|
|
35
|
-
"concerns": "ANY worker.verdict='concerns' (and no fails) → overall passed=false, forwarded confidence = fallthrough_confidence (manual-confirm available).",
|
|
36
|
-
"pass": "ALL workers.verdict='pass' → overall passed=true, advance to confirmed."
|
|
34
|
+
"fail": "ANY GATING worker.verdict='fail' OR worker.error != null → overall passed=false, no advance to confirmed.",
|
|
35
|
+
"concerns": "ANY GATING worker.verdict='concerns' (and no fails) → overall passed=false, forwarded confidence = fallthrough_confidence (manual-confirm available).",
|
|
36
|
+
"pass": "ALL GATING workers.verdict='pass' → overall passed=true, advance to confirmed.",
|
|
37
|
+
"non_gating": "task 1003977: a worker in NON_GATING_WORKERS (today: reader) is partitioned out of workerResults BEFORE any gate arithmetic — the pass/concerns/fail counts, the missing-signal and total-outage detection, the forwarded-confidence median and every verdict branch read the gating workers only. Its scores and issues still land in rubric_scores and signals.workers. Consequence worth stating out loud: a HEALTHY non-gating worker cannot mask a total grader outage by making 'every gating worker is missing signal' false — that is the task-1003574 regression class, where an outage presented as FAIL 0/10 and routed the builder to a re-grade that could not clear it."
|
|
37
38
|
},
|
|
38
39
|
"severity_gate_policy": "Task 1002663: style findings (comment style/length/placement, naming conventions, doc placement, test-name conventions) are ADVISORY — severity nit, recorded in issues[] for the audit trail, never a verdict driver. Enforced deterministically in grader-workers/worker-verdict.js applySeverityGatePolicy for narc/quality/efficiency: a worker 'fail' whose only basis is a sub-threshold score with NO major/blocker finding is an inconsistent self-report and downgrades to 'concerns' (manual-confirm pause) — a worker whose findings are all nit/minor can never verdict fail. The HACKER is deliberately EXEMPT: the security lens stays fail-closed on an unbacked sub-threshold score (a real finding mislabeled minor must still hard-fail). Untouched everywhere: blocker-driven fails, non-score fail triggers (Narc missing-AC), error short-circuits. Every worker prompt carries the matching STYLE_ADVISORY paragraph (worker-prompt.js).",
|
|
39
40
|
"active_workers": {
|
|
41
|
+
"reader": {
|
|
42
|
+
"specialty": "user-facing copy: truth, clarity, voice, judged against docs/voice.md",
|
|
43
|
+
"scores_reported": ["copy_score"],
|
|
44
|
+
"gating": false,
|
|
45
|
+
"verdict_rules": "NON-GATING (task 1003977, ADR 0298). Emits the single verdict 'advisory' on every path INCLUDING its own errors — computeVerdict has no branch returning pass/concerns/fail, so 'cannot block' is a property of the function, not a promise kept by callers. aggregatePanel additionally partitions it out of the gate (NON_GATING_WORKERS in grader-workers/index.js). Belt and braces: a filter is one refactor from being forgotten, a verdict is not. Stronger than the advisory treatment narc receives, which only downgrades a fail to manual-confirm — the reader cannot reach 'concerns' either. A 'blocker' severity is FLOORED to 'major' rather than dropped: the observation survives, the veto does not. Deliberately NO blocker escape hatch of the kind narc/hacker carry (grader.js isAdvisory, '... && !hasBlocker'), because that hatch is the exact hole the owner's term closes. An errored reader records NO copy_score rather than 0 — a 0 is a real reading, an outage is missing signal (the #943 distinction, per worker).",
|
|
46
|
+
"scope": "copy-bearing diffs ONLY. The caller (scripts/gds/ship-grade.js) asks hasCopyOrVisualWork(task) from modules/lifecycle/cascade.js and passes the answer to WORKERS.panelFor({ copyBearing }); the grading module may not require a sibling module (ADR 0083, CI-enforced), and a second predicate inside grading would let the at-ship lens and the after-the-fact artist review disagree about what they cover. Where false the panel is byte-identical to DEFAULT_PANEL and costs what it costs today, and the skip is recorded as signals.reader_skipped='not_copy_bearing' — never a silent absence, because a missing copy score must not read as a passing one.",
|
|
47
|
+
"standard": "docs/voice.md (task 1003976), cited by rule id. Hard rules V1-V5 are checkable and may be ASSERTED as breaks; judgement rules V6-V11 may only be FLAGGED with the line quoted. A finding that cites no rule id is not a finding — the guide's own rule, carried into the prompt, and what keeps an advisory lens from recording taste as fact.",
|
|
48
|
+
"prompt_lives_in": "modules/grading/grader-workers/reader.js"
|
|
49
|
+
},
|
|
40
50
|
"narc": {
|
|
41
51
|
"specialty": "semantic completion (description vs diff, line-level)",
|
|
42
52
|
"scores_reported": ["functional_fidelity"],
|
|
@@ -55,7 +55,13 @@ function isSubstantiveIssue(it) {
|
|
|
55
55
|
// grade-time input (the ADR's documented honor-system residual for Metic+);
|
|
56
56
|
// its job is separating "the panel ran and unanimously found nothing" (the
|
|
57
57
|
// guard's confirmed false-positive class) from "no evidence at all".
|
|
58
|
-
|
|
58
|
+
// 'advisory' (task 1003977) is the Reader's only verdict — a non-gating lens
|
|
59
|
+
// whose row must still COUNT AS EVIDENCE that the panel ran. Omit it here and
|
|
60
|
+
// hasPanelEvidence returns false for every copy-bearing ship, because the check
|
|
61
|
+
// below is an `every`: one unrecognised verdict makes a fully-healthy five-worker
|
|
62
|
+
// panel look like no evidence at all, and the implausible-pass guard then fires
|
|
63
|
+
// on exactly the ships that carry the most lenses.
|
|
64
|
+
const WORKER_VERDICTS = new Set(['pass', 'concerns', 'fail', 'advisory']);
|
|
59
65
|
function hasPanelEvidence(signals) {
|
|
60
66
|
const ws = Array.isArray(signals?.workers) ? signals.workers : null;
|
|
61
67
|
if (!ws || ws.length < 3) return false;
|
|
@@ -17,7 +17,13 @@
|
|
|
17
17
|
|
|
18
18
|
// Score keys that mark a parsed object as the real grade (vs an incidental
|
|
19
19
|
// prose object). Module-level so extractJson doesn't re-allocate per call.
|
|
20
|
-
|
|
20
|
+
// task 1003977: 'copy_score' joins the list when the Reader joins the panel.
|
|
21
|
+
// reader.js re-exports THIS extractJson, so without its key the disambiguation
|
|
22
|
+
// above silently does not apply to it — an ambiguous multi-object response would
|
|
23
|
+
// fall back to candidates[0] with no score-based preference. It fails safe (a
|
|
24
|
+
// non-gating worker with no signal blocks nothing), but failing safe is not the
|
|
25
|
+
// same as getting the protection every other worker gets.
|
|
26
|
+
const GRADE_SHAPE_KEYS = ['functional_fidelity', 'code_quality', 'security_score', 'efficiency_score', 'copy_score'];
|
|
21
27
|
|
|
22
28
|
function extractJson(raw) {
|
|
23
29
|
if (typeof raw !== 'string') return null;
|
|
@@ -18,18 +18,52 @@ const narc = require('./narc');
|
|
|
18
18
|
const quality = require('./quality');
|
|
19
19
|
const hacker = require('./hacker');
|
|
20
20
|
const efficiency = require('./efficiency');
|
|
21
|
+
const reader = require('./reader');
|
|
21
22
|
|
|
22
23
|
const REGISTRY = {
|
|
23
24
|
[narc.KIND]: narc,
|
|
24
25
|
[quality.KIND]: quality,
|
|
25
26
|
[hacker.KIND]: hacker,
|
|
26
27
|
[efficiency.KIND]: efficiency,
|
|
28
|
+
[reader.KIND]: reader,
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
// The active panel for the ship-time gate. Order is presentation-only —
|
|
30
32
|
// workers run in parallel inside gradeSubagent.
|
|
31
33
|
const DEFAULT_PANEL = [narc.KIND, quality.KIND, hacker.KIND, efficiency.KIND];
|
|
32
34
|
|
|
35
|
+
// Workers that are NEVER part of the gate — not a hard fail, not a downgrade to
|
|
36
|
+
// concerns, not a missing-signal outage. Distinct from grader.js's
|
|
37
|
+
// ADVISORY_WORKERS ('narc'), which only softens a fail into manual-confirm.
|
|
38
|
+
//
|
|
39
|
+
// The Reader is here because the owner's term for it (2026-09-17, ADR 0298) is
|
|
40
|
+
// absolute: ADR 0241 put the artist's judgement on the DEPLOY and off the ship
|
|
41
|
+
// path, and a copy lens that could hold a ship puts back what ADR 0162 retired.
|
|
42
|
+
// Deliberately NO blocker escape hatch of the kind narc/hacker carry — see the
|
|
43
|
+
// header of reader.js for why that hatch would be the hole the term closes.
|
|
44
|
+
//
|
|
45
|
+
// This set is belt; reader.js's computeVerdict, which cannot return anything but
|
|
46
|
+
// 'advisory', is braces. Either alone would hold; both, because a filter is one
|
|
47
|
+
// refactor from being forgotten and a verdict is not.
|
|
48
|
+
const NON_GATING_WORKERS = new Set([reader.KIND]);
|
|
49
|
+
function isNonGatingKind(kind) { return NON_GATING_WORKERS.has(kind); }
|
|
50
|
+
|
|
51
|
+
// The panel for one ship. `copyBearing` is the CALLER's answer, never computed
|
|
52
|
+
// here: the one definition is hasCopyOrVisualWork() in modules/lifecycle/
|
|
53
|
+
// cascade.js, and a module may require only src/module-api.js (ADR 0083), so
|
|
54
|
+
// grading cannot reach it. Writing a second predicate here is exactly what the
|
|
55
|
+
// task forbids — the at-ship lens and the after-the-fact artist review must
|
|
56
|
+
// never disagree about what they cover, and widening that set must widen both
|
|
57
|
+
// at once. So the caller (scripts/gds/ship-grade.js, which is not a module and
|
|
58
|
+
// may require both) asks lifecycle and passes the answer down.
|
|
59
|
+
//
|
|
60
|
+
// Where it is false the panel is BYTE-IDENTICAL to today's and costs what it
|
|
61
|
+
// costs today. The skip is recorded by the caller as a visible signal, never as
|
|
62
|
+
// a silent absence — a missing copy score must not read as a passing one.
|
|
63
|
+
function panelFor({ copyBearing = false } = {}) {
|
|
64
|
+
return copyBearing ? [...DEFAULT_PANEL, reader.KIND] : [...DEFAULT_PANEL];
|
|
65
|
+
}
|
|
66
|
+
|
|
33
67
|
// #465: transient subprocess/parse errors are infra noise, not a quality
|
|
34
68
|
// signal — auto-retry the worker ONCE on these before letting the error stand.
|
|
35
69
|
// Deliberately EXCLUDES:
|
|
@@ -79,6 +113,11 @@ const WORKER_MAX_TIMEOUT_MS = {
|
|
|
79
113
|
quality: 360_000, // Read-heavy: reads changed files for cleanliness review
|
|
80
114
|
hacker: 360_000, // Read-heavy on a security diff: exploit-surface scan of every changed file
|
|
81
115
|
efficiency: 360_000, // Read-heavy on a large diff: traces perf across every changed file
|
|
116
|
+
// Read-heavy by construction: the Reader's first Read is the voice guide, and
|
|
117
|
+
// the rest confirm that a string really is user-facing. Same heavy tier as the
|
|
118
|
+
// rest — worst-case panel wall time is max(), and the four already sit at 360s,
|
|
119
|
+
// so adding a fifth at the same budget does not move it (task 1003977).
|
|
120
|
+
reader: 360_000,
|
|
82
121
|
};
|
|
83
122
|
|
|
84
123
|
// task 1003313: the diagnostic a dark panel used to throw away.
|
|
@@ -244,6 +283,9 @@ async function runWorker(worker, { task, valueSummary, diff, changedFiles, diffS
|
|
|
244
283
|
module.exports = {
|
|
245
284
|
REGISTRY,
|
|
246
285
|
DEFAULT_PANEL,
|
|
286
|
+
NON_GATING_WORKERS,
|
|
287
|
+
isNonGatingKind,
|
|
288
|
+
panelFor,
|
|
247
289
|
resolvePanel,
|
|
248
290
|
runWorker,
|
|
249
291
|
timeoutForWorker, // public surface; WORKER_MAX_TIMEOUT_MS stays module-internal
|
|
@@ -255,4 +297,5 @@ module.exports = {
|
|
|
255
297
|
quality,
|
|
256
298
|
hacker,
|
|
257
299
|
efficiency,
|
|
300
|
+
reader,
|
|
258
301
|
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// The Reader — the advisory copy lens (task 1003977, ADR 0298).
|
|
2
|
+
//
|
|
3
|
+
// Specialty: the SENTENCES. Is a line a person reads true, clear, in the
|
|
4
|
+
// project's voice, and worth their attention? The other four workers read
|
|
5
|
+
// completion (Narc), code quality (Quality), security (Hacker) and runtime cost
|
|
6
|
+
// (Efficiency); not one of them reads a sentence. Task 1003937 established that
|
|
7
|
+
// across four consecutive artist reviews, all of which found the same wall and
|
|
8
|
+
// all of whose graded ships deservedly passed on the axes that WERE measured.
|
|
9
|
+
//
|
|
10
|
+
// ADVISORY, FULL STOP — and that is structural here, not a filter elsewhere.
|
|
11
|
+
// This worker's computeVerdict cannot return 'fail' or 'concerns'; it returns
|
|
12
|
+
// the single verdict 'advisory' on every path, including its own errors. The
|
|
13
|
+
// panel then excludes it from the gate (NON_GATING_WORKERS in index.js, and the
|
|
14
|
+
// partition in grader.js aggregatePanel). Two independent mechanisms, because
|
|
15
|
+
// the owner's term (2026-09-17) is absolute: ADR 0241 put the artist's judgement
|
|
16
|
+
// on the DEPLOY and off the ship path, after ADR 0162 retired review gates for
|
|
17
|
+
// stranding graded-PASS work. A reader that could fail a ship puts that back.
|
|
18
|
+
//
|
|
19
|
+
// NOTE THIS IS STRONGER THAN ADVISORY_WORKERS. The Narc is "advisory" in the
|
|
20
|
+
// sense that a Narc-only fail DOWNGRADES to concerns (manual-confirm). The
|
|
21
|
+
// Reader cannot reach concerns either. There is deliberately NO blocker escape
|
|
22
|
+
// hatch of the kind narc/hacker carry ("advisory ONLY if it carries no
|
|
23
|
+
// blocker"): a copy lens finding something that genuinely blocks a ship is a
|
|
24
|
+
// finding in another lens's territory, and the escape hatch would be the exact
|
|
25
|
+
// hole the owner's term closes.
|
|
26
|
+
//
|
|
27
|
+
// THE STANDARD IS WRITTEN DOWN. docs/voice.md (task 1003976) is the rubric, and
|
|
28
|
+
// its own rule is that a finding citing no rule id is not a finding. The guide
|
|
29
|
+
// splits HARD rules (V1-V5, checkable — a lens may assert a break) from
|
|
30
|
+
// JUDGEMENT rules (V6-V11, recognisable but not provable — a lens may only FLAG
|
|
31
|
+
// a candidate and must quote the line). That split is reproduced in the prompt
|
|
32
|
+
// below, because it is what keeps an advisory lens from recording taste as fact.
|
|
33
|
+
|
|
34
|
+
const { buildWorkerPrompt, LLM_PROJECT } = require('./worker-prompt');
|
|
35
|
+
const { parseCommon } = require('./worker-verdict');
|
|
36
|
+
const { extractJson } = require('./extract-json');
|
|
37
|
+
|
|
38
|
+
const KIND = 'reader';
|
|
39
|
+
const NAME = 'Reader';
|
|
40
|
+
const SPECIALTY = 'user-facing copy: truth, clarity, voice (docs/voice.md), against the written guide';
|
|
41
|
+
|
|
42
|
+
// The ONE verdict this worker emits. Not 'pass': a pass is a claim the gate may
|
|
43
|
+
// read, and an errored Reader claiming 'pass' would be a false one. Registered
|
|
44
|
+
// in grader-score.js WORKER_VERDICTS so hasPanelEvidence still recognises a
|
|
45
|
+
// panel that carries a Reader row.
|
|
46
|
+
const ADVISORY_VERDICT = 'advisory';
|
|
47
|
+
|
|
48
|
+
const PROMPT_HEADER = `You are the **Reader** — the copy lens for the ${LLM_PROJECT} project. You read the SENTENCES a person will see.
|
|
49
|
+
|
|
50
|
+
You are ADVISORY. Nothing you report can block this ship, and you should not write as though it could. Your output is coaching for the artist who reviews this work afterwards, and a record of where the grade was otherwise blind.
|
|
51
|
+
|
|
52
|
+
Your ONLY job is user-facing copy. You do NOT score whether the description was implemented (Narc), whether the code is well-written (Quality), whether it is secure (Hacker), or what it costs to run (Efficiency). If you notice something in those territories, say so in \`notes_md\` and move on — do not file it as an issue.
|
|
53
|
+
|
|
54
|
+
## The standard is written down, and you must cite it
|
|
55
|
+
|
|
56
|
+
\`docs/voice.md\` is the voice guide. **Read it first** — it is the rubric for this grade, and its rules carry ids (V1, V2, …).
|
|
57
|
+
|
|
58
|
+
**A finding that cites no rule id is not a finding.** If a line is bad and no rule in the guide reaches it, say that in \`notes_md\` as a gap in the guide. Do not invent a rule at grade time. This is the guide's own instruction and it is what separates a lens from a second model's taste.
|
|
59
|
+
|
|
60
|
+
The guide splits its rules in two, and the split governs what you may say:
|
|
61
|
+
|
|
62
|
+
- **Hard rules (V1-V5)** can be checked. A line either breaks one or it does not. You MAY assert a break.
|
|
63
|
+
- **Judgement rules (V6-V11)** name a failure a person can recognise but a checker cannot prove. You may **flag a candidate** and must quote the line and the rule id. You may NEVER assert that the line is wrong. The artist decides.
|
|
64
|
+
|
|
65
|
+
Set \`kind\` on every issue to \`"break"\` (hard rule, asserted) or \`"flag"\` (judgement rule, offered). A judgement-rule issue filed as a break is the single most damaging thing you can do here, because it records taste as fact.
|
|
66
|
+
|
|
67
|
+
## What counts as user-facing copy
|
|
68
|
+
|
|
69
|
+
What the guide governs: every string a person reads on the builders' hall, the status dashboard and the landing surface; server-side route failures and validation text; outbound broadcast text.
|
|
70
|
+
|
|
71
|
+
What it does NOT govern, and you must not score: repository documentation, ADRs, session logs, code comments, commit messages, skill prose. Those are written for builders and agents in ordinary technical prose. A diff that changes ONLY those carries no copy for you to read — say so and score 10.
|
|
72
|
+
|
|
73
|
+
## How you work
|
|
74
|
+
|
|
75
|
+
1. Read \`docs/voice.md\`.
|
|
76
|
+
2. Walk the diff for CHANGED or ADDED user-facing strings. Quote each line you judge.
|
|
77
|
+
3. For each, name the rule id it breaks or that it is a candidate against.
|
|
78
|
+
4. Score \`copy_score\` (0-10):
|
|
79
|
+
- 10 = no user-facing copy changed, or every changed line is clean against the guide.
|
|
80
|
+
- 8 = clean; one or two judgement-rule flags worth the artist's eye.
|
|
81
|
+
- 6 = at least one hard-rule break in a line a person will read.
|
|
82
|
+
- 4 = several hard-rule breaks, or one in a high-traffic surface.
|
|
83
|
+
- 2 = the copy actively misleads (a word that means something else here, a claim the code does not support).
|
|
84
|
+
- 0 = the copy is wrong in a way that would embarrass the project in front of a user.
|
|
85
|
+
|
|
86
|
+
The score is a RECORD, not a gate. Nothing happens to the ship at any value.
|
|
87
|
+
|
|
88
|
+
5. Severities: \`major\` for a hard-rule break on a line a person reads; \`minor\` for a hard-rule break somewhere quiet; \`nit\` for a judgement-rule flag. Never \`blocker\` — you cannot block, and a blocker severity from this lens is a category error.
|
|
89
|
+
|
|
90
|
+
## Read budget
|
|
91
|
+
|
|
92
|
+
**5 Read calls.** Spend the first on \`docs/voice.md\`. Spend the rest confirming that a string you are judging really is user-facing — the surrounding render path tells you, and a string in a code comment is not copy.
|
|
93
|
+
|
|
94
|
+
## Sanity checks
|
|
95
|
+
|
|
96
|
+
- A diff with no user-facing string changes scores 10 with no issues. That is a normal, common outcome. Do not manufacture findings to look useful.
|
|
97
|
+
- Every issue quotes the line and names a rule id.
|
|
98
|
+
- No issue carries severity \`blocker\`.
|
|
99
|
+
- Do not rewrite the copy for them unless the fix is obvious and one line; the artist owns the remake.
|
|
100
|
+
|
|
101
|
+
## Return ONLY JSON
|
|
102
|
+
|
|
103
|
+
No prose, no markdown fences:
|
|
104
|
+
|
|
105
|
+
\`\`\`
|
|
106
|
+
{
|
|
107
|
+
"copy_score": <number 0-10>,
|
|
108
|
+
"confidence": <number 0-1>,
|
|
109
|
+
"reads_used": <integer 0-5>,
|
|
110
|
+
"strings_reviewed": [
|
|
111
|
+
{ "where": "<file:line>", "line": "<the string, quoted>", "verdict": "ok"|"break"|"flag", "rule": "<V-id or null>" }
|
|
112
|
+
],
|
|
113
|
+
"issues": [
|
|
114
|
+
{ "severity": "major"|"minor"|"nit", "kind": "break"|"flag", "rule": "<V-id>", "file": "<path>", "summary": "<one line, quotes the copy and names the rule>" }
|
|
115
|
+
],
|
|
116
|
+
"notes_md": "<short markdown — which surfaces carried copy, what you could not judge, and any gap in the guide you hit>"
|
|
117
|
+
}
|
|
118
|
+
\`\`\`
|
|
119
|
+
|
|
120
|
+
You have a Read tool with a hard budget of 5 calls. Do not run tests, do not edit files, do not call the network.`;
|
|
121
|
+
|
|
122
|
+
function buildPrompt({ task, valueSummary, diff, changedFiles, diffStats, priorRound, contextBundle }) {
|
|
123
|
+
return buildWorkerPrompt({
|
|
124
|
+
priorRound,
|
|
125
|
+
contextBundle,
|
|
126
|
+
header: PROMPT_HEADER,
|
|
127
|
+
task: task || {},
|
|
128
|
+
valueSummary,
|
|
129
|
+
diff,
|
|
130
|
+
changedFiles,
|
|
131
|
+
taskHeader: '## Task (context — you judge the copy, not the completion)',
|
|
132
|
+
descLabel: '**Description:**',
|
|
133
|
+
emptyFilesText: '(no files changed)',
|
|
134
|
+
readFilesHint: 'You can Read any of these files to see their post-merge state (5-call budget — spend the first on docs/voice.md).',
|
|
135
|
+
finalInstruction: 'Now read the copy. Cite a rule id on every finding, or do not file it. Output ONLY the JSON object.',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The Reader's verdict is 'advisory' on EVERY path — a clean read, a bad read,
|
|
140
|
+
// a crashed subagent, a timeout. There is no branch that returns anything else,
|
|
141
|
+
// which is what makes "it cannot block" a property of this function rather than
|
|
142
|
+
// a promise kept by its callers.
|
|
143
|
+
//
|
|
144
|
+
// An error still travels: `error` is set, the notes say so, and copy_score is
|
|
145
|
+
// null rather than 0. A 0 would be a real score meaning "the copy is wrong in a
|
|
146
|
+
// way that would embarrass the project", and an outage is not that. The same
|
|
147
|
+
// distinction #943 draws for the panel — missing signal is not a bad signal.
|
|
148
|
+
function computeVerdict(parsed, meta = {}) {
|
|
149
|
+
const advisory = (extra) => ({
|
|
150
|
+
verdict: ADVISORY_VERDICT,
|
|
151
|
+
scores: {},
|
|
152
|
+
issues: [],
|
|
153
|
+
confidence: null,
|
|
154
|
+
error: null,
|
|
155
|
+
...extra,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (meta.subprocess_error) {
|
|
159
|
+
return advisory({
|
|
160
|
+
notes_md: `Reader — ADVISORY, no signal: subagent error (${meta.subprocess_error}). The copy was not read; this does not affect the ship.`,
|
|
161
|
+
error: meta.subprocess_error,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
165
|
+
return advisory({
|
|
166
|
+
notes_md: 'Reader — ADVISORY, no signal: could not parse subagent output as JSON. The copy was not read; this does not affect the ship.',
|
|
167
|
+
error: 'parse_error',
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const { conf, issues: rawIssues, notesFromSubagent } = parseCommon(parsed);
|
|
172
|
+
const score = Number(parsed.copy_score);
|
|
173
|
+
const reviewed = Array.isArray(parsed.strings_reviewed) ? parsed.strings_reviewed : [];
|
|
174
|
+
|
|
175
|
+
// A blocker from this lens is a category error, not a blocking finding. It is
|
|
176
|
+
// FLOORED to major rather than dropped: the observation may be real and the
|
|
177
|
+
// artist should see it; what it may not be is a severity the gate reads as
|
|
178
|
+
// blocking. Dropping it would hide a finding; honouring it would hand this
|
|
179
|
+
// worker the veto the owner's term denies it.
|
|
180
|
+
const issues = rawIssues.map((i) => (
|
|
181
|
+
i && i.severity === 'blocker'
|
|
182
|
+
? { ...i, severity: 'major', severity_floored_from: 'blocker' }
|
|
183
|
+
: i
|
|
184
|
+
));
|
|
185
|
+
|
|
186
|
+
if (!Number.isFinite(score) || score < 0 || score > 10) {
|
|
187
|
+
return advisory({
|
|
188
|
+
issues,
|
|
189
|
+
notes_md: `Reader — ADVISORY, no score: copy_score was non-numeric or out of range (got ${parsed.copy_score}). Findings below still stand.`,
|
|
190
|
+
error: 'schema_error',
|
|
191
|
+
confidence: conf,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const s = Math.round(score * 10) / 10;
|
|
196
|
+
const breaks = issues.filter((i) => i.kind === 'break').length;
|
|
197
|
+
const flags = issues.filter((i) => i.kind === 'flag').length;
|
|
198
|
+
const prefix = `Reader — ADVISORY (copy=${s}/10, ${breaks} rule break${breaks === 1 ? '' : 's'}, ${flags} flag${flags === 1 ? '' : 's'}, ${reviewed.length} string${reviewed.length === 1 ? '' : 's'} reviewed, conf=${conf ?? '?'})`;
|
|
199
|
+
|
|
200
|
+
return advisory({
|
|
201
|
+
scores: { copy_score: s },
|
|
202
|
+
issues,
|
|
203
|
+
strings_reviewed: reviewed,
|
|
204
|
+
notes_md: `${prefix}\n${notesFromSubagent.trim()}`.trim(),
|
|
205
|
+
confidence: conf,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
KIND,
|
|
211
|
+
NAME,
|
|
212
|
+
SPECIALTY,
|
|
213
|
+
ADVISORY_VERDICT,
|
|
214
|
+
PROMPT_HEADER,
|
|
215
|
+
buildPrompt,
|
|
216
|
+
extractJson,
|
|
217
|
+
computeVerdict,
|
|
218
|
+
};
|