@event4u/agent-config 16.1.0 → 16.2.0

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.
Files changed (217) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +237 -248
  5. package/dist/agent-src/commands/roadmap/process-full.md +1 -1
  6. package/dist/agent-src/commands/work.md +4 -0
  7. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +4 -8
  8. package/dist/agent-src/rules/ui-audit-gate.md +1 -2
  9. package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
  10. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
  11. package/dist/agent-src/skills/design-review/SKILL.md +30 -4
  12. package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
  13. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +16 -1
  14. package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
  15. package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
  16. package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
  17. package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
  18. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  19. package/dist/agent-src/templates/roadmaps.md +30 -0
  20. package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  21. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  22. package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  23. package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  24. package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
  25. package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  26. package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  27. package/dist/cli/python/knowledge_ingest.js +1 -1
  28. package/dist/cli/python/workspace_drive.js +1 -1
  29. package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
  30. package/dist/cli-delegate/{chunk-HDVVGPQW.js → chunk-7XGBX6ZH.js} +471 -387
  31. package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-FP36SWJD.js} +432 -144
  32. package/dist/cli-delegate/{chunk-UNCRJBT3.js → chunk-IGD6ERX6.js} +31 -0
  33. package/dist/cli-delegate/{chunk-FXYUYU4N.js → chunk-JNMVLWLR.js} +42 -1
  34. package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
  35. package/dist/cli-delegate/cmd_conformance.js +902 -782
  36. package/dist/cli-delegate/cmd_doctor.js +2 -2
  37. package/dist/cli-delegate/cmd_export.js +2 -2
  38. package/dist/cli-delegate/cmd_handoff.js +4 -4
  39. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  40. package/dist/cli-delegate/cmd_prune.js +2 -2
  41. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  42. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  43. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  44. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  45. package/dist/cli-delegate/cmd_sync.js +2 -2
  46. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  47. package/dist/cli-delegate/cmd_validate.js +2 -2
  48. package/dist/discovery/deprecation-report.md +1 -1
  49. package/dist/discovery/discovery-manifest.json +10 -10
  50. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  51. package/dist/discovery/discovery-manifest.summary.md +1 -1
  52. package/dist/discovery/dormancy-report.md +1 -1
  53. package/dist/discovery/orphan-report.md +1 -1
  54. package/dist/discovery/packs.json +2 -2
  55. package/dist/discovery/trust-report.md +1 -1
  56. package/dist/discovery/workspaces.json +2 -2
  57. package/dist/hooks/dispatch.js +6666 -2581
  58. package/dist/hooks/dispatch.sha256 +1 -0
  59. package/dist/install/install.mjs +848 -733
  60. package/dist/mcp/registry-manifest.json +2 -2
  61. package/dist/mcp/server.json +2 -2
  62. package/dist/mcp/server.mjs +2747 -1510
  63. package/dist/server/schemas/settings.js +23 -0
  64. package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
  65. package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
  66. package/dist/ui/index.html +2 -2
  67. package/docs/contracts/settings-classes.md +6 -4
  68. package/package.json +4 -4
  69. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  70. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  71. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  72. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  73. package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
  74. package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  75. package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  76. package/src/config/agent-settings.template.yml +29 -0
  77. package/src/config/ci-local-parity.yml +18 -0
  78. package/src/config/continuity-surface.json +2 -2
  79. package/src/config/gate-coverage.yml +234 -3
  80. package/src/config/gate-violation-baselines.json +7 -3
  81. package/src/config/hook-latency-budget.json +21 -0
  82. package/src/config/hook-token-budget.json +2 -2
  83. package/src/config/metric-registry.yml +114 -0
  84. package/src/config/placeholder-drift-budget.json +10 -0
  85. package/src/config/routing-coverage-seed.json +18 -4
  86. package/src/scripts/_cli/cmd_doctor.ts +5 -19
  87. package/src/scripts/_cli/doctor_execution.ts +100 -17
  88. package/src/scripts/_cli/doctor_network_posture.ts +226 -0
  89. package/src/scripts/_lib/body_portable.ts +102 -0
  90. package/src/scripts/_lib/cc_transcript.ts +132 -1
  91. package/src/scripts/_lib/concern_sla_window.ts +200 -0
  92. package/src/scripts/_lib/forge_protection.ts +114 -0
  93. package/src/scripts/_lib/forge_reader.ts +558 -0
  94. package/src/scripts/_lib/host_env_write.ts +111 -0
  95. package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
  96. package/src/scripts/_lib/obligations.ts +119 -8
  97. package/src/scripts/_lib/probe_inputs.ts +271 -0
  98. package/src/scripts/_lib/read_surface_scan.ts +322 -0
  99. package/src/scripts/_lib/release_highlights.ts +2 -1
  100. package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
  101. package/src/scripts/_lib/shell_write_shapes.ts +108 -0
  102. package/src/scripts/_lib/structural_hiding.ts +622 -0
  103. package/src/scripts/_lib/turn_end_refusals.ts +525 -4
  104. package/src/scripts/_lib/turn_end_transcript.ts +216 -0
  105. package/src/scripts/_lib/verification_command.ts +324 -0
  106. package/src/scripts/_lib/verification_evidence.ts +647 -0
  107. package/src/scripts/_lib/verify_clause.ts +280 -0
  108. package/src/scripts/ai_team/team_dispatch.ts +4 -0
  109. package/src/scripts/before_complete_hook.ts +340 -23
  110. package/src/scripts/bench_hook_latency.ts +412 -5
  111. package/src/scripts/cache_realization_report.ts +61 -1
  112. package/src/scripts/check_argument_hint.ts +287 -0
  113. package/src/scripts/check_claims.ts +125 -1
  114. package/src/scripts/check_enforcement_coverage.ts +38 -9
  115. package/src/scripts/check_enforcement_matrix.ts +245 -3
  116. package/src/scripts/check_finding_dispositions.ts +22 -3
  117. package/src/scripts/check_generator_sync.ts +71 -0
  118. package/src/scripts/check_host_docs_digest.ts +786 -0
  119. package/src/scripts/check_host_format_column.ts +559 -0
  120. package/src/scripts/check_input_references.ts +228 -0
  121. package/src/scripts/check_inputs_parity.ts +235 -0
  122. package/src/scripts/check_kernel_edit_ratified.ts +47 -5
  123. package/src/scripts/check_kill_switch_table.ts +294 -0
  124. package/src/scripts/check_placeholder_drift.ts +221 -0
  125. package/src/scripts/check_read_surface_coverage.ts +515 -0
  126. package/src/scripts/check_release_holds.ts +2 -1
  127. package/src/scripts/check_routing_coverage.ts +378 -11
  128. package/src/scripts/check_static_parity.ts +17 -0
  129. package/src/scripts/ci_settle.ts +145 -19
  130. package/src/scripts/closure_scan.ts +115 -1
  131. package/src/scripts/compile_hook_manifest.ts +60 -16
  132. package/src/scripts/external_sources_denylist.json +29 -4
  133. package/src/scripts/hook_manifest.json +1 -1
  134. package/src/scripts/hook_manifest.yaml +42 -5
  135. package/src/scripts/hooks/block_config_weakening.ts +229 -4
  136. package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
  137. package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
  138. package/src/scripts/hooks/bundle_integrity.ts +291 -0
  139. package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
  140. package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
  141. package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
  142. package/src/scripts/hooks/concern_registry.ts +2 -0
  143. package/src/scripts/hooks/concern_timings.ts +94 -0
  144. package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
  145. package/src/scripts/hooks/design_pass_hook.ts +100 -9
  146. package/src/scripts/hooks/design_slop_hook.ts +1 -2
  147. package/src/scripts/hooks/dispatch_hook.ts +25 -19
  148. package/src/scripts/hooks/edit_shape_hook.ts +1 -2
  149. package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
  150. package/src/scripts/hooks/evidence_independence.ts +1 -2
  151. package/src/scripts/hooks/exit_codes.ts +128 -0
  152. package/src/scripts/hooks/host_lowering.json +1 -0
  153. package/src/scripts/hooks/host_lowering.ts +128 -3
  154. package/src/scripts/hooks/host_lowering.yaml +262 -46
  155. package/src/scripts/hooks/host_semantics.ts +30 -2
  156. package/src/scripts/hooks/injection_budget.ts +9 -2
  157. package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
  158. package/src/scripts/hooks/journal_record_hook.ts +1 -2
  159. package/src/scripts/hooks/memory_recall_hook.ts +1 -2
  160. package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
  161. package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
  162. package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
  163. package/src/scripts/hooks/permission_gate.ts +1 -2
  164. package/src/scripts/hooks/push_settle_hook.ts +1 -3
  165. package/src/scripts/hooks/replay_hook.ts +2 -1
  166. package/src/scripts/hooks/reread_guard_hook.ts +1 -2
  167. package/src/scripts/hooks/review_baseline_hook.ts +1 -2
  168. package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
  169. package/src/scripts/hooks/rule_inject_hook.ts +8 -5
  170. package/src/scripts/hooks/run_continuation_hook.ts +13 -15
  171. package/src/scripts/hooks/skill_route_hook.ts +1 -2
  172. package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
  173. package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
  174. package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
  175. package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
  176. package/src/scripts/hooks/table_fingerprint.ts +49 -0
  177. package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
  178. package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
  179. package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
  180. package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
  181. package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
  182. package/src/scripts/install.ts +3 -4
  183. package/src/scripts/lint_deny_text.ts +419 -0
  184. package/src/scripts/lint_exit_codes.ts +262 -0
  185. package/src/scripts/lint_hook_manifest.ts +55 -0
  186. package/src/scripts/lint_roadmap_blockers.ts +78 -12
  187. package/src/scripts/lint_skill_descriptions.ts +1 -1
  188. package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
  189. package/src/scripts/mcp_server/prompts.ts +142 -1
  190. package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
  191. package/src/scripts/measure_turn_end_gate.ts +193 -0
  192. package/src/scripts/memory_lookup.ts +1 -1
  193. package/src/scripts/prepack-check.mjs +53 -0
  194. package/src/scripts/release_publication.ts +13 -1
  195. package/src/scripts/report_invocation_surface.ts +383 -0
  196. package/src/scripts/report_obligation_carriers.ts +699 -0
  197. package/src/scripts/report_obligation_writer_split.ts +236 -0
  198. package/src/scripts/report_skill_menu_census.ts +208 -0
  199. package/src/scripts/report_standing_payload_by_host.ts +1 -1
  200. package/src/scripts/roadmap_verify_share.ts +213 -0
  201. package/src/scripts/schemas/command.schema.json +44 -0
  202. package/src/scripts/schemas/review-findings.schema.json +4 -0
  203. package/src/scripts/schemas/skill.schema.json +44 -0
  204. package/src/scripts/self_review_gate.ts +359 -10
  205. package/src/scripts/semantic_parity_census.ts +314 -0
  206. package/src/scripts/session_eol_report.ts +54 -1
  207. package/src/scripts/sweep_dead_scan_roots.ts +1 -1
  208. package/src/scripts/trigger_eval_grandfather.json +0 -2
  209. package/src/scripts/trigger_eval_rotation.ts +122 -27
  210. package/src/scripts/ui_conformance_probe.ts +156 -4
  211. package/src/scripts/update_prices.ts +85 -3
  212. package/src/scripts/write_bundle_digest.mjs +74 -0
  213. package/src/server/schemas/settings.ts +31 -0
  214. package/src/templates/consumer-settings/ONBOARDING.md +1 -0
  215. package/dist/ui/assets/index-DH1xc62v.js +0 -29
  216. package/dist/ui/assets/index-jSfMW8_t.css +0 -1
  217. /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
