@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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +289 -140
- package/CONTRIBUTING.md +9 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/analyze/inbox.md +26 -0
- package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
- package/dist/agent-src/commands/challenge-me/closure.md +178 -0
- package/dist/agent-src/commands/challenge-me.md +9 -2
- package/dist/agent-src/commands/council/default.md +25 -5
- package/dist/agent-src/commands/feature/plan.md +28 -1
- package/dist/agent-src/commands/feature/roadmap.md +28 -1
- package/dist/agent-src/commands/implement-ticket.md +27 -0
- package/dist/agent-src/commands/jira-ticket.md +27 -0
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/pr/merge.md +60 -9
- package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
- package/dist/agent-src/commands/roadmap/create.md +74 -27
- package/dist/agent-src/commands/roadmap/materialize.md +27 -0
- package/dist/agent-src/commands/roadmap/process-full.md +123 -18
- package/dist/agent-src/commands/team.md +22 -0
- package/dist/agent-src/commands/work.md +4 -0
- package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
- package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +47 -0
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +19 -23
- package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
- package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
- package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
- package/dist/agent-src/rules/autonomous-execution.md +15 -8
- package/dist/agent-src/rules/delegation-policy.md +23 -45
- package/dist/agent-src/rules/design-fidelity.md +32 -37
- package/dist/agent-src/rules/evaluator-independence.md +43 -49
- package/dist/agent-src/rules/test-first.md +41 -0
- package/dist/agent-src/rules/ui-audit-gate.md +1 -2
- package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
- package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
- package/dist/agent-src/skills/database/data/manifest.json +4 -4
- package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
- package/dist/agent-src/skills/design-review/SKILL.md +80 -4
- package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +30 -5
- package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
- package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
- package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
- package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
- package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
- package/dist/agent-src/templates/agent-settings.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +218 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/dist/cli/python/knowledge_ingest.js +1 -1
- package/dist/cli/python/workspace_drive.js +1 -1
- package/dist/cli-delegate/check_update_banner.js +40 -2
- package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
- package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
- package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
- package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
- package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
- package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-7XGBX6ZH.js} +973 -555
- package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
- package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-FP36SWJD.js} +587 -141
- package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-IGD6ERX6.js} +62 -7
- package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-JNMVLWLR.js} +99 -14
- package/dist/cli-delegate/{chunk-H32DGUJH.js → chunk-P6ZHP5W2.js} +1 -1
- package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
- package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
- package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
- package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
- package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +985 -831
- package/dist/cli-delegate/cmd_doctor.js +7 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +9 -7
- package/dist/cli-delegate/cmd_fleet.js +4 -2
- package/dist/cli-delegate/cmd_handoff.js +7 -7
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +4 -2
- package/dist/cli-delegate/cmd_prune.js +10 -8
- package/dist/cli-delegate/cmd_route_audit.js +3 -3
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +28 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +10 -8
- package/dist/cli-delegate/cmd_uninstall.js +16 -13
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +12 -10
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +90 -43
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +7682 -2772
- package/dist/hooks/dispatch.sha256 +1 -0
- package/dist/install/claudeRuleRewrite.js +14 -3
- package/dist/install/conflict.js +60 -10
- package/dist/install/conflictTracker.js +218 -0
- package/dist/install/fsPrimitives.js +54 -0
- package/dist/install/install.mjs +2200 -1559
- package/dist/install/manifestFiles.js +259 -0
- package/dist/install/preflight.js +14 -2
- package/dist/install/preserve.js +157 -0
- package/dist/install/recordedOwnership.js +132 -0
- package/dist/install/txlog.js +11 -2
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +3066 -1470
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +3 -2
- package/dist/scripts/_lib/fs_atomic.js +167 -0
- package/dist/scripts/_lib/installed_tools.js +460 -0
- package/dist/scripts/_lib/lean_projection_mode.js +3 -2
- package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
- package/dist/server/routes/install.js +8 -0
- package/dist/server/routes/wizard.js +9 -3
- package/dist/server/schemas/settings.js +40 -5
- package/dist/shared/missionExecution.js +97 -0
- package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
- package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/docs/MIGRATION.md +103 -0
- package/docs/contracts/settings-classes.md +28 -7
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/docs/guidelines/design-fidelity-routing.md +29 -0
- package/package.json +4 -4
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
- package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
- package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
- package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
- package/src/config/agent-settings.template.yml +168 -27
- package/src/config/auto-rule-budget.json +16 -0
- package/src/config/ci-local-parity.yml +36 -5
- package/src/config/continuity-surface.json +23 -2
- package/src/config/gate-coverage.yml +377 -13
- package/src/config/gate-violation-baselines.json +10 -6
- package/src/config/hook-latency-budget.json +21 -0
- package/src/config/hook-token-budget.json +4 -2
- package/src/config/loop-surfaces.yaml +215 -0
- package/src/config/metric-registry.yml +114 -0
- package/src/config/placeholder-drift-budget.json +10 -0
- package/src/config/platform-anchor.json +3 -2
- package/src/config/release-gate-locality.yml +11 -0
- package/src/config/routing-coverage-seed.json +33 -5
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/install/claudeRuleRewrite.ts +16 -2
- package/src/install/conflict.ts +65 -9
- package/src/install/conflictTracker.ts +299 -0
- package/src/install/fsPrimitives.ts +54 -0
- package/src/install/manifestFiles.ts +312 -0
- package/src/install/preflight.ts +16 -2
- package/src/install/preserve.ts +205 -0
- package/src/install/recordedOwnership.ts +149 -0
- package/src/install/txlog.ts +11 -2
- package/src/install/types.ts +19 -4
- package/src/scripts/_cli/cmd_conformance.ts +88 -15
- package/src/scripts/_cli/cmd_doctor.ts +7 -20
- package/src/scripts/_cli/cmd_route_explain.ts +9 -2
- package/src/scripts/_cli/cmd_settings_get.ts +44 -0
- package/src/scripts/_cli/doctor_execution.ts +173 -0
- package/src/scripts/_cli/doctor_network_posture.ts +226 -0
- package/src/scripts/_lib/activation_payload.ts +247 -0
- package/src/scripts/_lib/agent_settings.ts +4 -2
- package/src/scripts/_lib/authority_path.ts +102 -0
- package/src/scripts/_lib/body_portable.ts +102 -0
- package/src/scripts/_lib/cascade_base.ts +94 -0
- package/src/scripts/_lib/cc_transcript.ts +132 -1
- package/src/scripts/_lib/changelog_eras.ts +7 -2
- package/src/scripts/_lib/concern_sla_window.ts +200 -0
- package/src/scripts/_lib/continuation_ladder.ts +149 -2
- package/src/scripts/_lib/council_transport.ts +129 -0
- package/src/scripts/_lib/delivery_ready.ts +112 -0
- package/src/scripts/_lib/forge_protection.ts +381 -0
- package/src/scripts/_lib/forge_reader.ts +558 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -0
- package/src/scripts/_lib/host_env_write.ts +111 -0
- package/src/scripts/_lib/json_python_parity.ts +127 -0
- package/src/scripts/_lib/lean_projection_mode.ts +14 -3
- package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
- package/src/scripts/_lib/loop_surfaces.ts +382 -0
- package/src/scripts/_lib/mission_record.ts +190 -0
- package/src/scripts/_lib/obligation_frequency.ts +217 -0
- package/src/scripts/_lib/obligations.ts +513 -0
- package/src/scripts/_lib/orchestration_record.ts +11 -20
- package/src/scripts/_lib/probe_inputs.ts +271 -0
- package/src/scripts/_lib/read_surface_scan.ts +322 -0
- package/src/scripts/_lib/release_highlights.ts +201 -11
- package/src/scripts/_lib/release_holds.ts +493 -0
- package/src/scripts/_lib/release_migration_gate.ts +184 -0
- package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
- package/src/scripts/_lib/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/shell_write_shapes.ts +108 -0
- package/src/scripts/_lib/structural_hiding.ts +622 -0
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- package/src/scripts/_lib/turn_end_refusals.ts +525 -4
- package/src/scripts/_lib/turn_end_transcript.ts +216 -0
- package/src/scripts/_lib/typed_op_grant.ts +246 -0
- package/src/scripts/_lib/typed_op_watch.ts +185 -0
- package/src/scripts/_lib/verification_command.ts +324 -0
- package/src/scripts/_lib/verification_evidence.ts +647 -0
- package/src/scripts/_lib/verify_clause.ts +280 -0
- package/src/scripts/ai_council/config.ts +8 -11
- package/src/scripts/ai_council/fallback_config.ts +5 -3
- package/src/scripts/ai_council/ownership_classes.ts +118 -0
- package/src/scripts/ai_team/team_dispatch.ts +4 -0
- package/src/scripts/ask_block_census.ts +285 -15
- package/src/scripts/before_complete_hook.ts +340 -23
- package/src/scripts/bench_hook_latency.ts +412 -5
- package/src/scripts/cache_realization_report.ts +61 -1
- package/src/scripts/check_argument_hint.ts +287 -0
- package/src/scripts/check_claims.ts +125 -1
- package/src/scripts/check_enforcement_coverage.ts +38 -9
- package/src/scripts/check_enforcement_matrix.ts +866 -0
- package/src/scripts/check_finding_dispositions.ts +22 -3
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +1025 -0
- package/src/scripts/check_host_docs_digest.ts +786 -0
- package/src/scripts/check_host_format_column.ts +559 -0
- package/src/scripts/check_input_references.ts +228 -0
- package/src/scripts/check_inputs_parity.ts +235 -0
- package/src/scripts/check_kernel_edit_ratified.ts +47 -5
- package/src/scripts/check_kill_switch_table.ts +294 -0
- package/src/scripts/check_placeholder_drift.ts +221 -0
- package/src/scripts/check_preamble_payload_budget.ts +75 -2
- package/src/scripts/check_read_surface_coverage.ts +515 -0
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +568 -0
- package/src/scripts/check_routing_coverage.ts +378 -11
- package/src/scripts/check_rule_projection_integrity.ts +22 -12
- package/src/scripts/check_static_parity.ts +17 -0
- package/src/scripts/check_test_delta.ts +294 -0
- package/src/scripts/check_test_weakening.ts +299 -0
- package/src/scripts/ci_settle.ts +145 -19
- package/src/scripts/closure_scan.ts +447 -0
- package/src/scripts/compile_hook_manifest.ts +102 -18
- package/src/scripts/council_record_shape.ts +209 -0
- package/src/scripts/external_sources_denylist.json +29 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +127 -11
- package/src/scripts/hooks/block_config_weakening.ts +229 -4
- package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
- package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
- package/src/scripts/hooks/bundle_integrity.ts +291 -0
- package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
- package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
- package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
- package/src/scripts/hooks/concern_registry.ts +6 -0
- package/src/scripts/hooks/concern_timings.ts +94 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
- package/src/scripts/hooks/design_pass_hook.ts +221 -5
- package/src/scripts/hooks/design_slop_hook.ts +1 -2
- package/src/scripts/hooks/dispatch_hook.ts +25 -19
- package/src/scripts/hooks/edit_shape_hook.ts +1 -2
- package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
- package/src/scripts/hooks/evidence_independence.ts +97 -2
- package/src/scripts/hooks/exit_codes.ts +128 -0
- package/src/scripts/hooks/host_lowering.json +1 -0
- package/src/scripts/hooks/host_lowering.ts +128 -3
- package/src/scripts/hooks/host_lowering.yaml +262 -46
- package/src/scripts/hooks/host_semantics.ts +30 -2
- package/src/scripts/hooks/injection_budget.ts +9 -2
- package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
- package/src/scripts/hooks/journal_record_hook.ts +1 -2
- package/src/scripts/hooks/memory_recall_hook.ts +539 -0
- package/src/scripts/hooks/obligation_settle_hook.ts +328 -0
- package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
- package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
- package/src/scripts/hooks/permission_gate.ts +1 -2
- package/src/scripts/hooks/push_settle_hook.ts +1 -3
- package/src/scripts/hooks/replay_hook.ts +2 -1
- package/src/scripts/hooks/reread_guard_hook.ts +1 -2
- package/src/scripts/hooks/review_baseline_hook.ts +1 -2
- package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
- package/src/scripts/hooks/rule_inject_hook.ts +55 -4
- package/src/scripts/hooks/run_continuation_hook.ts +30 -35
- package/src/scripts/hooks/skill_route_hook.ts +1 -2
- package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
- package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
- package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
- package/src/scripts/hooks/table_fingerprint.ts +49 -0
- package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
- package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
- package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
- package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
- package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
- package/src/scripts/hooks_doctor.ts +85 -0
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +109 -210
- package/src/scripts/janitor.ts +7 -0
- package/src/scripts/lint_decision_classes.ts +464 -0
- package/src/scripts/lint_deny_text.ts +419 -0
- package/src/scripts/lint_exit_codes.ts +262 -0
- package/src/scripts/lint_hook_manifest.ts +66 -0
- package/src/scripts/lint_major_migration_sections.ts +450 -0
- package/src/scripts/lint_plan_risk_register.ts +47 -3
- package/src/scripts/lint_roadmap_blockers.ts +139 -12
- package/src/scripts/lint_roadmap_producers.ts +202 -0
- package/src/scripts/lint_skill_descriptions.ts +1 -1
- package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
- package/src/scripts/mcp_server/prompts.ts +142 -1
- package/src/scripts/measure_rule_budget.ts +101 -5
- package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
- package/src/scripts/measure_turn_end_gate.ts +193 -0
- package/src/scripts/memory_lookup.ts +1 -1
- package/src/scripts/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/prepack-check.mjs +53 -0
- package/src/scripts/release.ts +14 -98
- package/src/scripts/release_drill.ts +267 -1
- package/src/scripts/release_publication.ts +64 -1
- package/src/scripts/report_enforcement_drift.ts +566 -0
- package/src/scripts/report_host_injection_effect.ts +3 -11
- package/src/scripts/report_invocation_surface.ts +383 -0
- package/src/scripts/report_obligation_carriers.ts +699 -0
- package/src/scripts/report_obligation_writer_split.ts +236 -0
- package/src/scripts/report_skill_menu_census.ts +208 -0
- package/src/scripts/report_standing_payload_by_host.ts +26 -152
- package/src/scripts/roadmap_verify_share.ts +213 -0
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/command.schema.json +48 -0
- package/src/scripts/schemas/review-findings.schema.json +4 -0
- package/src/scripts/schemas/skill.schema.json +44 -0
- package/src/scripts/scope_growth.ts +219 -0
- package/src/scripts/self_review_gate.ts +359 -10
- package/src/scripts/semantic_parity_census.ts +314 -0
- package/src/scripts/session_eol_report.ts +54 -1
- package/src/scripts/sweep_dead_scan_roots.ts +1 -1
- package/src/scripts/trigger_eval_grandfather.json +0 -3
- package/src/scripts/trigger_eval_rotation.ts +122 -27
- package/src/scripts/ui_conformance_probe.ts +774 -0
- package/src/scripts/update_prices.ts +85 -3
- package/src/scripts/write_bundle_digest.mjs +74 -0
- package/src/server/schemas/settings.ts +62 -7
- package/src/shared/missionExecution.ts +126 -0
- package/src/templates/consumer-settings/ONBOARDING.md +1 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
- package/dist/ui/assets/index-D0jaCRPM.js +0 -29
- package/dist/ui/assets/index-jSfMW8_t.css +0 -1
- /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
|
+
}
|