@event4u/agent-config 16.0.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 (382) 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 +289 -140
  5. package/CONTRIBUTING.md +9 -0
  6. package/README.md +2 -2
  7. package/dist/agent-src/commands/analyze/inbox.md +26 -0
  8. package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
  9. package/dist/agent-src/commands/challenge-me/closure.md +178 -0
  10. package/dist/agent-src/commands/challenge-me.md +9 -2
  11. package/dist/agent-src/commands/council/default.md +25 -5
  12. package/dist/agent-src/commands/feature/plan.md +28 -1
  13. package/dist/agent-src/commands/feature/roadmap.md +28 -1
  14. package/dist/agent-src/commands/implement-ticket.md +27 -0
  15. package/dist/agent-src/commands/jira-ticket.md +27 -0
  16. package/dist/agent-src/commands/pr/create.md +29 -0
  17. package/dist/agent-src/commands/pr/merge.md +60 -9
  18. package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
  19. package/dist/agent-src/commands/roadmap/create.md +74 -27
  20. package/dist/agent-src/commands/roadmap/materialize.md +27 -0
  21. package/dist/agent-src/commands/roadmap/process-full.md +123 -18
  22. package/dist/agent-src/commands/team.md +22 -0
  23. package/dist/agent-src/commands/work.md +4 -0
  24. package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
  25. package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +47 -0
  26. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
  27. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +19 -23
  28. package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
  29. package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
  30. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
  31. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
  32. package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  33. package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  34. package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
  35. package/dist/agent-src/rules/autonomous-execution.md +15 -8
  36. package/dist/agent-src/rules/delegation-policy.md +23 -45
  37. package/dist/agent-src/rules/design-fidelity.md +32 -37
  38. package/dist/agent-src/rules/evaluator-independence.md +43 -49
  39. package/dist/agent-src/rules/test-first.md +41 -0
  40. package/dist/agent-src/rules/ui-audit-gate.md +1 -2
  41. package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
  42. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
  43. package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
  44. package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
  45. package/dist/agent-src/skills/database/data/manifest.json +4 -4
  46. package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
  47. package/dist/agent-src/skills/design-review/SKILL.md +80 -4
  48. package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
  49. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +30 -5
  50. package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
  51. package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
  52. package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
  53. package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
  54. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  55. package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
  56. package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
  57. package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
  58. package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
  59. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
  60. package/dist/agent-src/templates/agent-settings.md +1 -1
  61. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  62. package/dist/agent-src/templates/roadmaps.md +218 -0
  63. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  64. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  65. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  66. package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  67. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  68. package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  69. package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  70. package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
  71. package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  72. package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  73. package/dist/cli/python/knowledge_ingest.js +1 -1
  74. package/dist/cli/python/workspace_drive.js +1 -1
  75. package/dist/cli-delegate/check_update_banner.js +40 -2
  76. package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
  77. package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
  78. package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
  79. package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
  80. package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
  81. package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-7XGBX6ZH.js} +973 -555
  82. package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
  83. package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-FP36SWJD.js} +587 -141
  84. package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-IGD6ERX6.js} +62 -7
  85. package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-JNMVLWLR.js} +99 -14
  86. package/dist/cli-delegate/{chunk-H32DGUJH.js → chunk-P6ZHP5W2.js} +1 -1
  87. package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
  88. package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
  89. package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
  90. package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
  91. package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
  92. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  93. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  94. package/dist/cli-delegate/cmd_conformance.js +985 -831
  95. package/dist/cli-delegate/cmd_doctor.js +7 -5
  96. package/dist/cli-delegate/cmd_explain.js +2 -2
  97. package/dist/cli-delegate/cmd_export.js +9 -7
  98. package/dist/cli-delegate/cmd_fleet.js +4 -2
  99. package/dist/cli-delegate/cmd_handoff.js +7 -7
  100. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  101. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  102. package/dist/cli-delegate/cmd_migrate.js +2 -2
  103. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  104. package/dist/cli-delegate/cmd_preflight.js +4 -2
  105. package/dist/cli-delegate/cmd_prune.js +10 -8
  106. package/dist/cli-delegate/cmd_route_audit.js +3 -3
  107. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  108. package/dist/cli-delegate/cmd_session_recycle.js +4 -4
  109. package/dist/cli-delegate/cmd_settings_get.js +28 -2
  110. package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
  111. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  112. package/dist/cli-delegate/cmd_sync.js +10 -8
  113. package/dist/cli-delegate/cmd_uninstall.js +16 -13
  114. package/dist/cli-delegate/cmd_update.js +1 -1
  115. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  116. package/dist/cli-delegate/cmd_validate.js +12 -10
  117. package/dist/cli-delegate/cmd_versions.js +1 -1
  118. package/dist/discovery/deprecation-report.md +1 -1
  119. package/dist/discovery/discovery-manifest.json +90 -43
  120. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  121. package/dist/discovery/discovery-manifest.summary.md +4 -4
  122. package/dist/discovery/dormancy-report.md +1 -1
  123. package/dist/discovery/orphan-report.md +1 -1
  124. package/dist/discovery/packs.json +10 -8
  125. package/dist/discovery/trust-report.md +3 -3
  126. package/dist/discovery/workspaces.json +8 -6
  127. package/dist/hooks/dispatch.js +7682 -2772
  128. package/dist/hooks/dispatch.sha256 +1 -0
  129. package/dist/install/claudeRuleRewrite.js +14 -3
  130. package/dist/install/conflict.js +60 -10
  131. package/dist/install/conflictTracker.js +218 -0
  132. package/dist/install/fsPrimitives.js +54 -0
  133. package/dist/install/install.mjs +2200 -1559
  134. package/dist/install/manifestFiles.js +259 -0
  135. package/dist/install/preflight.js +14 -2
  136. package/dist/install/preserve.js +157 -0
  137. package/dist/install/recordedOwnership.js +132 -0
  138. package/dist/install/txlog.js +11 -2
  139. package/dist/mcp/registry-manifest.json +3 -3
  140. package/dist/mcp/server.json +2 -2
  141. package/dist/mcp/server.mjs +3066 -1470
  142. package/dist/router.json +1 -1
  143. package/dist/scripts/_lib/agent_settings.js +3 -2
  144. package/dist/scripts/_lib/fs_atomic.js +167 -0
  145. package/dist/scripts/_lib/installed_tools.js +460 -0
  146. package/dist/scripts/_lib/lean_projection_mode.js +3 -2
  147. package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
  148. package/dist/server/routes/install.js +8 -0
  149. package/dist/server/routes/wizard.js +9 -3
  150. package/dist/server/schemas/settings.js +40 -5
  151. package/dist/shared/missionExecution.js +97 -0
  152. package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
  153. package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
  154. package/dist/ui/index.html +2 -2
  155. package/docs/MIGRATION.md +103 -0
  156. package/docs/contracts/settings-classes.md +28 -7
  157. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
  158. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
  159. package/docs/guidelines/design-fidelity-routing.md +29 -0
  160. package/package.json +4 -4
  161. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
  162. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
  163. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
  164. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  165. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  166. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  167. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  168. package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
  169. package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  170. package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  171. package/src/config/agent-settings.template.yml +168 -27
  172. package/src/config/auto-rule-budget.json +16 -0
  173. package/src/config/ci-local-parity.yml +36 -5
  174. package/src/config/continuity-surface.json +23 -2
  175. package/src/config/gate-coverage.yml +377 -13
  176. package/src/config/gate-violation-baselines.json +10 -6
  177. package/src/config/hook-latency-budget.json +21 -0
  178. package/src/config/hook-token-budget.json +4 -2
  179. package/src/config/loop-surfaces.yaml +215 -0
  180. package/src/config/metric-registry.yml +114 -0
  181. package/src/config/placeholder-drift-budget.json +10 -0
  182. package/src/config/platform-anchor.json +3 -2
  183. package/src/config/release-gate-locality.yml +11 -0
  184. package/src/config/routing-coverage-seed.json +33 -5
  185. package/src/config/rule-enforcement-baseline.json +0 -1
  186. package/src/install/claudeRuleRewrite.ts +16 -2
  187. package/src/install/conflict.ts +65 -9
  188. package/src/install/conflictTracker.ts +299 -0
  189. package/src/install/fsPrimitives.ts +54 -0
  190. package/src/install/manifestFiles.ts +312 -0
  191. package/src/install/preflight.ts +16 -2
  192. package/src/install/preserve.ts +205 -0
  193. package/src/install/recordedOwnership.ts +149 -0
  194. package/src/install/txlog.ts +11 -2
  195. package/src/install/types.ts +19 -4
  196. package/src/scripts/_cli/cmd_conformance.ts +88 -15
  197. package/src/scripts/_cli/cmd_doctor.ts +7 -20
  198. package/src/scripts/_cli/cmd_route_explain.ts +9 -2
  199. package/src/scripts/_cli/cmd_settings_get.ts +44 -0
  200. package/src/scripts/_cli/doctor_execution.ts +173 -0
  201. package/src/scripts/_cli/doctor_network_posture.ts +226 -0
  202. package/src/scripts/_lib/activation_payload.ts +247 -0
  203. package/src/scripts/_lib/agent_settings.ts +4 -2
  204. package/src/scripts/_lib/authority_path.ts +102 -0
  205. package/src/scripts/_lib/body_portable.ts +102 -0
  206. package/src/scripts/_lib/cascade_base.ts +94 -0
  207. package/src/scripts/_lib/cc_transcript.ts +132 -1
  208. package/src/scripts/_lib/changelog_eras.ts +7 -2
  209. package/src/scripts/_lib/concern_sla_window.ts +200 -0
  210. package/src/scripts/_lib/continuation_ladder.ts +149 -2
  211. package/src/scripts/_lib/council_transport.ts +129 -0
  212. package/src/scripts/_lib/delivery_ready.ts +112 -0
  213. package/src/scripts/_lib/forge_protection.ts +381 -0
  214. package/src/scripts/_lib/forge_reader.ts +558 -0
  215. package/src/scripts/_lib/hook_platform_keys.ts +27 -0
  216. package/src/scripts/_lib/host_env_write.ts +111 -0
  217. package/src/scripts/_lib/json_python_parity.ts +127 -0
  218. package/src/scripts/_lib/lean_projection_mode.ts +14 -3
  219. package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
  220. package/src/scripts/_lib/loop_surfaces.ts +382 -0
  221. package/src/scripts/_lib/mission_record.ts +190 -0
  222. package/src/scripts/_lib/obligation_frequency.ts +217 -0
  223. package/src/scripts/_lib/obligations.ts +513 -0
  224. package/src/scripts/_lib/orchestration_record.ts +11 -20
  225. package/src/scripts/_lib/probe_inputs.ts +271 -0
  226. package/src/scripts/_lib/read_surface_scan.ts +322 -0
  227. package/src/scripts/_lib/release_highlights.ts +201 -11
  228. package/src/scripts/_lib/release_holds.ts +493 -0
  229. package/src/scripts/_lib/release_migration_gate.ts +184 -0
  230. package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
  231. package/src/scripts/_lib/rides_along.ts +102 -0
  232. package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
  233. package/src/scripts/_lib/shell_write_shapes.ts +108 -0
  234. package/src/scripts/_lib/structural_hiding.ts +622 -0
  235. package/src/scripts/_lib/test_delta.ts +166 -0
  236. package/src/scripts/_lib/test_provenance.ts +171 -0
  237. package/src/scripts/_lib/turn_end_refusals.ts +525 -4
  238. package/src/scripts/_lib/turn_end_transcript.ts +216 -0
  239. package/src/scripts/_lib/typed_op_grant.ts +246 -0
  240. package/src/scripts/_lib/typed_op_watch.ts +185 -0
  241. package/src/scripts/_lib/verification_command.ts +324 -0
  242. package/src/scripts/_lib/verification_evidence.ts +647 -0
  243. package/src/scripts/_lib/verify_clause.ts +280 -0
  244. package/src/scripts/ai_council/config.ts +8 -11
  245. package/src/scripts/ai_council/fallback_config.ts +5 -3
  246. package/src/scripts/ai_council/ownership_classes.ts +118 -0
  247. package/src/scripts/ai_team/team_dispatch.ts +4 -0
  248. package/src/scripts/ask_block_census.ts +285 -15
  249. package/src/scripts/before_complete_hook.ts +340 -23
  250. package/src/scripts/bench_hook_latency.ts +412 -5
  251. package/src/scripts/cache_realization_report.ts +61 -1
  252. package/src/scripts/check_argument_hint.ts +287 -0
  253. package/src/scripts/check_claims.ts +125 -1
  254. package/src/scripts/check_enforcement_coverage.ts +38 -9
  255. package/src/scripts/check_enforcement_matrix.ts +866 -0
  256. package/src/scripts/check_finding_dispositions.ts +22 -3
  257. package/src/scripts/check_gate_reachability.ts +173 -10
  258. package/src/scripts/check_generator_sync.ts +1025 -0
  259. package/src/scripts/check_host_docs_digest.ts +786 -0
  260. package/src/scripts/check_host_format_column.ts +559 -0
  261. package/src/scripts/check_input_references.ts +228 -0
  262. package/src/scripts/check_inputs_parity.ts +235 -0
  263. package/src/scripts/check_kernel_edit_ratified.ts +47 -5
  264. package/src/scripts/check_kill_switch_table.ts +294 -0
  265. package/src/scripts/check_placeholder_drift.ts +221 -0
  266. package/src/scripts/check_preamble_payload_budget.ts +75 -2
  267. package/src/scripts/check_read_surface_coverage.ts +515 -0
  268. package/src/scripts/check_release_highlights.ts +33 -1
  269. package/src/scripts/check_release_holds.ts +568 -0
  270. package/src/scripts/check_routing_coverage.ts +378 -11
  271. package/src/scripts/check_rule_projection_integrity.ts +22 -12
  272. package/src/scripts/check_static_parity.ts +17 -0
  273. package/src/scripts/check_test_delta.ts +294 -0
  274. package/src/scripts/check_test_weakening.ts +299 -0
  275. package/src/scripts/ci_settle.ts +145 -19
  276. package/src/scripts/closure_scan.ts +447 -0
  277. package/src/scripts/compile_hook_manifest.ts +102 -18
  278. package/src/scripts/council_record_shape.ts +209 -0
  279. package/src/scripts/external_sources_denylist.json +29 -4
  280. package/src/scripts/hook_manifest.json +1 -1
  281. package/src/scripts/hook_manifest.yaml +127 -11
  282. package/src/scripts/hooks/block_config_weakening.ts +229 -4
  283. package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
  284. package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
  285. package/src/scripts/hooks/bundle_integrity.ts +291 -0
  286. package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
  287. package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
  288. package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
  289. package/src/scripts/hooks/concern_registry.ts +6 -0
  290. package/src/scripts/hooks/concern_timings.ts +94 -0
  291. package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
  292. package/src/scripts/hooks/design_pass_hook.ts +221 -5
  293. package/src/scripts/hooks/design_slop_hook.ts +1 -2
  294. package/src/scripts/hooks/dispatch_hook.ts +25 -19
  295. package/src/scripts/hooks/edit_shape_hook.ts +1 -2
  296. package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
  297. package/src/scripts/hooks/evidence_independence.ts +97 -2
  298. package/src/scripts/hooks/exit_codes.ts +128 -0
  299. package/src/scripts/hooks/host_lowering.json +1 -0
  300. package/src/scripts/hooks/host_lowering.ts +128 -3
  301. package/src/scripts/hooks/host_lowering.yaml +262 -46
  302. package/src/scripts/hooks/host_semantics.ts +30 -2
  303. package/src/scripts/hooks/injection_budget.ts +9 -2
  304. package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
  305. package/src/scripts/hooks/journal_record_hook.ts +1 -2
  306. package/src/scripts/hooks/memory_recall_hook.ts +539 -0
  307. package/src/scripts/hooks/obligation_settle_hook.ts +328 -0
  308. package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
  309. package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
  310. package/src/scripts/hooks/permission_gate.ts +1 -2
  311. package/src/scripts/hooks/push_settle_hook.ts +1 -3
  312. package/src/scripts/hooks/replay_hook.ts +2 -1
  313. package/src/scripts/hooks/reread_guard_hook.ts +1 -2
  314. package/src/scripts/hooks/review_baseline_hook.ts +1 -2
  315. package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
  316. package/src/scripts/hooks/rule_inject_hook.ts +55 -4
  317. package/src/scripts/hooks/run_continuation_hook.ts +30 -35
  318. package/src/scripts/hooks/skill_route_hook.ts +1 -2
  319. package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
  320. package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
  321. package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
  322. package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
  323. package/src/scripts/hooks/table_fingerprint.ts +49 -0
  324. package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
  325. package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
  326. package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
  327. package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
  328. package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
  329. package/src/scripts/hooks_doctor.ts +85 -0
  330. package/src/scripts/hooks_status.ts +19 -0
  331. package/src/scripts/install.ts +109 -210
  332. package/src/scripts/janitor.ts +7 -0
  333. package/src/scripts/lint_decision_classes.ts +464 -0
  334. package/src/scripts/lint_deny_text.ts +419 -0
  335. package/src/scripts/lint_exit_codes.ts +262 -0
  336. package/src/scripts/lint_hook_manifest.ts +66 -0
  337. package/src/scripts/lint_major_migration_sections.ts +450 -0
  338. package/src/scripts/lint_plan_risk_register.ts +47 -3
  339. package/src/scripts/lint_roadmap_blockers.ts +139 -12
  340. package/src/scripts/lint_roadmap_producers.ts +202 -0
  341. package/src/scripts/lint_skill_descriptions.ts +1 -1
  342. package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
  343. package/src/scripts/mcp_server/prompts.ts +142 -1
  344. package/src/scripts/measure_rule_budget.ts +101 -5
  345. package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
  346. package/src/scripts/measure_turn_end_gate.ts +193 -0
  347. package/src/scripts/memory_lookup.ts +1 -1
  348. package/src/scripts/new_roadmap.ts +37 -0
  349. package/src/scripts/orchestration_record.ts +0 -5
  350. package/src/scripts/prepack-check.mjs +53 -0
  351. package/src/scripts/release.ts +14 -98
  352. package/src/scripts/release_drill.ts +267 -1
  353. package/src/scripts/release_publication.ts +64 -1
  354. package/src/scripts/report_enforcement_drift.ts +566 -0
  355. package/src/scripts/report_host_injection_effect.ts +3 -11
  356. package/src/scripts/report_invocation_surface.ts +383 -0
  357. package/src/scripts/report_obligation_carriers.ts +699 -0
  358. package/src/scripts/report_obligation_writer_split.ts +236 -0
  359. package/src/scripts/report_skill_menu_census.ts +208 -0
  360. package/src/scripts/report_standing_payload_by_host.ts +26 -152
  361. package/src/scripts/roadmap_verify_share.ts +213 -0
  362. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  363. package/src/scripts/schemas/command.schema.json +48 -0
  364. package/src/scripts/schemas/review-findings.schema.json +4 -0
  365. package/src/scripts/schemas/skill.schema.json +44 -0
  366. package/src/scripts/scope_growth.ts +219 -0
  367. package/src/scripts/self_review_gate.ts +359 -10
  368. package/src/scripts/semantic_parity_census.ts +314 -0
  369. package/src/scripts/session_eol_report.ts +54 -1
  370. package/src/scripts/sweep_dead_scan_roots.ts +1 -1
  371. package/src/scripts/trigger_eval_grandfather.json +0 -3
  372. package/src/scripts/trigger_eval_rotation.ts +122 -27
  373. package/src/scripts/ui_conformance_probe.ts +774 -0
  374. package/src/scripts/update_prices.ts +85 -3
  375. package/src/scripts/write_bundle_digest.mjs +74 -0
  376. package/src/server/schemas/settings.ts +62 -7
  377. package/src/shared/missionExecution.ts +126 -0
  378. package/src/templates/consumer-settings/ONBOARDING.md +1 -0
  379. package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
  380. package/dist/ui/assets/index-D0jaCRPM.js +0 -29
  381. package/dist/ui/assets/index-jSfMW8_t.css +0 -1
  382. /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