@@ -7,35 +7,49 @@
7
7
  * the corpus's expected skill per line". Building it surfaced two defects in
8
8
  * that premise, and this file is written around them rather than over them.
9
9
  *
10
- * DEFECT A — the 496-line corpus has no expected SKILL. `tests/eval/routing-matrix/`
11
- * is keyed by `rule:` and every prompt is labelled with the RULE it should
12
- * activate. Scoring a skill ranker against it would need a skill label that
13
- * does not exist there, so a "hit rate" over it would be invented.
10
+ * DEFECT A — the routing matrix had no expected SKILL. `tests/eval/routing-matrix/`
11
+ * is keyed by `rule:` and every prompt was labelled with the RULE it should
12
+ * activate. Scoring a skill ranker against it would need a skill label that did
13
+ * not exist there, so a "hit rate" over it would be invented.
14
14
  *
15
- * DEFECT B — 496 is stale. The matrix holds 499 prompts today
16
- * (`grep -h "^\s*- prompt:" tests/eval/routing-matrix/*.yaml | wc -l`), and 496
17
- * is still published in six places including `hook-token-budget.json`.
15
+ * **Defect A is CLOSED as of `road-to-a-menu-whose-precision-is-measured` 1.1.**
16
+ * Every case in the matrix now carries an `expected_skills` list, hand-written
17
+ * by seats that had not read this ranker's scoring (the protocol is in
18
+ * `tests/eval/routing-matrix/README.md`). An EMPTY list is a first-class value
19
+ * meaning "deliberately no skill expectation", and such a row is excluded from
20
+ * the accuracy denominator rather than counted as a miss. So the matrix arm now
21
+ * has ground truth; `--corpus routing-matrix` scores against it.
18
22
  *
