@event4u/agent-config 16.0.0 → 16.1.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 +297 -137
- 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/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 +15 -15
- 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/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 +50 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
- package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
- 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/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 +188 -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/cli-delegate/check_update_banner.js +40 -2
- package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
- 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-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
- package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
- package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
- package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
- 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-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
- 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/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +90 -56
- 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 +4 -4
- package/dist/cli-delegate/cmd_mcp_available.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 +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- 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 +85 -38
- 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 +1832 -1007
- 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 +2007 -1481
- 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 +398 -39
- 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 +17 -5
- package/dist/shared/missionExecution.js +97 -0
- package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
- package/dist/ui/index.html +1 -1
- package/docs/MIGRATION.md +103 -0
- package/docs/contracts/settings-classes.md +26 -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 +1 -1
- 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/config/agent-settings.template.yml +139 -27
- package/src/config/auto-rule-budget.json +16 -0
- package/src/config/ci-local-parity.yml +18 -5
- package/src/config/continuity-surface.json +21 -0
- package/src/config/gate-coverage.yml +146 -13
- package/src/config/gate-violation-baselines.json +6 -6
- package/src/config/hook-token-budget.json +2 -0
- package/src/config/loop-surfaces.yaml +215 -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 +18 -4
- 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 +4 -3
- 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 +90 -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/cascade_base.ts +94 -0
- package/src/scripts/_lib/changelog_eras.ts +7 -2
- 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 +267 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -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/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 +402 -0
- package/src/scripts/_lib/orchestration_record.ts +11 -20
- package/src/scripts/_lib/release_highlights.ts +199 -10
- package/src/scripts/_lib/release_holds.ts +493 -0
- package/src/scripts/_lib/release_migration_gate.ts +184 -0
- package/src/scripts/_lib/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- package/src/scripts/_lib/typed_op_grant.ts +246 -0
- package/src/scripts/_lib/typed_op_watch.ts +185 -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/ask_block_census.ts +285 -15
- package/src/scripts/check_enforcement_matrix.ts +624 -0
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +954 -0
- package/src/scripts/check_preamble_payload_budget.ts +75 -2
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +567 -0
- package/src/scripts/check_rule_projection_integrity.ts +22 -12
- package/src/scripts/check_test_delta.ts +294 -0
- package/src/scripts/check_test_weakening.ts +299 -0
- package/src/scripts/closure_scan.ts +333 -0
- package/src/scripts/compile_hook_manifest.ts +43 -3
- package/src/scripts/council_record_shape.ts +209 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +88 -9
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/design_pass_hook.ts +128 -3
- package/src/scripts/hooks/evidence_independence.ts +96 -0
- package/src/scripts/hooks/memory_recall_hook.ts +540 -0
- package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
- package/src/scripts/hooks/rule_inject_hook.ts +49 -1
- package/src/scripts/hooks/run_continuation_hook.ts +17 -20
- package/src/scripts/hooks_doctor.ts +85 -0
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +106 -206
- package/src/scripts/janitor.ts +7 -0
- package/src/scripts/lint_decision_classes.ts +464 -0
- package/src/scripts/lint_hook_manifest.ts +11 -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 +61 -0
- package/src/scripts/lint_roadmap_producers.ts +202 -0
- package/src/scripts/measure_rule_budget.ts +101 -5
- package/src/scripts/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/release.ts +14 -98
- package/src/scripts/release_drill.ts +267 -1
- package/src/scripts/release_publication.ts +51 -0
- package/src/scripts/report_enforcement_drift.ts +566 -0
- package/src/scripts/report_host_injection_effect.ts +3 -11
- package/src/scripts/report_standing_payload_by_host.ts +26 -152
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/command.schema.json +4 -0
- package/src/scripts/scope_growth.ts +219 -0
- package/src/scripts/trigger_eval_grandfather.json +0 -1
- package/src/scripts/ui_conformance_probe.ts +622 -0
- package/src/server/schemas/settings.ts +31 -7
- package/src/shared/missionExecution.ts +126 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
|
@@ -0,0 +1,622 @@
|
|
|
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
|
+
* THE NOISE CONTROL. Computed-style comparison over every property fires on
|
|
33
|
+
* font fallbacks and token indirection nobody cares about, so the compared set
|
|
34
|
+
* is curated (`COMPARED_PROPERTIES`) to the properties a design token actually
|
|
35
|
+
* lands in. A deviation that is intended is recorded in a sibling
|
|
36
|
+
* `conformance.declared.json` and suppressed with its reason retained, so an
|
|
37
|
+
* approved change is not re-litigated on every run.
|
|
38
|
+
*
|
|
39
|
+
* Exit codes: 0 written (with or without findings) · 1 usage error.
|
|
40
|
+
*/
|
|
41
|
+
import fs from 'node:fs';
|
|
42
|
+
import { createRequire } from 'node:module';
|
|
43
|
+
import path from 'node:path';
|
|
44
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
45
|
+
|
|
46
|
+
// Type-only, and that is load-bearing rather than stylistic: these are erased
|
|
47
|
+
// at compile time, so they add no second runtime path to a module that refuses
|
|
48
|
+
// to be loaded twice (see `loadPlaywright`).
|
|
49
|
+
import type * as PlaywrightTest from '@playwright/test';
|
|
50
|
+
type Page = PlaywrightTest.Page;
|
|
51
|
+
|
|
52
|
+
export const DIMENSIONS = [
|
|
53
|
+
'structure',
|
|
54
|
+
'computed_style',
|
|
55
|
+
'interaction',
|
|
56
|
+
'viewport_matrix',
|
|
57
|
+
'media_emulation',
|
|
58
|
+
] as const;
|
|
59
|
+
export type Dimension = (typeof DIMENSIONS)[number];
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Curated, not exhaustive. Every entry is a property a design token resolves
|
|
63
|
+
* into or a layout property a breakpoint changes; nothing here fires on a font
|
|
64
|
+
* fallback or on inherited text metrics.
|
|
65
|
+
*/
|
|
66
|
+
export const COMPARED_PROPERTIES = [
|
|
67
|
+
'background-color',
|
|
68
|
+
'color',
|
|
69
|
+
'font-size',
|
|
70
|
+
'line-height',
|
|
71
|
+
'padding-top',
|
|
72
|
+
'padding-left',
|
|
73
|
+
'display',
|
|
74
|
+
'flex-direction',
|
|
75
|
+
'align-items',
|
|
76
|
+
'gap',
|
|
77
|
+
'outline-color',
|
|
78
|
+
'outline-width',
|
|
79
|
+
'transition-duration',
|
|
80
|
+
] as const;
|
|
81
|
+
|
|
82
|
+
/** Widest first. A divergence is attributed to the first width it appears at. */
|
|
83
|
+
export const VIEWPORTS = [1440, 768, 375, 320] as const;
|
|
84
|
+
|
|
85
|
+
export const INTERACTION_STATES = ['hover', 'focus', 'active', 'keyboard'] as const;
|
|
86
|
+
|
|
87
|
+
export type StyleMap = Record<string, string>;
|
|
88
|
+
|
|
89
|
+
export interface NodeObservation {
|
|
90
|
+
probe_id: string;
|
|
91
|
+
base: StyleMap;
|
|
92
|
+
hover: StyleMap;
|
|
93
|
+
focus: StyleMap;
|
|
94
|
+
active: StyleMap;
|
|
95
|
+
keyboard: StyleMap;
|
|
96
|
+
reduced_motion: StyleMap;
|
|
97
|
+
viewports: Record<string, StyleMap>;
|
|
98
|
+
/**
|
|
99
|
+
* A serialization of what a click did to the document, or null where the
|
|
100
|
+
* node is not clickable. Comparing the EFFECT rather than the presence of a
|
|
101
|
+
* listener is the point: a registered handler that mutates nothing and a
|
|
102
|
+
* missing handler are the same defect to a user.
|
|
103
|
+
*/
|
|
104
|
+
click_effect: string | null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface Observation {
|
|
108
|
+
variant: string;
|
|
109
|
+
source: string;
|
|
110
|
+
nodes: NodeObservation[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface Declaration {
|
|
114
|
+
probe_id: string;
|
|
115
|
+
dimension: Dimension;
|
|
116
|
+
property: string;
|
|
117
|
+
reason: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface Finding {
|
|
121
|
+
dimension: Dimension;
|
|
122
|
+
probe_id: string;
|
|
123
|
+
/** The interaction state, the viewport width, or null for a flat compare. */
|
|
124
|
+
state: string | null;
|
|
125
|
+
property: string | null;
|
|
126
|
+
expected: string;
|
|
127
|
+
observed: string;
|
|
128
|
+
detail: string;
|
|
129
|
+
/** Viewport dimension only: every width at which the divergence holds. */
|
|
130
|
+
widths?: string[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface DimensionRow {
|
|
134
|
+
dimension: Dimension;
|
|
135
|
+
status: 'exercised' | 'not_applicable';
|
|
136
|
+
/** `null` on a not-applicable row. Never 0 — see the header. */
|
|
137
|
+
findings: number | null;
|
|
138
|
+
reason?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface SuppressedDeviation {
|
|
142
|
+
probe_id: string;
|
|
143
|
+
property: string;
|
|
144
|
+
reason: string;
|
|
145
|
+
expected: string;
|
|
146
|
+
observed: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface ProbeArtefact {
|
|
150
|
+
schema: 'ui-conformance/v1';
|
|
151
|
+
generated_at: string;
|
|
152
|
+
host_class: 'A' | 'B' | 'C' | 'unknown';
|
|
153
|
+
target: string;
|
|
154
|
+
reference: string | null;
|
|
155
|
+
structure_gate: 'passed' | 'stopped';
|
|
156
|
+
unmatched_nodes: string[];
|
|
157
|
+
dimensions: DimensionRow[];
|
|
158
|
+
findings: Finding[];
|
|
159
|
+
declared_suppressed: SuppressedDeviation[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------- pure core
|
|
163
|
+
|
|
164
|
+
export function readObservation(file: string): Observation {
|
|
165
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8')) as Observation;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function loadDeclarations(file: string): Declaration[] {
|
|
169
|
+
try {
|
|
170
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf-8')) as { declared?: Declaration[] };
|
|
171
|
+
return raw.declared ?? [];
|
|
172
|
+
} catch {
|
|
173
|
+
// A missing declaration file means nothing is declared, which is the
|
|
174
|
+
// strict reading. It is never an error: most targets declare nothing.
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const isDeclared = (d: readonly Declaration[], probeId: string, dimension: Dimension, property: string): Declaration | undefined =>
|
|
180
|
+
d.find((x) => x.probe_id === probeId && x.dimension === dimension && x.property === property);
|
|
181
|
+
|
|
182
|
+
/** Did driving this state change anything, relative to the given baseline? */
|
|
183
|
+
function stateDelta(base: StyleMap, state: StyleMap): string[] {
|
|
184
|
+
return COMPARED_PROPERTIES.filter((p) => base[p] !== state[p]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Which reading each state is measured AGAINST, and `active` is the interesting
|
|
189
|
+
* one. A pointer cannot press an element without hovering it first, so the
|
|
190
|
+
* `:active` reading always carries the `:hover` styles too. Measured against
|
|
191
|
+
* `base`, removing a single hover rule therefore surfaces as TWO findings —
|
|
192
|
+
* hover lost and active lost — which counts one defect twice and was exactly
|
|
193
|
+
* what the fixture caught on the first run. Measuring `active` against `hover`
|
|
194
|
+
* isolates what pressing actually contributes, and a design that darkens
|
|
195
|
+
* further on press still reports correctly.
|
|
196
|
+
*/
|
|
197
|
+
const STATE_BASELINE: Record<(typeof INTERACTION_STATES)[number], 'base' | 'hover'> = {
|
|
198
|
+
hover: 'base',
|
|
199
|
+
active: 'hover',
|
|
200
|
+
focus: 'base',
|
|
201
|
+
keyboard: 'base',
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export function evaluate(
|
|
205
|
+
reference: Observation,
|
|
206
|
+
target: Observation,
|
|
207
|
+
declarations: readonly Declaration[],
|
|
208
|
+
): ProbeArtefact {
|
|
209
|
+
const targetById = new Map(target.nodes.map((n) => [n.probe_id, n]));
|
|
210
|
+
const findings: Finding[] = [];
|
|
211
|
+
const suppressed: SuppressedDeviation[] = [];
|
|
212
|
+
const unmatched: string[] = [];
|
|
213
|
+
|
|
214
|
+
for (const ref of reference.nodes) {
|
|
215
|
+
const got = targetById.get(ref.probe_id);
|
|
216
|
+
if (!got) {
|
|
217
|
+
unmatched.push(ref.probe_id);
|
|
218
|
+
findings.push({
|
|
219
|
+
dimension: 'structure',
|
|
220
|
+
probe_id: ref.probe_id,
|
|
221
|
+
state: null,
|
|
222
|
+
property: null,
|
|
223
|
+
expected: 'present',
|
|
224
|
+
observed: 'absent',
|
|
225
|
+
detail:
|
|
226
|
+
`no node carries data-probe-id="${ref.probe_id}" in the target. ` +
|
|
227
|
+
'Style comparison is skipped for this node rather than run against ' +
|
|
228
|
+
'whatever occupies its position.',
|
|
229
|
+
});
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- computed_style, at the base viewport
|
|
234
|
+
for (const p of COMPARED_PROPERTIES) {
|
|
235
|
+
if (ref.base[p] === got.base[p]) continue;
|
|
236
|
+
const declared = isDeclared(declarations, ref.probe_id, 'computed_style', p);
|
|
237
|
+
if (declared) {
|
|
238
|
+
suppressed.push({
|
|
239
|
+
probe_id: ref.probe_id,
|
|
240
|
+
property: p,
|
|
241
|
+
reason: declared.reason,
|
|
242
|
+
expected: ref.base[p] ?? '',
|
|
243
|
+
observed: got.base[p] ?? '',
|
|
244
|
+
});
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
findings.push({
|
|
248
|
+
dimension: 'computed_style',
|
|
249
|
+
probe_id: ref.probe_id,
|
|
250
|
+
state: null,
|
|
251
|
+
property: p,
|
|
252
|
+
expected: ref.base[p] ?? '',
|
|
253
|
+
observed: got.base[p] ?? '',
|
|
254
|
+
detail: `resolved ${p} differs at the base viewport`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// --- interaction: a state the reference reacts to and the target does not
|
|
259
|
+
for (const state of INTERACTION_STATES) {
|
|
260
|
+
const baseline = STATE_BASELINE[state];
|
|
261
|
+
const refDelta = stateDelta(baseline === 'hover' ? ref.hover : ref.base, ref[state]);
|
|
262
|
+
const gotDelta = stateDelta(baseline === 'hover' ? got.hover : got.base, got[state]);
|
|
263
|
+
const lost = refDelta.filter((p) => !gotDelta.includes(p));
|
|
264
|
+
if (!lost.length) continue;
|
|
265
|
+
if (isDeclared(declarations, ref.probe_id, 'interaction', lost[0]!)) continue;
|
|
266
|
+
findings.push({
|
|
267
|
+
dimension: 'interaction',
|
|
268
|
+
probe_id: ref.probe_id,
|
|
269
|
+
state,
|
|
270
|
+
property: lost[0]!,
|
|
271
|
+
expected: `${state} changes ${lost.join(', ')}`,
|
|
272
|
+
observed: gotDelta.length ? `${state} changes ${gotDelta.join(', ')}` : `${state} changes nothing`,
|
|
273
|
+
detail:
|
|
274
|
+
`the reference responds to ${state} on this node and the target does not. ` +
|
|
275
|
+
'No resting capture at any viewport contains this difference.',
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// --- interaction: a handler whose effect disappeared
|
|
280
|
+
if (ref.click_effect !== null && ref.click_effect !== got.click_effect) {
|
|
281
|
+
findings.push({
|
|
282
|
+
dimension: 'interaction',
|
|
283
|
+
probe_id: ref.probe_id,
|
|
284
|
+
state: 'click',
|
|
285
|
+
property: null,
|
|
286
|
+
expected: ref.click_effect,
|
|
287
|
+
observed: got.click_effect ?? 'no effect',
|
|
288
|
+
detail:
|
|
289
|
+
'clicking this node changes the document in the reference and does not in ' +
|
|
290
|
+
'the target. The element renders identically in both.',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// --- viewport_matrix: ONE finding per breakpoint row, not per property.
|
|
295
|
+
// A single removed media rule usually changes several properties at once
|
|
296
|
+
// (this fixture's changes both `flex-direction` and `align-items`), and
|
|
297
|
+
// reporting one finding each would count one defect several times and
|
|
298
|
+
// make "exactly the matching breakpoint row is red" untestable. The row
|
|
299
|
+
// is the unit a reader acts on: this node does not change at this width.
|
|
300
|
+
const perWidth = new Map<string, string[]>();
|
|
301
|
+
for (const w of VIEWPORTS) {
|
|
302
|
+
const key = String(w);
|
|
303
|
+
const refV = ref.viewports[key];
|
|
304
|
+
const gotV = got.viewports[key];
|
|
305
|
+
if (!refV || !gotV) continue;
|
|
306
|
+
const diverging = COMPARED_PROPERTIES.filter(
|
|
307
|
+
(p) =>
|
|
308
|
+
refV[p] !== gotV[p] &&
|
|
309
|
+
// A divergence already visible at the base viewport is a flat
|
|
310
|
+
// style difference, not a breakpoint one. Reporting it in both
|
|
311
|
+
// dimensions would count one defect twice.
|
|
312
|
+
ref.base[p] === got.base[p] &&
|
|
313
|
+
!isDeclared(declarations, ref.probe_id, 'viewport_matrix', p),
|
|
314
|
+
);
|
|
315
|
+
if (diverging.length) perWidth.set(key, diverging);
|
|
316
|
+
}
|
|
317
|
+
if (perWidth.size) {
|
|
318
|
+
const widths = [...perWidth.keys()];
|
|
319
|
+
const first = widths[0]!;
|
|
320
|
+
const properties = perWidth.get(first)!;
|
|
321
|
+
findings.push({
|
|
322
|
+
dimension: 'viewport_matrix',
|
|
323
|
+
probe_id: ref.probe_id,
|
|
324
|
+
state: first,
|
|
325
|
+
property: properties.join(', '),
|
|
326
|
+
expected: properties.map((p) => `${p}: ${ref.viewports[first]?.[p] ?? ''}`).join('; '),
|
|
327
|
+
observed: properties.map((p) => `${p}: ${got.viewports[first]?.[p] ?? ''}`).join('; '),
|
|
328
|
+
detail:
|
|
329
|
+
`this node's layout diverges at ${widths.join('px, ')}px while matching at the base ` +
|
|
330
|
+
'viewport — a breakpoint the target does not cross.',
|
|
331
|
+
widths,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// --- media_emulation
|
|
336
|
+
const refMotion = stateDelta(ref.base, ref.reduced_motion);
|
|
337
|
+
const gotMotion = stateDelta(got.base, got.reduced_motion);
|
|
338
|
+
const lostMotion = refMotion.filter((p) => !gotMotion.includes(p));
|
|
339
|
+
if (lostMotion.length && !isDeclared(declarations, ref.probe_id, 'media_emulation', lostMotion[0]!)) {
|
|
340
|
+
findings.push({
|
|
341
|
+
dimension: 'media_emulation',
|
|
342
|
+
probe_id: ref.probe_id,
|
|
343
|
+
state: 'reduced-motion',
|
|
344
|
+
property: lostMotion[0]!,
|
|
345
|
+
expected: `reduced motion changes ${lostMotion.join(', ')}`,
|
|
346
|
+
observed: 'reduced motion changes nothing',
|
|
347
|
+
detail: 'the reference presents a reduced-motion alternative on this node and the target does not',
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const dimensions: DimensionRow[] = DIMENSIONS.map((d) => ({
|
|
353
|
+
dimension: d,
|
|
354
|
+
status: 'exercised',
|
|
355
|
+
findings: findings.filter((f) => f.dimension === d).length,
|
|
356
|
+
}));
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
schema: 'ui-conformance/v1',
|
|
360
|
+
generated_at: new Date().toISOString(),
|
|
361
|
+
host_class: resolveHostClass(),
|
|
362
|
+
target: target.source,
|
|
363
|
+
reference: reference.source,
|
|
364
|
+
structure_gate: unmatched.length ? 'stopped' : 'passed',
|
|
365
|
+
unmatched_nodes: unmatched,
|
|
366
|
+
dimensions,
|
|
367
|
+
findings,
|
|
368
|
+
declared_suppressed: suppressed,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* The artefact a host that cannot run the probe must still produce. Emitting
|
|
374
|
+
* nothing is indistinguishable from a clean run to everything downstream.
|
|
375
|
+
*/
|
|
376
|
+
export function unavailableArtefact(target: string, reason: string): ProbeArtefact {
|
|
377
|
+
return {
|
|
378
|
+
schema: 'ui-conformance/v1',
|
|
379
|
+
generated_at: new Date().toISOString(),
|
|
380
|
+
host_class: 'unknown',
|
|
381
|
+
target,
|
|
382
|
+
reference: null,
|
|
383
|
+
structure_gate: 'stopped',
|
|
384
|
+
unmatched_nodes: [],
|
|
385
|
+
dimensions: DIMENSIONS.map((d) => ({
|
|
386
|
+
dimension: d,
|
|
387
|
+
status: 'not_applicable' as const,
|
|
388
|
+
findings: null,
|
|
389
|
+
reason: `${d} needs a browser engine this host did not provide: ${reason}`,
|
|
390
|
+
})),
|
|
391
|
+
findings: [],
|
|
392
|
+
declared_suppressed: [],
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function resolveHostClass(): ProbeArtefact['host_class'] {
|
|
397
|
+
if (!chromiumAvailable()) return 'unknown';
|
|
398
|
+
return process.env['CI'] ? 'C' : 'A';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ------------------------------------------------------------ browser layer
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* ONE loader, memoized, and that is not a style preference. Playwright throws
|
|
405
|
+
* `Requiring @playwright/test second time` when the same process reaches it
|
|
406
|
+
* through both CJS `require` and ESM `import`, so the availability probe and
|
|
407
|
+
* the capture must share a single path. `createRequire` rather than a bare
|
|
408
|
+
* `require`, because this file is ESM and a bare `require` is undefined
|
|
409
|
+
* under tsx.
|
|
410
|
+
*/
|
|
411
|
+
let playwrightModule: typeof PlaywrightTest | null | undefined;
|
|
412
|
+
function loadPlaywright(): typeof PlaywrightTest | null {
|
|
413
|
+
if (playwrightModule !== undefined) return playwrightModule;
|
|
414
|
+
try {
|
|
415
|
+
const req = createRequire(import.meta.url);
|
|
416
|
+
playwrightModule = req('@playwright/test') as typeof PlaywrightTest;
|
|
417
|
+
} catch {
|
|
418
|
+
playwrightModule = null;
|
|
419
|
+
}
|
|
420
|
+
return playwrightModule;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function chromiumAvailable(): boolean {
|
|
424
|
+
try {
|
|
425
|
+
// Resolving the module is not enough — the binaries are a separate
|
|
426
|
+
// install, and this package declares the runner without installing them.
|
|
427
|
+
const pw = loadPlaywright();
|
|
428
|
+
const exe = pw?.chromium?.executablePath?.();
|
|
429
|
+
return Boolean(exe && fs.existsSync(exe));
|
|
430
|
+
} catch {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const READ_STYLES = (el: Element, props: readonly string[]): Record<string, string> => {
|
|
436
|
+
const cs = getComputedStyle(el);
|
|
437
|
+
const out: Record<string, string> = {};
|
|
438
|
+
for (const p of props) out[p] = cs.getPropertyValue(p).trim();
|
|
439
|
+
return out;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
export async function captureVariant(indexHtml: string): Promise<Observation> {
|
|
443
|
+
const pw = loadPlaywright();
|
|
444
|
+
if (!pw) throw new Error('captureVariant: @playwright/test is not resolvable on this host');
|
|
445
|
+
const browser = await pw.chromium.launch();
|
|
446
|
+
const url = pathToFileURL(indexHtml).href;
|
|
447
|
+
const props = [...COMPARED_PROPERTIES];
|
|
448
|
+
const nodes: NodeObservation[] = [];
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
const page = await browser.newPage({ viewport: { width: VIEWPORTS[0], height: 900 } });
|
|
452
|
+
await page.goto(url);
|
|
453
|
+
const ids = await page.$$eval('[data-probe-id]', (els) =>
|
|
454
|
+
els.map((e) => e.getAttribute('data-probe-id') ?? ''),
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
for (const id of ids.filter(Boolean).sort()) {
|
|
458
|
+
const sel = `[data-probe-id="${id}"]`;
|
|
459
|
+
await page.setViewportSize({ width: VIEWPORTS[0], height: 900 });
|
|
460
|
+
await page.emulateMedia({ reducedMotion: 'no-preference' });
|
|
461
|
+
await page.reload();
|
|
462
|
+
|
|
463
|
+
const base = await page.$eval(sel, READ_STYLES, props);
|
|
464
|
+
|
|
465
|
+
await page.hover(sel).catch(() => undefined);
|
|
466
|
+
const hover = await page.$eval(sel, READ_STYLES, props);
|
|
467
|
+
await page.mouse.move(0, 0);
|
|
468
|
+
|
|
469
|
+
await page.$eval(sel, (el) => (el as HTMLElement).focus?.());
|
|
470
|
+
const focus = await page.$eval(sel, READ_STYLES, props);
|
|
471
|
+
await page.$eval(sel, (el) => (el as HTMLElement).blur?.());
|
|
472
|
+
|
|
473
|
+
const box = await page.$eval(sel, (el) => {
|
|
474
|
+
const r = el.getBoundingClientRect();
|
|
475
|
+
return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
|
|
476
|
+
});
|
|
477
|
+
await page.mouse.move(box.x, box.y);
|
|
478
|
+
await page.mouse.down();
|
|
479
|
+
const active = await page.$eval(sel, READ_STYLES, props);
|
|
480
|
+
await page.mouse.up();
|
|
481
|
+
await page.reload();
|
|
482
|
+
|
|
483
|
+
const keyboard = await captureKeyboardState(page, id, sel, props);
|
|
484
|
+
|
|
485
|
+
await page.emulateMedia({ reducedMotion: 'reduce' });
|
|
486
|
+
const reducedMotion = await page.$eval(sel, READ_STYLES, props);
|
|
487
|
+
await page.emulateMedia({ reducedMotion: 'no-preference' });
|
|
488
|
+
|
|
489
|
+
const viewports: Record<string, StyleMap> = {};
|
|
490
|
+
for (const w of VIEWPORTS) {
|
|
491
|
+
await page.setViewportSize({ width: w, height: 900 });
|
|
492
|
+
viewports[String(w)] = await page.$eval(sel, READ_STYLES, props);
|
|
493
|
+
}
|
|
494
|
+
await page.setViewportSize({ width: VIEWPORTS[0], height: 900 });
|
|
495
|
+
|
|
496
|
+
const clickEffect = await captureClickEffect(page, sel);
|
|
497
|
+
|
|
498
|
+
nodes.push({
|
|
499
|
+
probe_id: id,
|
|
500
|
+
base,
|
|
501
|
+
hover,
|
|
502
|
+
focus,
|
|
503
|
+
active,
|
|
504
|
+
keyboard,
|
|
505
|
+
reduced_motion: reducedMotion,
|
|
506
|
+
viewports,
|
|
507
|
+
click_effect: clickEffect,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
await page.close();
|
|
511
|
+
} finally {
|
|
512
|
+
await browser.close();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return { variant: path.basename(path.dirname(indexHtml)), source: indexHtml, nodes };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Keyboard focus, reached by actually pressing Tab. `element.focus()` does not
|
|
520
|
+
* set `:focus-visible` in Chromium when the last input was not a keyboard, so a
|
|
521
|
+
* programmatic focus would silently report the focus ring as absent.
|
|
522
|
+
*/
|
|
523
|
+
async function captureKeyboardState(
|
|
524
|
+
page: Page,
|
|
525
|
+
id: string,
|
|
526
|
+
sel: string,
|
|
527
|
+
props: readonly string[],
|
|
528
|
+
): Promise<StyleMap> {
|
|
529
|
+
await page.reload();
|
|
530
|
+
for (let i = 0; i < 12; i += 1) {
|
|
531
|
+
await page.keyboard.press('Tab');
|
|
532
|
+
const hit = await page.evaluate(
|
|
533
|
+
(probeId) => document.activeElement?.getAttribute('data-probe-id') === probeId,
|
|
534
|
+
id,
|
|
535
|
+
);
|
|
536
|
+
if (hit) break;
|
|
537
|
+
}
|
|
538
|
+
const styles = await page.$eval(sel, READ_STYLES, props);
|
|
539
|
+
await page.reload();
|
|
540
|
+
return styles;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Click, then serialize what CHANGED about the document — a delta, never an
|
|
545
|
+
* absolute snapshot.
|
|
546
|
+
*
|
|
547
|
+
* The absolute form was the first implementation and it was wrong in a way the
|
|
548
|
+
* fixture caught: the snapshot listed every `data-probe-id` on the page, so
|
|
549
|
+
* renaming ONE unrelated element changed the recorded click effect of EVERY
|
|
550
|
+
* node, and the renamed variant produced a spurious interaction finding on a
|
|
551
|
+
* button whose handler was untouched. A delta is scoped to the nodes the click
|
|
552
|
+
* actually moved, so an unrelated rename is invisible here and stays the
|
|
553
|
+
* structure dimension's business.
|
|
554
|
+
*
|
|
555
|
+
* A node whose click changes nothing reports null — the correct reading for a
|
|
556
|
+
* paragraph, and the defect reading for a button.
|
|
557
|
+
*/
|
|
558
|
+
async function captureClickEffect(page: Page, sel: string): Promise<string | null> {
|
|
559
|
+
const snapshot = (): Promise<Record<string, string>> =>
|
|
560
|
+
page.evaluate(() =>
|
|
561
|
+
Object.fromEntries(
|
|
562
|
+
[...document.querySelectorAll('[data-probe-id]')].map((e) => [
|
|
563
|
+
e.getAttribute('data-probe-id') ?? '',
|
|
564
|
+
`${e.hasAttribute('hidden') ? 'hidden' : 'shown'}/${e.getAttribute('aria-expanded') ?? '-'}`,
|
|
565
|
+
]),
|
|
566
|
+
),
|
|
567
|
+
);
|
|
568
|
+
await page.reload();
|
|
569
|
+
const before = await snapshot();
|
|
570
|
+
await page.click(sel, { timeout: 2000 }).catch(() => undefined);
|
|
571
|
+
const after = await snapshot();
|
|
572
|
+
await page.reload();
|
|
573
|
+
|
|
574
|
+
const changed = Object.keys({ ...before, ...after })
|
|
575
|
+
.filter((id) => before[id] !== after[id])
|
|
576
|
+
.sort()
|
|
577
|
+
.map((id) => `${id}: ${before[id] ?? 'absent'} -> ${after[id] ?? 'absent'}`);
|
|
578
|
+
return changed.length ? changed.join('; ') : null;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// -------------------------------------------------------------------- CLI
|
|
582
|
+
|
|
583
|
+
const arg = (name: string): string | undefined => {
|
|
584
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
585
|
+
return i >= 0 ? process.argv[i + 1] : undefined;
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
async function main(): Promise<number> {
|
|
589
|
+
const target = arg('target');
|
|
590
|
+
if (!target) {
|
|
591
|
+
process.stderr.write('usage: ui_conformance_probe --target <index.html> [--reference <index.html>] [--out <file>]\n');
|
|
592
|
+
return 1;
|
|
593
|
+
}
|
|
594
|
+
const out = arg('out') ?? path.join('agents', 'runtime', 'state', 'ui-conformance.json');
|
|
595
|
+
const referencePath = arg('reference');
|
|
596
|
+
|
|
597
|
+
let artefact: ProbeArtefact;
|
|
598
|
+
if (!chromiumAvailable()) {
|
|
599
|
+
artefact = unavailableArtefact(target, 'no Chromium binary resolved from @playwright/test');
|
|
600
|
+
} else if (!referencePath) {
|
|
601
|
+
artefact = unavailableArtefact(target, 'no --reference given; a comparison needs an intended implementation');
|
|
602
|
+
for (const row of artefact.dimensions) {
|
|
603
|
+
row.reason = `${row.dimension} needs a --reference to compare against; none was given`;
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
artefact = evaluate(await captureVariant(referencePath), await captureVariant(target), loadDeclarations(path.join(path.dirname(target), 'conformance.declared.json')));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
610
|
+
fs.writeFileSync(out, `${JSON.stringify(artefact, null, 2)}\n`);
|
|
611
|
+
for (const row of artefact.dimensions) {
|
|
612
|
+
const count = row.findings === null ? `not-applicable — ${row.reason ?? ''}` : String(row.findings);
|
|
613
|
+
process.stdout.write(`${row.dimension}: ${count}\n`);
|
|
614
|
+
}
|
|
615
|
+
process.stdout.write(`structure_gate: ${artefact.structure_gate}\n`);
|
|
616
|
+
return 0;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export const _HERE = fileURLToPath(import.meta.url);
|
|
620
|
+
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
|
621
|
+
main().then((c) => process.exit(c));
|
|
622
|
+
}
|