@@ -0,0 +1,774 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * `ui_conformance_probe` — behavioural evidence for a built UI surface.
4
+ *
5
+ * Phase 2 of `road-to-behaviour-evidence-over-pixels`. A Playwright consumer
6
+ * that compares a target against a reference on the five dimensions a resting
7
+ * frame cannot contain, and writes `ui-conformance.json`.
8
+ *
9
+ * NOT A GATE. It has no `lint_` / `check_` / `audit_` prefix, emits no
10
+ * `scanned:` line and carries no `src/config/gate-coverage.yml` row, following
11
+ * the `annotate_r1_outcomes.ts` precedent. It is mounted in shadow only: the
12
+ * design-pass hook reads its artefact and reports a verdict that never blocks.
13
+ * Making it a gate is a separate decision with its own six surfaces.
14
+ *
15
+ * THE STRUCTURE GATE IS PER NODE, AND THAT IS THE WHOLE DESIGN.
16
+ * Nodes are keyed by `data-probe-id`. A reference node with no match in the
17
+ * target produces a `structure` finding and is then simply not style-compared;
18
+ * matched nodes are compared normally. The rejected alternative was a global
19
+ * halt — stop the entire run on the first unmatched node. It reads stricter and
20
+ * is worse: a target missing one element would report one finding and hide
21
+ * every behavioural defect behind it, which is precisely the "looked clean
22
+ * because it stopped early" failure. What the gate actually prevents is
23
+ * comparing an unmatched node against whatever sits in its position, because
24
+ * those style findings would be about the wrong element. Per-node matching
25
+ * prevents that by construction, without hiding anything.
26
+ *
27
+ * NO AGGREGATE, ANYWHERE. Counters per dimension, never a single number
28
+ * standing for conformance. A dimension that could not run reports
29
+ * `not_applicable` with a reason and carries `findings: null` — never 0, which
30
+ * would read as "ran and found nothing".
31
+ *
32
+ * TWO CAUSES OF `not_applicable`, NOT ONE. The clause above was written for a
33
+ * host with no browser and then violated by the other empty input:
34
+ * `evaluate()` used to stamp all five dimensions `exercised` with `findings: 0`
35
+ * and return `structure_gate: 'passed'` for a reference that collected ZERO
36
+ * nodes — which is what every design handover in this tree produces, since none
37
+ * carries a `data-probe-id`. A comparison that never happened reported as a
38
+ * clean one. Since `road-to-a-probe-that-cannot-report-a-false-green` an empty
39
+ * reference takes the not-applicable path too, and its reason names the
40
+ * HANDOVER rather than the host, so the two causes stay tellable apart in the
41
+ * one field a reader looks at.
42
+ *
43
+ * THE ARTEFACT CARRIES ITS DENOMINATOR. `reference_nodes` and `target_nodes`
44
+ * are how many handled nodes each side actually held — `null` where nothing was
45
+ * observed at all, on the same never-0 contract as `findings`. Without them a
46
+ * zero-node run and a fully-matched run were indistinguishable in the JSON, so
47
+ * the cause above was readable only from prose.
48
+ *
49
+ * STRUCTURE IS SYMMETRIC. A handle present only in the target is a finding
50
+ * (`expected: 'absent'`, `observed: 'present'`), mirroring the reference-side
51
+ * one. `design-fidelity`'s Iron Law is "never omit or add an element" and the
52
+ * add half was unobservable while the loop read `reference.nodes` alone; a
53
+ * rename consequently reported one finding where it now reports two. The
54
+ * finding keys on the handle, never on the DOM node, so the wrapper and portal
55
+ * nodes a legitimate framework port inserts produce nothing.
56
+ *
57
+ * WHAT THIS DOES NOT FIX, said plainly because the fix is easy to over-read:
58
+ * no design handover in this tree carries a `data-probe-id` at all, so every
59
+ * real run still lands in the not-applicable branch. What changed is that the
60
+ * run now SAYS so. The correspondence problem — getting handles into a handover
61
+ * — is untouched, and nothing here promotes the probe out of shadow.
62
+ *
63
+ * THE NOISE CONTROL. Computed-style comparison over every property fires on
64
+ * font fallbacks and token indirection nobody cares about, so the compared set
65
+ * is curated (`COMPARED_PROPERTIES`) to the properties a design token actually
66
+ * lands in. A deviation that is intended is recorded in a sibling
67
+ * `conformance.declared.json` and suppressed with its reason retained, so an
68
+ * approved change is not re-litigated on every run.
69
+ *
70
+ * THE ARTEFACT KNOWS WHAT IT READ. `generated_at` alone made a run before the
71
+ * last UI edit indistinguishable from one after it, so every artefact carries an
72
+ * `inputs` block: a content digest of the target surface, of the reference, and
73
+ * of the declarations file, plus the probe build that wrote them. The design-pass
74
+ * reader recomputes those digests and reports `stale` when they moved — a file
75
+ * comparison, no browser. The vocabulary is shared rather than duplicated
76
+ * (`_lib/probe_inputs.ts`), because a producer and a consumer with separate
77
+ * copies of a hash rule agree only until one of them is edited.
78
+ *
79
+ * Exit codes: 0 written (with or without findings) · 1 usage error.
80
+ */
81
+ import fs from 'node:fs';
82
+ import { createRequire } from 'node:module';
83
+ import path from 'node:path';
84
+ import { fileURLToPath, pathToFileURL } from 'node:url';
85
+
86
+ import { collectInputs, declarationsPathFor, type ProbeInputs } from './_lib/probe_inputs.js';
87
+
88
+ // Type-only, and that is load-bearing rather than stylistic: these are erased
89
+ // at compile time, so they add no second runtime path to a module that refuses
90
+ // to be loaded twice (see `loadPlaywright`).
91
+ import type * as PlaywrightTest from '@playwright/test';
92
+ type Page = PlaywrightTest.Page;
93
+
94
+ export const DIMENSIONS = [
95
+ 'structure',
96
+ 'computed_style',
97
+ 'interaction',
98
+ 'viewport_matrix',
99
+ 'media_emulation',
100
+ ] as const;
101
+ export type Dimension = (typeof DIMENSIONS)[number];
102
+
103
+ /**
104
+ * Curated, not exhaustive. Every entry is a property a design token resolves
105
+ * into or a layout property a breakpoint changes; nothing here fires on a font
106
+ * fallback or on inherited text metrics.
107
+ */
108
+ export const COMPARED_PROPERTIES = [
109
+ 'background-color',
110
+ 'color',
111
+ 'font-size',
112
+ 'line-height',
113
+ 'padding-top',
114
+ 'padding-left',
115
+ 'display',
116
+ 'flex-direction',
117
+ 'align-items',
118
+ 'gap',
119
+ 'outline-color',
120
+ 'outline-width',
121
+ 'transition-duration',
122
+ ] as const;
123
+
124
+ /** Widest first. A divergence is attributed to the first width it appears at. */
125
+ export const VIEWPORTS = [1440, 768, 375, 320] as const;
126
+
127
+ export const INTERACTION_STATES = ['hover', 'focus', 'active', 'keyboard'] as const;
128
+
129
+ export type StyleMap = Record<string, string>;
130
+
131
+ export interface NodeObservation {
132
+ probe_id: string;
133
+ base: StyleMap;
134
+ hover: StyleMap;
135
+ focus: StyleMap;
136
+ active: StyleMap;
137
+ keyboard: StyleMap;
138
+ reduced_motion: StyleMap;
139
+ viewports: Record<string, StyleMap>;
140
+ /**
141
+ * A serialization of what a click did to the document, or null where the
142
+ * node is not clickable. Comparing the EFFECT rather than the presence of a
143
+ * listener is the point: a registered handler that mutates nothing and a
144
+ * missing handler are the same defect to a user.
145
+ */
146
+ click_effect: string | null;
147
+ }
148
+
149
+ export interface Observation {
150
+ variant: string;
151
+ source: string;
152
+ nodes: NodeObservation[];
153
+ }
154
+
155
+ export interface Declaration {
156
+ probe_id: string;
157
+ dimension: Dimension;
158
+ property: string;
159
+ reason: string;
160
+ }
161
+
162
+ export interface Finding {
163
+ dimension: Dimension;
164
+ probe_id: string;
165
+ /** The interaction state, the viewport width, or null for a flat compare. */
166
+ state: string | null;
167
+ property: string | null;
168
+ expected: string;
169
+ observed: string;
170
+ detail: string;
171
+ /** Viewport dimension only: every width at which the divergence holds. */
172
+ widths?: string[];
173
+ }
174
+
175
+ export interface DimensionRow {
176
+ dimension: Dimension;
177
+ status: 'exercised' | 'not_applicable';
178
+ /** `null` on a not-applicable row. Never 0 — see the header. */
179
+ findings: number | null;
180
+ reason?: string;
181
+ }
182
+
183
+ export interface SuppressedDeviation {
184
+ probe_id: string;
185
+ property: string;
186
+ reason: string;
187
+ expected: string;
188
+ observed: string;
189
+ }
190
+
191
+ export interface ProbeArtefact {
192
+ schema: 'ui-conformance/v1';
193
+ generated_at: string;
194
+ /**
195
+ * What this run read, by content. Beside `generated_at` rather than instead
196
+ * of it: the timestamp says when, the digests say against what — and only
197
+ * the second question can be re-asked later.
198
+ */
199
+ inputs: ProbeInputs;
200
+ host_class: 'A' | 'B' | 'C' | 'unknown';
201
+ target: string;
202
+ reference: string | null;
203
+ /**
204
+ * The denominator each side actually had. `null` where nothing was observed
205
+ * at all — the same contract `findings` carries, and for the same reason: a
206
+ * 0 would read as "looked and saw none", which the no-browser path did not.
207
+ */
208
+ reference_nodes: number | null;
209
+ target_nodes: number | null;
210
+ structure_gate: 'passed' | 'stopped';
211
+ /** Reference-side only: the nodes whose STYLE comparison was skipped. */
212
+ unmatched_nodes: string[];
213
+ dimensions: DimensionRow[];
214
+ findings: Finding[];
215
+ declared_suppressed: SuppressedDeviation[];
216
+ }
217
+
218
+ // ---------------------------------------------------------------- pure core
219
+
220
+ export function readObservation(file: string): Observation {
221
+ return JSON.parse(fs.readFileSync(file, 'utf-8')) as Observation;
222
+ }
223
+
224
+ export function loadDeclarations(file: string): Declaration[] {
225
+ try {
226
+ const raw = JSON.parse(fs.readFileSync(file, 'utf-8')) as { declared?: Declaration[] };
227
+ return raw.declared ?? [];
228
+ } catch {
229
+ // A missing declaration file means nothing is declared, which is the
230
+ // strict reading. It is never an error: most targets declare nothing.
231
+ return [];
232
+ }
233
+ }
234
+
235
+ const isDeclared = (d: readonly Declaration[], probeId: string, dimension: Dimension, property: string): Declaration | undefined =>
236
+ d.find((x) => x.probe_id === probeId && x.dimension === dimension && x.property === property);
237
+
238
+ /** Did driving this state change anything, relative to the given baseline? */
239
+ function stateDelta(base: StyleMap, state: StyleMap): string[] {
240
+ return COMPARED_PROPERTIES.filter((p) => base[p] !== state[p]);
241
+ }
242
+
243
+ /**
244
+ * Which reading each state is measured AGAINST, and `active` is the interesting
245
+ * one. A pointer cannot press an element without hovering it first, so the
246
+ * `:active` reading always carries the `:hover` styles too. Measured against
247
+ * `base`, removing a single hover rule therefore surfaces as TWO findings —
248
+ * hover lost and active lost — which counts one defect twice and was exactly
249
+ * what the fixture caught on the first run. Measuring `active` against `hover`
250
+ * isolates what pressing actually contributes, and a design that darkens
251
+ * further on press still reports correctly.
252
+ */
253
+ const STATE_BASELINE: Record<(typeof INTERACTION_STATES)[number], 'base' | 'hover'> = {
254
+ hover: 'base',
255
+ active: 'hover',
256
+ focus: 'base',
257
+ keyboard: 'base',
258
+ };
259
+
260
+ /**
261
+ * The second cause of `not_applicable`, and it is deliberately worded to be
262
+ * unmistakable for the first.
263
+ *
264
+ * A handover carrying no `data-probe-id` and a host carrying no browser both
265
+ * end in five not-applicable rows, so the reason string is the one field a
266
+ * reader can tell them apart by. An operator who reads "no browser" goes
267
+ * looking for a missing binary instead of fixing the handover — so this text
268
+ * names the handover and never the host, and `reference_nodes: 0` makes the
269
+ * same cause readable from the data without the prose being read at all.
270
+ */
271
+ const EMPTY_REFERENCE_REASON =
272
+ 'the reference observation carries no data-probe-id handles, so there was nothing to ' +
273
+ 'compare against. The capture ran and collected zero nodes: this is a property of the ' +
274
+ 'handover, not of this machine.';
275
+
276
+ export function evaluate(
277
+ reference: Observation,
278
+ target: Observation,
279
+ declarations: readonly Declaration[],
280
+ ): ProbeArtefact {
281
+ // AN EMPTY COMPARISON IS NOT A CLEAN ONE.
282
+ // Keyed on the REFERENCE only, and that asymmetry is the whole point: a
283
+ // target that collected nothing is a real comparison in which every node is
284
+ // missing, and four real structure findings must not be collapsed into a
285
+ // shrug. A reference that collected nothing has no question to ask, so no
286
+ // dimension was entered and none may report a count.
287
+ if (reference.nodes.length === 0) {
288
+ return {
289
+ schema: 'ui-conformance/v1',
290
+ generated_at: new Date().toISOString(),
291
+ host_class: resolveHostClass(),
292
+ target: target.source,
293
+ reference: reference.source,
294
+ reference_nodes: 0,
295
+ target_nodes: target.nodes.length,
296
+ // The empty-reference path READ both sources — it found no handles
297
+ // in the reference, which is a property of the handover, not a
298
+ // failure to look. So it records the same digests the normal path
299
+ // does; omitting them would make a stopped run indistinguishable
300
+ // from one whose inputs were never seen.
301
+ inputs: collectInputs(target.source, reference.source),
302
+ structure_gate: 'stopped',
303
+ unmatched_nodes: [],
304
+ dimensions: DIMENSIONS.map((d) => ({
305
+ dimension: d,
306
+ status: 'not_applicable' as const,
307
+ findings: null,
308
+ reason: `${d} was never entered: ${EMPTY_REFERENCE_REASON}`,
309
+ })),
310
+ findings: [],
311
+ declared_suppressed: [],
312
+ };
313
+ }
314
+
315
+ const targetById = new Map(target.nodes.map((n) => [n.probe_id, n]));
316
+ const findings: Finding[] = [];
317
+ const suppressed: SuppressedDeviation[] = [];
318
+ const unmatched: string[] = [];
319
+
320
+ for (const ref of reference.nodes) {
321
+ const got = targetById.get(ref.probe_id);
322
+ if (!got) {
323
+ unmatched.push(ref.probe_id);
324
+ findings.push({
325
+ dimension: 'structure',
326
+ probe_id: ref.probe_id,
327
+ state: null,
328
+ property: null,
329
+ expected: 'present',
330
+ observed: 'absent',
331
+ detail:
332
+ `no node carries data-probe-id="${ref.probe_id}" in the target. ` +
333
+ 'Style comparison is skipped for this node rather than run against ' +
334
+ 'whatever occupies its position.',
335
+ });
336
+ continue;
337
+ }
338
+
339
+ // --- computed_style, at the base viewport
340
+ for (const p of COMPARED_PROPERTIES) {
341
+ if (ref.base[p] === got.base[p]) continue;
342
+ const declared = isDeclared(declarations, ref.probe_id, 'computed_style', p);
343
+ if (declared) {
344
+ suppressed.push({
345
+ probe_id: ref.probe_id,
346
+ property: p,
347
+ reason: declared.reason,
348
+ expected: ref.base[p] ?? '',
349
+ observed: got.base[p] ?? '',
350
+ });
351
+ continue;
352
+ }
353
+ findings.push({
354
+ dimension: 'computed_style',
355
+ probe_id: ref.probe_id,
356
+ state: null,
357
+ property: p,
358
+ expected: ref.base[p] ?? '',
359
+ observed: got.base[p] ?? '',
360
+ detail: `resolved ${p} differs at the base viewport`,
361
+ });
362
+ }
363
+
364
+ // --- interaction: a state the reference reacts to and the target does not
365
+ for (const state of INTERACTION_STATES) {
366
+ const baseline = STATE_BASELINE[state];
367
+ const refDelta = stateDelta(baseline === 'hover' ? ref.hover : ref.base, ref[state]);
368
+ const gotDelta = stateDelta(baseline === 'hover' ? got.hover : got.base, got[state]);
369
+ const lost = refDelta.filter((p) => !gotDelta.includes(p));
370
+ if (!lost.length) continue;
371
+ if (isDeclared(declarations, ref.probe_id, 'interaction', lost[0]!)) continue;
372
+ findings.push({
373
+ dimension: 'interaction',
374
+ probe_id: ref.probe_id,
375
+ state,
376
+ property: lost[0]!,
377
+ expected: `${state} changes ${lost.join(', ')}`,
378
+ observed: gotDelta.length ? `${state} changes ${gotDelta.join(', ')}` : `${state} changes nothing`,
379
+ detail:
380
+ `the reference responds to ${state} on this node and the target does not. ` +
381
+ 'No resting capture at any viewport contains this difference.',
382
+ });
383
+ }
384
+
385
+ // --- interaction: a handler whose effect disappeared
386
+ if (ref.click_effect !== null && ref.click_effect !== got.click_effect) {
387
+ findings.push({
388
+ dimension: 'interaction',
389
+ probe_id: ref.probe_id,
390
+ state: 'click',
391
+ property: null,
392
+ expected: ref.click_effect,
393
+ observed: got.click_effect ?? 'no effect',
394
+ detail:
395
+ 'clicking this node changes the document in the reference and does not in ' +
396
+ 'the target. The element renders identically in both.',
397
+ });
398
+ }
399
+
400
+ // --- viewport_matrix: ONE finding per breakpoint row, not per property.
401
+ // A single removed media rule usually changes several properties at once
402
+ // (this fixture's changes both `flex-direction` and `align-items`), and
403
+ // reporting one finding each would count one defect several times and
404
+ // make "exactly the matching breakpoint row is red" untestable. The row
405
+ // is the unit a reader acts on: this node does not change at this width.
406
+ const perWidth = new Map<string, string[]>();
407
+ for (const w of VIEWPORTS) {
408
+ const key = String(w);
409
+ const refV = ref.viewports[key];
410
+ const gotV = got.viewports[key];
411
+ if (!refV || !gotV) continue;
412
+ const diverging = COMPARED_PROPERTIES.filter(
413
+ (p) =>
414
+ refV[p] !== gotV[p] &&
415
+ // A divergence already visible at the base viewport is a flat
416
+ // style difference, not a breakpoint one. Reporting it in both
417
+ // dimensions would count one defect twice.
418
+ ref.base[p] === got.base[p] &&
419
+ !isDeclared(declarations, ref.probe_id, 'viewport_matrix', p),
420
+ );
421
+ if (diverging.length) perWidth.set(key, diverging);
422
+ }
423
+ if (perWidth.size) {
424
+ const widths = [...perWidth.keys()];
425
+ const first = widths[0]!;
426
+ const properties = perWidth.get(first)!;
427
+ findings.push({
428
+ dimension: 'viewport_matrix',
429
+ probe_id: ref.probe_id,
430
+ state: first,
431
+ property: properties.join(', '),
432
+ expected: properties.map((p) => `${p}: ${ref.viewports[first]?.[p] ?? ''}`).join('; '),
433
+ observed: properties.map((p) => `${p}: ${got.viewports[first]?.[p] ?? ''}`).join('; '),
434
+ detail:
435
+ `this node's layout diverges at ${widths.join('px, ')}px while matching at the base ` +
436
+ 'viewport — a breakpoint the target does not cross.',
437
+ widths,
438
+ });
439
+ }
440
+
441
+ // --- media_emulation
442
+ const refMotion = stateDelta(ref.base, ref.reduced_motion);
443
+ const gotMotion = stateDelta(got.base, got.reduced_motion);
444
+ const lostMotion = refMotion.filter((p) => !gotMotion.includes(p));
445
+ if (lostMotion.length && !isDeclared(declarations, ref.probe_id, 'media_emulation', lostMotion[0]!)) {
446
+ findings.push({
447
+ dimension: 'media_emulation',
448
+ probe_id: ref.probe_id,
449
+ state: 'reduced-motion',
450
+ property: lostMotion[0]!,
451
+ expected: `reduced motion changes ${lostMotion.join(', ')}`,
452
+ observed: 'reduced motion changes nothing',
453
+ detail: 'the reference presents a reduced-motion alternative on this node and the target does not',
454
+ });
455
+ }
456
+ }
457
+
458
+ // --- structure, from the TARGET side. `design-fidelity`'s Iron Law is
459
+ // "never omit or add an element" and only the omit half was observable: the
460
+ // loop above reads `reference.nodes`, so a handle present only in the target
461
+ // reached nothing.
462
+ //
463
+ // KEYED ON THE HANDLE, NEVER ON THE DOM NODE, and that is what keeps this
464
+ // from reddening correct work. A faithful port into a component framework
465
+ // inserts wrapper and portal nodes the handover never had; none of them
466
+ // carries a `data-probe-id`, so none of them is observed here. Excluding the
467
+ // false-positive class by construction, rather than by a heuristic, is the
468
+ // difference between a dimension that gets acted on and one that gets muted.
469
+ const referenceIds = new Set(reference.nodes.map((n) => n.probe_id));
470
+ const addedIds = target.nodes
471
+ .map((n) => n.probe_id)
472
+ .filter((id) => !referenceIds.has(id))
473
+ .sort();
474
+ for (const id of addedIds) {
475
+ findings.push({
476
+ dimension: 'structure',
477
+ probe_id: id,
478
+ state: null,
479
+ property: null,
480
+ expected: 'absent',
481
+ observed: 'present',
482
+ detail:
483
+ `a node carries data-probe-id="${id}" in the target and no node carries it in ` +
484
+ 'the reference. There is nothing to compare its style against, so it is reported ' +
485
+ 'and then left alone — the mirror of the missing-node case above.',
486
+ });
487
+ }
488
+
489
+ const dimensions: DimensionRow[] = DIMENSIONS.map((d) => ({
490
+ dimension: d,
491
+ status: 'exercised',
492
+ findings: findings.filter((f) => f.dimension === d).length,
493
+ }));
494
+
495
+ return {
496
+ schema: 'ui-conformance/v1',
497
+ generated_at: new Date().toISOString(),
498
+ inputs: collectInputs(target.source, reference.source),
499
+ host_class: resolveHostClass(),
500
+ target: target.source,
501
+ reference: reference.source,
502
+ reference_nodes: reference.nodes.length,
503
+ target_nodes: target.nodes.length,
504
+ // Both directions stop the gate, because both describe a node that was
505
+ // never style-compared. `unmatched_nodes` keeps its narrower meaning —
506
+ // the reference-side nodes whose comparison was SKIPPED — so a reader of
507
+ // that list still learns exactly what it always said.
508
+ structure_gate: unmatched.length || addedIds.length ? 'stopped' : 'passed',
509
+ unmatched_nodes: unmatched,
510
+ dimensions,
511
+ findings,
512
+ declared_suppressed: suppressed,
513
+ };
514
+ }
515
+
516
+ /**
517
+ * The artefact a host that cannot run the probe must still produce. Emitting
518
+ * nothing is indistinguishable from a clean run to everything downstream.
519
+ */
520
+ export function unavailableArtefact(target: string, reason: string, reference: string | null = null): ProbeArtefact {
521
+ return {
522
+ schema: 'ui-conformance/v1',
523
+ generated_at: new Date().toISOString(),
524
+ // A degraded run still records what it WOULD have compared. An artefact
525
+ // with no inputs block reads as a version skew to the reader, which is a
526
+ // different and less useful statement than "this host has no browser".
527
+ inputs: collectInputs(target, reference),
528
+ host_class: 'unknown',
529
+ target,
530
+ reference: null,
531
+ // Null, never 0 — nothing was observed on either side, and a 0 would
532
+ // read as "looked and saw none".
533
+ reference_nodes: null,
534
+ target_nodes: null,
535
+ structure_gate: 'stopped',
536
+ unmatched_nodes: [],
537
+ dimensions: DIMENSIONS.map((d) => ({
538
+ dimension: d,
539
+ status: 'not_applicable' as const,
540
+ findings: null,
541
+ reason: `${d} needs a browser engine this host did not provide: ${reason}`,
542
+ })),
543
+ findings: [],
544
+ declared_suppressed: [],
545
+ };
546
+ }
547
+
548
+ export function resolveHostClass(): ProbeArtefact['host_class'] {
549
+ if (!chromiumAvailable()) return 'unknown';
550
+ return process.env['CI'] ? 'C' : 'A';
551
+ }
552
+
553
+ // ------------------------------------------------------------ browser layer
554
+
555
+ /**
556
+ * ONE loader, memoized, and that is not a style preference. Playwright throws
557
+ * `Requiring @playwright/test second time` when the same process reaches it
558
+ * through both CJS `require` and ESM `import`, so the availability probe and
559
+ * the capture must share a single path. `createRequire` rather than a bare
560
+ * `require`, because this file is ESM and a bare `require` is undefined
561
+ * under tsx.
562
+ */
563
+ let playwrightModule: typeof PlaywrightTest | null | undefined;
564
+ function loadPlaywright(): typeof PlaywrightTest | null {
565
+ if (playwrightModule !== undefined) return playwrightModule;
566
+ try {
567
+ const req = createRequire(import.meta.url);
568
+ playwrightModule = req('@playwright/test') as typeof PlaywrightTest;
569
+ } catch {
570
+ playwrightModule = null;
571
+ }
572
+ return playwrightModule;
573
+ }
574
+
575
+ export function chromiumAvailable(): boolean {
576
+ try {
577
+ // Resolving the module is not enough — the binaries are a separate
578
+ // install, and this package declares the runner without installing them.
579
+ const pw = loadPlaywright();
580
+ const exe = pw?.chromium?.executablePath?.();
581
+ return Boolean(exe && fs.existsSync(exe));
582
+ } catch {
583
+ return false;
584
+ }
585
+ }
586
+
587
+ const READ_STYLES = (el: Element, props: readonly string[]): Record<string, string> => {
588
+ const cs = getComputedStyle(el);
589
+ const out: Record<string, string> = {};
590
+ for (const p of props) out[p] = cs.getPropertyValue(p).trim();
591
+ return out;
592
+ };
593
+
594
+ export async function captureVariant(indexHtml: string): Promise<Observation> {
595
+ const pw = loadPlaywright();
596
+ if (!pw) throw new Error('captureVariant: @playwright/test is not resolvable on this host');
597
+ const browser = await pw.chromium.launch();
598
+ const url = pathToFileURL(indexHtml).href;
599
+ const props = [...COMPARED_PROPERTIES];
600
+ const nodes: NodeObservation[] = [];
601
+
602
+ try {
603
+ const page = await browser.newPage({ viewport: { width: VIEWPORTS[0], height: 900 } });
604
+ await page.goto(url);
605
+ const ids = await page.$$eval('[data-probe-id]', (els) =>
606
+ els.map((e) => e.getAttribute('data-probe-id') ?? ''),
607
+ );
608
+
609
+ for (const id of ids.filter(Boolean).sort()) {
610
+ const sel = `[data-probe-id="${id}"]`;
611
+ await page.setViewportSize({ width: VIEWPORTS[0], height: 900 });
612
+ await page.emulateMedia({ reducedMotion: 'no-preference' });
613
+ await page.reload();
614
+
615
+ const base = await page.$eval(sel, READ_STYLES, props);
616
+
617
+ await page.hover(sel).catch(() => undefined);
618
+ const hover = await page.$eval(sel, READ_STYLES, props);
619
+ await page.mouse.move(0, 0);
620
+
621
+ await page.$eval(sel, (el) => (el as HTMLElement).focus?.());
622
+ const focus = await page.$eval(sel, READ_STYLES, props);
623
+ await page.$eval(sel, (el) => (el as HTMLElement).blur?.());
624
+
625
+ const box = await page.$eval(sel, (el) => {
626
+ const r = el.getBoundingClientRect();
627
+ return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
628
+ });
629
+ await page.mouse.move(box.x, box.y);
630
+ await page.mouse.down();
631
+ const active = await page.$eval(sel, READ_STYLES, props);
632
+ await page.mouse.up();
633
+ await page.reload();
634
+
635
+ const keyboard = await captureKeyboardState(page, id, sel, props);
636
+
637
+ await page.emulateMedia({ reducedMotion: 'reduce' });
638
+ const reducedMotion = await page.$eval(sel, READ_STYLES, props);
639
+ await page.emulateMedia({ reducedMotion: 'no-preference' });
640
+
641
+ const viewports: Record<string, StyleMap> = {};
642
+ for (const w of VIEWPORTS) {
643
+ await page.setViewportSize({ width: w, height: 900 });
644
+ viewports[String(w)] = await page.$eval(sel, READ_STYLES, props);
645
+ }
646
+ await page.setViewportSize({ width: VIEWPORTS[0], height: 900 });
647
+
648
+ const clickEffect = await captureClickEffect(page, sel);
649
+
650
+ nodes.push({
651
+ probe_id: id,
652
+ base,
653
+ hover,
654
+ focus,
655
+ active,
656
+ keyboard,
657
+ reduced_motion: reducedMotion,
658
+ viewports,
659
+ click_effect: clickEffect,
660
+ });
661
+ }
662
+ await page.close();
663
+ } finally {
664
+ await browser.close();
665
+ }
666
+
667
+ return { variant: path.basename(path.dirname(indexHtml)), source: indexHtml, nodes };
668
+ }
669
+
670
+ /**
671
+ * Keyboard focus, reached by actually pressing Tab. `element.focus()` does not
672
+ * set `:focus-visible` in Chromium when the last input was not a keyboard, so a
673
+ * programmatic focus would silently report the focus ring as absent.
674
+ */
675
+ async function captureKeyboardState(
676
+ page: Page,
677
+ id: string,
678
+ sel: string,
679
+ props: readonly string[],
680
+ ): Promise<StyleMap> {
681
+ await page.reload();
682
+ for (let i = 0; i < 12; i += 1) {
683
+ await page.keyboard.press('Tab');
684
+ const hit = await page.evaluate(
685
+ (probeId) => document.activeElement?.getAttribute('data-probe-id') === probeId,
686
+ id,
687
+ );
688
+ if (hit) break;
689
+ }
690
+ const styles = await page.$eval(sel, READ_STYLES, props);
691
+ await page.reload();
692
+ return styles;
693
+ }
694
+
695
+ /**
696
+ * Click, then serialize what CHANGED about the document — a delta, never an
697
+ * absolute snapshot.
698
+ *
699
+ * The absolute form was the first implementation and it was wrong in a way the
700
+ * fixture caught: the snapshot listed every `data-probe-id` on the page, so
701
+ * renaming ONE unrelated element changed the recorded click effect of EVERY
702
+ * node, and the renamed variant produced a spurious interaction finding on a
703
+ * button whose handler was untouched. A delta is scoped to the nodes the click
704
+ * actually moved, so an unrelated rename is invisible here and stays the
705
+ * structure dimension's business.
706
+ *
707
+ * A node whose click changes nothing reports null — the correct reading for a
708
+ * paragraph, and the defect reading for a button.
709
+ */
710
+ async function captureClickEffect(page: Page, sel: string): Promise<string | null> {
711
+ const snapshot = (): Promise<Record<string, string>> =>
712
+ page.evaluate(() =>
713
+ Object.fromEntries(
714
+ [...document.querySelectorAll('[data-probe-id]')].map((e) => [
715
+ e.getAttribute('data-probe-id') ?? '',
716
+ `${e.hasAttribute('hidden') ? 'hidden' : 'shown'}/${e.getAttribute('aria-expanded') ?? '-'}`,
717
+ ]),
718
+ ),
719
+ );
720
+ await page.reload();
721
+ const before = await snapshot();
722
+ await page.click(sel, { timeout: 2000 }).catch(() => undefined);
723
+ const after = await snapshot();
724
+ await page.reload();
725
+
726
+ const changed = Object.keys({ ...before, ...after })
727
+ .filter((id) => before[id] !== after[id])
728
+ .sort()
729
+ .map((id) => `${id}: ${before[id] ?? 'absent'} -> ${after[id] ?? 'absent'}`);
730
+ return changed.length ? changed.join('; ') : null;
731
+ }
732
+
733
+ // -------------------------------------------------------------------- CLI
734
+
735
+ const arg = (name: string): string | undefined => {
736
+ const i = process.argv.indexOf(`--${name}`);
737
+ return i >= 0 ? process.argv[i + 1] : undefined;
738
+ };
739
+
740
+ async function main(): Promise<number> {
741
+ const target = arg('target');
742
+ if (!target) {
743
+ process.stderr.write('usage: ui_conformance_probe --target <index.html> [--reference <index.html>] [--out <file>]\n');
744
+ return 1;
745
+ }
746
+ const out = arg('out') ?? path.join('agents', 'runtime', 'state', 'ui-conformance.json');
747
+ const referencePath = arg('reference');
748
+
749
+ let artefact: ProbeArtefact;
750
+ if (!chromiumAvailable()) {
751
+ artefact = unavailableArtefact(target, 'no Chromium binary resolved from @playwright/test', referencePath ?? null);
752
+ } else if (!referencePath) {
753
+ artefact = unavailableArtefact(target, 'no --reference given; a comparison needs an intended implementation');
754
+ for (const row of artefact.dimensions) {
755
+ row.reason = `${row.dimension} needs a --reference to compare against; none was given`;
756
+ }
757
+ } else {
758
+ artefact = evaluate(await captureVariant(referencePath), await captureVariant(target), loadDeclarations(declarationsPathFor(target)));
759
+ }
760
+
761
+ fs.mkdirSync(path.dirname(out), { recursive: true });
762
+ fs.writeFileSync(out, `${JSON.stringify(artefact, null, 2)}\n`);
763
+ for (const row of artefact.dimensions) {
764
+ const count = row.findings === null ? `not-applicable — ${row.reason ?? ''}` : String(row.findings);
765
+ process.stdout.write(`${row.dimension}: ${count}\n`);
766
+ }
767
+ process.stdout.write(`structure_gate: ${artefact.structure_gate}\n`);
768
+ return 0;
769
+ }
770
+
771
+ export const _HERE = fileURLToPath(import.meta.url);
772
+ if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
773
+ main().then((c) => process.exit(c));
774
+ }