19
- * So the baseline is measured against the corpus that DOES carry expected
20
- * skills — `tests/eval/corpus-{dev,non-dev}.yaml`, 26 labelled prompts — and
21
- * the routing matrix is reported alongside it as what it can honestly be: a
22
- * COVERAGE measurement (does the ranker return anything at all, and how
23
- * confident) with no ground truth and therefore no accuracy.
23
+ * DEFECT B — the published size is stale, repeatedly. 496 was published while
24
+ * the matrix held 499, and 499 is still published in several sites while the
25
+ * matrix holds more than that again. The size is therefore MEASURED on every run
26
+ * and printed, never asserted from a constant.
24
27
  *
25
- * Both halves are emitted in one JSON object so a later ranker revision can be
26
- * compared on the same two axes in one run:
28
+ * THE INTERVAL, AND THE REFUSAL TO PRINT A VERDICT UNDER n = 100.
29
+ * A hit rate over 26 prompts
30
+ * moves ~4 points per prompt; quoting it as "61.5 %" invites a comparison the
31
+ * sample cannot carry. Both arms therefore report a 95 % Wilson score interval
32
+ * (`_lib/capture_rate.ts` — Wilson rather than the normal approximation for the
33
+ * same reason stated there: near p = 1 the normal one produces bounds above 1).
27
34
  *
28
- * ./scripts-run src/scripts/measure_skill_ranker_baseline --json
29
- * ./scripts-run src/scripts/measure_skill_ranker_baseline --ranker keyword-v2 --json
35
+ * Below {@link MIN_POWERED_N} labelled rows the arm's `verdict` is
36
+ * `underpowered`. That is not a failure and not an exit code — the point
37
+ * estimate and the interval are still printed, because suppressing them would
38
+ * replace a wide measurement with no measurement. It is a label that travels
39
+ * with the number so a later reader cannot quote the point estimate alone.
30
40
  *
31
- * A 26-prompt denominator is small and is stated as such in the output. It is
32
- * the whole labelled ground truth this repository has.
41
+ * Usage:
42
+ * ./scripts-run src/scripts/measure_skill_ranker_baseline
43
+ * ./scripts-run src/scripts/measure_skill_ranker_baseline --corpus routing-matrix
44
+ * ./scripts-run src/scripts/measure_skill_ranker_baseline --corpus all
45
+ * ./scripts-run src/scripts/measure_skill_ranker_baseline --ranker keyword-v2
33
46
  */
34
47
 
35
48
  import * as fs from 'node:fs';
36
49
  import * as path from 'node:path';
37
50
  import { fileURLToPath } from 'node:url';
38
51
 
52
+ import { wilsonInterval } from './_lib/capture_rate.js';
39
53
  import { rank } from './skill_tools/score_skill_relevance.js';
40
54
  import type { RankOptions } from '../shared/skillRanking.js';
41
55
 
@@ -45,6 +59,20 @@ const MATRIX_DIR = 'tests/eval/routing-matrix';
45
59
  /** Where the projected skills live in this checkout. */
46
60
  export const SKILLS_DIR = path.join(REPO, 'src', 'skills');
47
61
 
62
+ /**
63
+ * The smallest labelled corpus this report will call powered.
64
+ *
65
+ * 100 is the roadmap's number, not a derived optimum, and it is stated as such:
66
+ * at n = 100 a point estimate near 0.7 carries a 95 % Wilson half-width of
67
+ * roughly ±9 points, which is wide but no longer moves on a single prompt the
68
+ * way a 26-row denominator does. Raise it with a dated reason if a decision
69
+ * ever needs a tighter bound than that.
70
+ */
71
+ export const MIN_POWERED_N = 100;
72
+
73
+ export type CorpusName = 'labelled' | 'routing-matrix' | 'all';
74
+ export const CORPUS_NAMES: readonly CorpusName[] = ['labelled', 'routing-matrix', 'all'];
75
+
48
76
  export interface LabelledPrompt {
49
77
  id: string;
50
78
  corpus: string;
@@ -94,38 +122,193 @@ export function readLabelledPrompts(repo = REPO): LabelledPrompt[] {
94
122
  return out;
95
123
  }
96
124
 
97
- /** Every `- prompt:` string in the rule routing matrix. No skill label exists. */
98
- export function readMatrixPrompts(repo = REPO): string[] {
125
+ export interface MatrixCase {
126
+ rule: string;
127
+ section: 'positives' | 'near_misses';
128
+ ordinal: number;
129
+ prompt: string;
130
+ /** `undefined` = the key is missing (a defect the lint catches); `[]` = deliberately unlabelled. */
131
+ expected?: string[];
132
+ }
133
+
134
+ /**
135
+ * Every case in the rule routing matrix, with its `expected_skills` label when
136
+ * one is present.
137
+ *
138
+ * Line-oriented for the same reason as the reader above, and because a YAML
139
+ * round-trip through this directory would reformat 100+ hand-kept fixtures.
140
+ * A case is `prompt:` plus the keys indented under it up to the next `- `.
141
+ */
142
+ export function readMatrixCases(repo = REPO): MatrixCase[] {
99
143
  const dir = path.join(repo, MATRIX_DIR);
100
144
  if (!fs.existsSync(dir)) return [];
101
- const out: string[] = [];
145
+ const out: MatrixCase[] = [];
102
146
  for (const name of fs.readdirSync(dir).sort()) {
103
147
  if (!name.endsWith('.yaml')) continue;
148
+ const rule = name.replace(/\.yaml$/, '');
149
+ let section: MatrixCase['section'] = 'positives';
150
+ let ordinal = 0;
151
+ let current: MatrixCase | undefined;
104
152
  for (const raw of fs.readFileSync(path.join(dir, name), 'utf8').split('\n')) {
105
- const m = /^\s*-?\s*prompt:\s*"(.*)"\s*$/.exec(raw.replace(/\r$/, ''));
106
- if (m && m[1]) out.push(m[1]);
153
+ const line = raw.replace(/\r$/, '');
154
+ if (/^positives:\s*$/.test(line)) {
155
+ section = 'positives';
156
+ ordinal = 0;
157
+ current = undefined;
158
+ continue;
159
+ }
160
+ if (/^near_misses:\s*$/.test(line)) {
161
+ section = 'near_misses';
162
+ ordinal = 0;
163
+ current = undefined;
164
+ continue;
165
+ }
166
+ const mPrompt = /^\s*-?\s*prompt:\s*"(.*)"\s*$/.exec(line);
167
+ if (mPrompt && mPrompt[1] !== undefined) {
168
+ current = { rule, section, ordinal: ordinal++, prompt: mPrompt[1] };
169
+ out.push(current);
170
+ continue;
171
+ }
172
+ const mExp = /^\s*expected_skills:\s*\[(.*)\]\s*$/.exec(line);
173
+ if (mExp && current) {
174
+ current.expected = mExp[1]!
175
+ .split(',')
176
+ .map((s) => s.trim())
177
+ .filter(Boolean);
178
+ }
107
179
  }
108
180
  }
109
181
  return out;
110
182
  }
111
183
 
184
+ /** Every `- prompt:` string in the rule routing matrix, label or no label. */
185
+ export function readMatrixPrompts(repo = REPO): string[] {
186
+ return readMatrixCases(repo).map((c) => c.prompt);
187
+ }
188
+
189
+ /** The matrix rows that carry a NON-EMPTY label — the accuracy denominator. */
190
+ export function readMatrixLabelledPrompts(repo = REPO): LabelledPrompt[] {
191
+ return readMatrixCases(repo)
192
+ .filter((c) => (c.expected?.length ?? 0) > 0)
193
+ .map((c) => ({
194
+ id: `${c.rule}#${c.section}[${c.ordinal}]`,
195
+ corpus: 'routing-matrix',
196
+ prompt: c.prompt,
197
+ expected: c.expected!,
198
+ }));
199
+ }
200
+
201
+ export function promptsForCorpus(corpus: CorpusName, repo = REPO): LabelledPrompt[] {
202
+ if (corpus === 'labelled') return readLabelledPrompts(repo);
203
+ if (corpus === 'routing-matrix') return readMatrixLabelledPrompts(repo);
204
+ return [...readLabelledPrompts(repo), ...readMatrixLabelledPrompts(repo)];
205
+ }
206
+
207
+ /**
208
+ * The packs a skill declares, read from its own `packs:` frontmatter block.
209
+ *
210
+ * Line-oriented, and deliberately NOT read from `src/packs/*\/pack.yaml`: those
211
+ * manifests are generated FROM this frontmatter, and four of them
212
+ * (`analytics`, `core`, `memory`, `product-reasoning`) carry no skill at all.
213
+ * A pack with no skill can never own a labelled prompt, so a census over the
214
+ * manifest set would state a floor nothing could ever meet.
215
+ */
216
+ export function packsForSkill(skillsDir: string, name: string): string[] {
217
+ const file = path.join(skillsDir, name, 'SKILL.md');
218
+ if (!fs.existsSync(file)) return [];
219
+ const text = fs.readFileSync(file, 'utf8');
220
+ if (!text.startsWith('---')) return [];
221
+ const end = text.indexOf('\n---', 3);
222
+ if (end === -1) return [];
223
+ const lines = text.slice(3, end).split('\n');
224
+ const at = lines.findIndex((l) => /^packs:/u.test(l));
225
+ if (at === -1) return [];
226
+ const out: string[] = [];
227
+ for (let i = at + 1; i < lines.length; i += 1) {
228
+ const m = /^\s+-\s+(\S+)\s*$/u.exec(lines[i] as string);
229
+ if (!m) break;
230
+ out.push(m[1] as string);
231
+ }
232
+ return out;
233
+ }
234
+
235
+ /** Every pack id that at least one shipped skill declares. The census domain. */
236
+ export function allPacks(skillsDir: string): string[] {
237
+ const packs = new Set<string>();
238
+ for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
239
+ if (!entry.isDirectory()) continue;
240
+ for (const p of packsForSkill(skillsDir, entry.name)) packs.add(p);
241
+ }
242
+ return [...packs].sort();
243
+ }
244
+
245
+ /**
246
+ * How many labelled prompts each pack owns, via the packs of its expected skills.
247
+ *
248
+ * A prompt counts ONCE per pack however many of that pack's skills it expects —
249
+ * otherwise a single prompt naming three `laravel` skills would satisfy a
250
+ * three-prompt floor on its own, which is the shape of coverage this census
251
+ * exists to refuse.
252
+ */
253
+ export function packCensus(prompts: readonly LabelledPrompt[], skillsDir: string): Record<string, number> {
254
+ const out: Record<string, number> = {};
255
+ for (const p of allPacks(skillsDir)) out[p] = 0;
256
+ for (const prompt of prompts) {
257
+ const packs = new Set<string>();
258
+ for (const skill of prompt.expected) for (const pack of packsForSkill(skillsDir, skill)) packs.add(pack);
259
+ for (const pack of packs) out[pack] = (out[pack] ?? 0) + 1;
260
+ }
261
+ return out;
262
+ }
263
+
264
+ /** Packs owning fewer than `floor` labelled prompts, sorted by pack id. */
265
+ export function packsBelow(census: Record<string, number>, floor: number): string[] {
266
+ return Object.keys(census)
267
+ .filter((p) => (census[p] ?? 0) < floor)
268
+ .sort();
269
+ }
270
+
271
+ /** The per-pack floor `road-to-a-menu-whose-precision-is-measured` 1.1 states. */
272
+ export const MIN_PROMPTS_PER_PACK = 3;
273
+
274
+ export interface Interval {
275
+ lower: number;
276
+ upper: number;
277
+ }
278
+
112
279
  export interface AccuracyArm {
280
+ corpus: CorpusName;
113
281
  corpus_prompts: number;
114
282
  top1: number;
283
+ top1_ci95: Interval;
115
284
  top3: number;
285
+ top3_ci95: Interval;
286
+ /** `underpowered` below MIN_POWERED_N labelled rows; `measured` at or above. */
287
+ verdict: 'underpowered' | 'measured';
288
+ min_powered_n: number;
116
289
  misses: string[];
117
290
  denominator_note: string;
291
+ /** Pack ids at least one shipped skill declares — the census domain. */
292
+ packs_total: number;
293
+ min_prompts_per_pack: number;
294
+ /** Packs this corpus leaves under the floor. Empty is the passing shape. */
295
+ packs_below_floor: string[];
296
+ /** Full census, so a reader can see the distribution and not just the failures. */
297
+ pack_census: Record<string, number>;
118
298
  }
119
299
 
120
300
  export interface CoverageArm {
121
301
  corpus_prompts: number;
302
+ labelled_prompts: number;
303
+ unlabelled_prompts: number;
304
+ missing_label_key: number;
122
305
  prompts_with_any_result: number;
123
306
  mean_top_score: number;
124
307
  note: string;
125
308
  }
126
309
 
127
310
  export interface RankerBaseline {
128
- schema: 1;
311
+ schema: 2;
129
312
  ranker: string;
130
313
  commit: string;
131
314
  skills_dir: string;
@@ -139,28 +322,85 @@ function hitAt(rows: readonly (readonly [string, number, string[]])[], n: number
139
322
  return expected.some((e) => top.includes(e));
140
323
  }
141
324
 
142
- export function measure(opts: { ranker: string; commit: string; skillsDir?: string; repo?: string }): RankerBaseline {
143
- const repo = opts.repo ?? REPO;
144
- const skillsDir = opts.skillsDir ?? SKILLS_DIR;
145
- // `keyword-v2` is Phase 3.1: the same formula with `triggers:` prose folded
146
- // into each skill's term source. Any other label measures v1.
147
- const rankOpts: RankOptions = opts.ranker === 'keyword-v2' ? { includeTriggers: true } : {};
148
- const labelled = readLabelledPrompts(repo);
325
+ function round3(n: number): number {
326
+ return Math.round(n * 1000) / 1000;
327
+ }
328
+
329
+ function ci(successes: number, trials: number): Interval {
330
+ const raw = wilsonInterval(successes, trials);
331
+ return { lower: round3(raw.lower), upper: round3(raw.upper) };
332
+ }
333
+
334
+ const DENOMINATOR_NOTES: Record<CorpusName, string> = {
335
+ labelled:
336
+ 'tests/eval/corpus-dev.yaml + corpus-non-dev.yaml — the two hand-written eval corpora. ' +
337
+ 'Small; a single prompt moves each rate by ~4 points, which is why the interval is ' +
338
+ 'printed beside the point estimate and not instead of it.',
339
+ 'routing-matrix':
340
+ 'tests/eval/routing-matrix/ rows whose expected_skills list is NON-EMPTY. A row with an ' +
341
+ 'empty list is a deliberate "no skill expectation" and is excluded from the ' +
342
+ 'denominator — never counted as a miss.',
343
+ all:
344
+ 'Both labelled sources pooled. The two were written under different protocols, so a ' +
345
+ 'pooled rate is a convenience reading and the per-corpus arms are the citable ones.',
346
+ };
347
+
348
+ export function measureAccuracy(opts: {
349
+ corpus: CorpusName;
350
+ repo: string;
351
+ skillsDir: string;
352
+ rankOpts: RankOptions;
353
+ }): AccuracyArm {
354
+ const labelled = promptsForCorpus(opts.corpus, opts.repo);
149
355
  let top1 = 0;
150
356
  let top3 = 0;
151
357
  const misses: string[] = [];
152
358
  for (const p of labelled) {
153
- const rows = rank(p.prompt, skillsDir, rankOpts);
359
+ const rows = rank(p.prompt, opts.skillsDir, opts.rankOpts);
154
360
  if (hitAt(rows, 1, p.expected)) top1++;
155
361
  if (hitAt(rows, 3, p.expected)) top3++;
156
362
  else misses.push(p.id);
157
363
  }
364
+ const n = labelled.length;
365
+ const census = packCensus(labelled, opts.skillsDir);
366
+ return {
367
+ corpus: opts.corpus,
368
+ corpus_prompts: n,
369
+ top1: n ? round3(top1 / n) : 0,
370
+ top1_ci95: ci(top1, n),
371
+ top3: n ? round3(top3 / n) : 0,
372
+ top3_ci95: ci(top3, n),
373
+ verdict: n >= MIN_POWERED_N ? 'measured' : 'underpowered',
374
+ min_powered_n: MIN_POWERED_N,
375
+ misses,
376
+ denominator_note: DENOMINATOR_NOTES[opts.corpus],
377
+ packs_total: Object.keys(census).length,
378
+ min_prompts_per_pack: MIN_PROMPTS_PER_PACK,
379
+ packs_below_floor: packsBelow(census, MIN_PROMPTS_PER_PACK),
380
+ pack_census: census,
381
+ };
382
+ }
383
+
384
+ export function measure(opts: {
385
+ ranker: string;
386
+ commit: string;
387
+ corpus?: CorpusName;
388
+ skillsDir?: string;
389
+ repo?: string;
390
+ }): RankerBaseline {
391
+ const repo = opts.repo ?? REPO;
392
+ const skillsDir = opts.skillsDir ?? SKILLS_DIR;
393
+ // `keyword-v2` is Phase 3.1: the same formula with `triggers:` prose folded
394
+ // into each skill's term source. Any other label measures v1.
395
+ const rankOpts: RankOptions = opts.ranker === 'keyword-v2' ? { includeTriggers: true } : {};
396
+ const corpus = opts.corpus ?? 'labelled';
397
+ const accuracy = measureAccuracy({ corpus, repo, skillsDir, rankOpts });
158
398
 
159
- const matrix = readMatrixPrompts(repo);
399
+ const cases = readMatrixCases(repo);
160
400
  let withResult = 0;
161
401
  let scoreSum = 0;
162
- for (const prompt of matrix) {
163
- const rows = rank(prompt, skillsDir, rankOpts);
402
+ for (const c of cases) {
403
+ const rows = rank(c.prompt, skillsDir, rankOpts);
164
404
  if (rows.length > 0) {
165
405
  withResult++;
166
406
  scoreSum += rows[0]![1];
@@ -172,37 +412,51 @@ export function measure(opts: { ranker: string; commit: string; skillsDir?: stri
172
412
  : 0;
173
413
 
174
414
  return {
175
- schema: 1,
415
+ schema: 2,
176
416
  ranker: opts.ranker,
177
417
  commit: opts.commit,
178
418
  skills_dir: path.relative(repo, skillsDir),
179
419
  skills_indexed: skillsIndexed,
180
- accuracy: {
181
- corpus_prompts: labelled.length,
182
- top1: labelled.length ? Math.round((top1 / labelled.length) * 1000) / 1000 : 0,
183
- top3: labelled.length ? Math.round((top3 / labelled.length) * 1000) / 1000 : 0,
184
- misses,
185
- denominator_note:
186
- 'tests/eval/corpus-dev.yaml + corpus-non-dev.yaml — the only expected-skill ground ' +
187
- 'truth in this tree. Small; a single prompt moves each rate by ~4 points.',
188
- },
420
+ accuracy,
189
421
  matrix_coverage: {
190
- corpus_prompts: matrix.length,
422
+ corpus_prompts: cases.length,
423
+ labelled_prompts: cases.filter((c) => (c.expected?.length ?? 0) > 0).length,
424
+ unlabelled_prompts: cases.filter((c) => c.expected?.length === 0).length,
425
+ missing_label_key: cases.filter((c) => c.expected === undefined).length,
191
426
  prompts_with_any_result: withResult,
192
427
  mean_top_score: withResult ? Math.round((scoreSum / withResult) * 100) / 100 : 0,
193
428
  note:
194
- 'tests/eval/routing-matrix/ is labelled with the expected RULE, never a skill, so ' +
195
- 'this arm carries no accuracy — only whether the ranker answers at all and how ' +
196
- 'confident its top answer is. Its size is measured here, not assumed: the roadmap ' +
197
- 'and five other sites still publish 496.',
429
+ 'Coverage over EVERY matrix case, labelled or not: whether the ranker answers at ' +
430
+ 'all and how confident its top answer is. Accuracy over the labelled subset is ' +
431
+ 'the `accuracy` arm under `--corpus routing-matrix`. Size is measured here, never ' +
432
+ 'asserted: 496 and 499 are both published elsewhere and both stale.',
198
433
  },
199
434
  };
200
435
  }
201
436
 
437
+ function parseCorpus(argv: readonly string[]): CorpusName {
438
+ const i = argv.indexOf('--corpus');
439
+ if (i === -1) return 'labelled';
440
+ const raw = argv[i + 1];
441
+ if (!raw || !CORPUS_NAMES.includes(raw as CorpusName)) {
442
+ throw new Error(
443
+ `measure_skill_ranker_baseline: --corpus expects one of ${CORPUS_NAMES.join(' | ')}, got ${raw ?? '(nothing)'}`,
444
+ );
445
+ }
446
+ return raw as CorpusName;
447
+ }
448
+
202
449
  export function main(argv: readonly string[]): number {
203
450
  const ranker = argv.includes('--ranker') ? (argv[argv.indexOf('--ranker') + 1] ?? 'keyword-v1') : 'keyword-v1';
204
451
  const commit = argv.includes('--commit') ? (argv[argv.indexOf('--commit') + 1] ?? 'unknown') : 'unknown';
205
- const out = measure({ ranker, commit });
452
+ let corpus: CorpusName;
453
+ try {
454
+ corpus = parseCorpus(argv);
455
+ } catch (err) {
456
+ process.stderr.write(`${(err as Error).message}\n`);
457
+ return 2;
458
+ }
459
+ const out = measure({ ranker, commit, corpus });
206
460
  process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
207
461
  return 0;
208
462
  }