my-frontend-observer 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +59 -7
  3. package/dist/application/comparisonService.d.ts +56 -0
  4. package/dist/application/comparisonService.js +77 -0
  5. package/dist/application/comparisonService.js.map +1 -0
  6. package/dist/application/frontendContractEvaluationService.d.ts +49 -0
  7. package/dist/application/frontendContractEvaluationService.js +112 -0
  8. package/dist/application/frontendContractEvaluationService.js.map +1 -0
  9. package/dist/application/frontendContractPersistenceService.d.ts +56 -0
  10. package/dist/application/frontendContractPersistenceService.js +91 -0
  11. package/dist/application/frontendContractPersistenceService.js.map +1 -0
  12. package/dist/artifacts/artifactReader.d.ts +19 -0
  13. package/dist/artifacts/artifactReader.js +36 -0
  14. package/dist/artifacts/artifactReader.js.map +1 -0
  15. package/dist/artifacts/comparisonArtifactReader.d.ts +18 -0
  16. package/dist/artifacts/comparisonArtifactReader.js +35 -0
  17. package/dist/artifacts/comparisonArtifactReader.js.map +1 -0
  18. package/dist/artifacts/comparisonArtifactWriter.d.ts +41 -0
  19. package/dist/artifacts/comparisonArtifactWriter.js +67 -0
  20. package/dist/artifacts/comparisonArtifactWriter.js.map +1 -0
  21. package/dist/artifacts/frontendContractArtifactReader.d.ts +24 -0
  22. package/dist/artifacts/frontendContractArtifactReader.js +47 -0
  23. package/dist/artifacts/frontendContractArtifactReader.js.map +1 -0
  24. package/dist/artifacts/frontendContractArtifactWriter.d.ts +34 -0
  25. package/dist/artifacts/frontendContractArtifactWriter.js +70 -0
  26. package/dist/artifacts/frontendContractArtifactWriter.js.map +1 -0
  27. package/dist/artifacts/frontendContractEvaluationArtifactReader.d.ts +17 -0
  28. package/dist/artifacts/frontendContractEvaluationArtifactReader.js +34 -0
  29. package/dist/artifacts/frontendContractEvaluationArtifactReader.js.map +1 -0
  30. package/dist/artifacts/frontendContractEvaluationArtifactWriter.d.ts +32 -0
  31. package/dist/artifacts/frontendContractEvaluationArtifactWriter.js +58 -0
  32. package/dist/artifacts/frontendContractEvaluationArtifactWriter.js.map +1 -0
  33. package/dist/cli.js +712 -3
  34. package/dist/cli.js.map +1 -1
  35. package/dist/domain/comparison.d.ts +198 -0
  36. package/dist/domain/comparison.js +324 -0
  37. package/dist/domain/comparison.js.map +1 -0
  38. package/dist/domain/comparisonEngine.d.ts +48 -0
  39. package/dist/domain/comparisonEngine.js +694 -0
  40. package/dist/domain/comparisonEngine.js.map +1 -0
  41. package/dist/domain/comparisonIdentity.d.ts +13 -0
  42. package/dist/domain/comparisonIdentity.js +46 -0
  43. package/dist/domain/comparisonIdentity.js.map +1 -0
  44. package/dist/domain/evidence.d.ts +2 -0
  45. package/dist/domain/evidence.js +4 -0
  46. package/dist/domain/evidence.js.map +1 -1
  47. package/dist/domain/frontendContractEvaluation.d.ts +57 -0
  48. package/dist/domain/frontendContractEvaluation.js +454 -0
  49. package/dist/domain/frontendContractEvaluation.js.map +1 -0
  50. package/dist/domain/frontendContractEvaluationArtifact.d.ts +65 -0
  51. package/dist/domain/frontendContractEvaluationArtifact.js +108 -0
  52. package/dist/domain/frontendContractEvaluationArtifact.js.map +1 -0
  53. package/dist/domain/frontendContractIdentity.d.ts +39 -0
  54. package/dist/domain/frontendContractIdentity.js +70 -0
  55. package/dist/domain/frontendContractIdentity.js.map +1 -0
  56. package/dist/domain/frontendContracts.d.ts +188 -0
  57. package/dist/domain/frontendContracts.js +260 -0
  58. package/dist/domain/frontendContracts.js.map +1 -0
  59. package/dist/domain/relationships.d.ts +168 -0
  60. package/dist/domain/relationships.js +343 -0
  61. package/dist/domain/relationships.js.map +1 -0
  62. package/dist/index.d.ts +36 -1
  63. package/dist/index.js +20 -1
  64. package/dist/index.js.map +1 -1
  65. package/docs/ARCHITECTURE.md +173 -3
  66. package/docs/CI_CD.md +76 -0
  67. package/docs/COMMANDS.md +299 -0
  68. package/docs/CONTRACTS.md +270 -6
  69. package/docs/CURRENT_STATE.md +183 -8
  70. package/docs/DEVELOPMENT.md +83 -12
  71. package/docs/PROJECT_OVERVIEW.md +18 -8
  72. package/docs/QUICKSTART.md +10 -0
  73. package/docs/RELEASE.md +17 -8
  74. package/docs/ROADMAP.md +10 -0
  75. package/docs/SECURITY.md +26 -1
  76. package/docs/WORKFLOWS.md +113 -6
  77. package/package.json +1 -1
@@ -3,7 +3,7 @@
3
3
  ## Current scaffold architecture
4
4
 
5
5
  The current repository is one published TypeScript ESM package
6
- (`my-frontend-observer@0.3.0`):
6
+ (`my-frontend-observer@0.5.0`):
7
7
 
8
8
  - `src/cli.ts` is the real, thin `observe` command entry point (argument
9
9
  parsing/output only).
@@ -45,8 +45,8 @@ Playwright or the filesystem-write path directly. Batch 6 closed the
45
45
  remaining real-Chromium coverage gap (a genuine navigation failure, distinct
46
46
  from a readiness timeout or a pre-launch safety rejection) and validated the
47
47
  packed npm tarball end to end in a clean consumer environment, independent
48
- of the source checkout. There is still no controlled-scroll/comparison
49
- behavior.
48
+ of the source checkout. At the end of the v0.1 implementation there was no
49
+ controlled-scroll or comparison behavior.
50
50
 
51
51
  ## Current v0.2 architecture (released/current architecture)
52
52
 
@@ -108,6 +108,176 @@ is still one canonical `observe()` application use case and one artifact
108
108
  writer; `scrollScenarioEvidence` is simply one more optional field on the
109
109
  same `ObservationArtifact`.
110
110
 
111
+ ## Current v0.4 architecture (released/current architecture)
112
+
113
+ v0.4 adds one new downstream pipeline that consumes `ObservationArtifact`
114
+ values rather than producing them - it never adds a second browser lifecycle,
115
+ target resolver, or observation engine:
116
+
117
+ ```text
118
+ ObservationArtifact before ObservationArtifact after
119
+ \ /
120
+ `--------. .-------'
121
+ \ /
122
+ artifact reader (src/artifacts/artifactReader.ts)
123
+ ↓
124
+ comparability evaluation (src/domain/comparisonEngine.ts)
125
+ ↓
126
+ canonical relationship derivation, called for each side independently
127
+ (src/domain/relationships.ts#deriveLayoutRelationships)
128
+ ↓
129
+ canonical comparison derivation
130
+ (src/domain/comparisonEngine.ts#compareObservations)
131
+ ↓
132
+ ComparisonArtifact
133
+ ↓
134
+ atomic comparison writer (src/artifacts/comparisonArtifactWriter.ts)
135
+
136
+ CLI `compare`
137
+ ↓
138
+ application service only (src/application/comparisonService.ts)
139
+ ↓
140
+ [reader → domain comparison → writer, as above]
141
+ ```
142
+
143
+ `src/domain/relationships.ts` froze the layout-relationship contract and
144
+ implements the one canonical pure derivation,
145
+ `deriveLayoutRelationships(observation, options?)`: horizontal/vertical
146
+ order, area overlap, relative width, geometric fit, vertical sequencing,
147
+ page-width fit/exceeds, and a standalone `deriveTargetClipping(record)` -
148
+ all computed only from an already-captured `ObservationArtifact`'s own
149
+ `targetEvidence`/`pageEvidence`, never from a second browser query. DOM
150
+ containment is read directly from the existing v0.2 `TargetContainment`
151
+ evidence rather than re-derived, and stays a distinct concept from
152
+ geometric fit.
153
+
154
+ `src/domain/comparisonEngine.ts` implements the one canonical pure
155
+ before/after engine, `compareObservations(before, after, config?)`:
156
+ validates both source artifacts, evaluates comparability *before* any
157
+ rendered difference is calculated, calls `deriveLayoutRelationships` once
158
+ per side with the same tolerance, and derives target/page differences and
159
+ relationship changes. `src/artifacts/comparisonArtifactWriter.ts` persists
160
+ the result atomically (sibling temp directory, then one rename) as
161
+ `<outputLocation>/<comparisonId>/manifest.json` only - no screenshot is
162
+ copied; the manifest's `before`/`after` references point back to the
163
+ source observations' own `screenshot.path`. `src/application/
164
+ comparisonService.ts` is the one application-layer seam: `compareAndPersist`
165
+ takes two in-memory `ObservationArtifact`s and does exactly one comparison
166
+ plus exactly one persist; `compareAndPersistFromArtifactRoots` is a thin
167
+ wrapper that additionally reads both sides from disk via the existing
168
+ `src/artifacts/artifactReader.ts#readObservationArtifact` reader (itself
169
+ just a `manifest.json` parse plus the same `isValidObservationArtifact`
170
+ structural gate the writer uses).
171
+
172
+ `src/cli.ts` gained one new top-level command, `compare`
173
+ (`--before`/`--after`/`--output`/`--config-file`), implemented with the same
174
+ thin-CLI-boundary discipline as `observe`: `parseCompareArgs` handles only
175
+ argument shape/duplication, an optional `loadComparisonConfigFile` reads and
176
+ validates only file readability/JSON-validity/non-array-object-root (exactly
177
+ like `--targets-file`/`--scroll-scenario-file`), and the command body calls
178
+ `compareAndPersistFromArtifactRoots` exactly once. **The CLI's comparison
179
+ path never launches Chromium** - `src/cli.ts` imports nothing from
180
+ `src/browser/` or `src/artifacts/` (it only reaches persistence and artifact
181
+ reading indirectly, through the application-layer seam above), matching the
182
+ same import-boundary discipline already enforced for `observe`.
183
+
184
+ ## Current v0.5 architecture (released/current architecture)
185
+
186
+ v0.5 adds one new downstream layer that consumes `ComparisonArtifact` values
187
+ (plus the source `ObservationArtifact` pair) rather than producing them - no
188
+ new browser lifecycle, target resolver, or comparison engine is added:
189
+
190
+ ```text
191
+ ObservationArtifact before + after
192
+ ↓
193
+ existing v0.4 comparison/relationship pipeline (unchanged)
194
+ ↓
195
+ ComparisonArtifact
196
+ ↓ PersistentBaselineContract
197
+ | +
198
+ `------------------------→ PerChangeContract
199
+ ↓
200
+ canonical contract evaluation
201
+ (src/domain/frontendContractEvaluation.ts#evaluateFrontendContract)
202
+ ↓
203
+ clause results + unexpected changes + overall PASS/FAIL
204
+ ```
205
+
206
+ `src/domain/frontendContracts.ts` froze the contract/change-scope type,
207
+ constant, and structural-validator vocabulary (Batch 1); `src/domain/
208
+ frontendContractIdentity.ts` froze deterministic contract/baseline/clause
209
+ identity in the same canonicalize+sha256(+opaque-nonce) style as `src/domain/
210
+ comparisonIdentity.ts`. `src/domain/frontendContractEvaluation.ts#evaluateFrontendContract`
211
+ (Batch 2) is the one canonical pure evaluation entry point: it validates its
212
+ five inputs (before/after `ObservationArtifact`, `ComparisonArtifact`,
213
+ `PersistentBaselineContract`, `PerChangeContract`) are structurally coherent
214
+ and mutually consistent, calculates the active baseline clause set after
215
+ explicit supersession, detects bounded structural conflicts, evaluates every
216
+ active clause via the frozen 15-primitive vocabulary against the existing
217
+ `ComparisonArtifact`/`ObservationArtifact` evidence (never re-deriving
218
+ clipping/relationship/scroll-owner facts), classifies unaccounted-for
219
+ `ComparisonArtifact.differences` entries as `unexpected`, and derives one
220
+ overall `PASS`/`FAIL` verdict. It performs no I/O, launches no browser, and
221
+ persists nothing - `src/domain/frontendContractEvaluation.ts` itself remains
222
+ untouched by the persistence layer below (Batch 3).
223
+
224
+ Batch 3 adds the persistence/application boundary around this frozen domain,
225
+ without redefining it:
226
+
227
+ ```text
228
+ ComparisonArtifact (read via new src/artifacts/comparisonArtifactReader.ts)
229
+ +
230
+ PersistentBaselineContract / PerChangeContract
231
+ (read/written via src/artifacts/frontendContractArtifactReader.ts / ...Writer.ts)
232
+ ↓
233
+ src/application/frontendContractEvaluationService.ts#evaluateAndPersist
234
+ ↓
235
+ evaluateFrontendContract() [called exactly once, unmodified]
236
+ ↓
237
+ src/domain/frontendContractEvaluationArtifact.ts
238
+ (minimal additive persisted envelope around the frozen result)
239
+ ↓
240
+ src/artifacts/frontendContractEvaluationArtifactWriter.ts
241
+ (atomic write, exactly once on a structurally constructible result)
242
+ ```
243
+
244
+ `evaluateAndPersistFromArtifactRoots` is the CLI-facing wrapper, reading
245
+ before/after observations through the existing `readObservationArtifact` (no
246
+ second observation reader), the comparison and both contract classes
247
+ through the new readers, then delegating to `evaluateAndPersist` exactly
248
+ once - mirroring `application/comparisonService.ts#compareAndPersistFromArtifactRoots`'s
249
+ own thin-wrapper shape.
250
+
251
+ Batch 4 exposes this through the same thin-CLI boundary already established
252
+ by `observe`/`compare`:
253
+
254
+ ```text
255
+ src/cli.ts (argument parsing, JSON-file-shape checks, help/output formatting,
256
+ exit-code selection only)
257
+ ↓
258
+ src/application/frontendContractPersistenceService.ts#approveAndPersistBaseline
259
+ src/application/frontendContractPersistenceService.ts#persistPerChangeContract
260
+ src/application/frontendContractEvaluationService.ts#evaluateAndPersistFromArtifactRoots
261
+ ↓
262
+ domain validators (isValidPersistentBaselineContract / isValidPerChangeContract)
263
+ + artifact readers/writers (Batch 3)
264
+ + evaluateFrontendContract() (Batch 2, unmodified)
265
+ ```
266
+
267
+ Three new top-level commands - `approve-baseline`, `save-change-contract`,
268
+ `evaluate-contract` - each parse only CLI-syntax concerns (duplicate/missing
269
+ flags, JSON-file readability/parseability/object-root shape) and delegate to
270
+ exactly one application-layer call; `src/cli.ts` imports no artifact writer/
271
+ reader module and no browser code, matching the existing `observe`/`compare`
272
+ import-boundary discipline exactly. `approveAndPersistBaseline` adds the one
273
+ new coherence check Batch 3 did not need: verifying a baseline contract's
274
+ frozen `sourceObservation` reference actually matches the supplied
275
+ observation artifact before persisting - explicit approval only, never
276
+ inferred from a `compare` or `evaluate-contract` result. `--enforce` on
277
+ `evaluate-contract` is applied only after evaluation and persistence have
278
+ already completed; it selects the process exit status for an already-final
279
+ `FAIL` result and is never part of any identity or persisted field.
280
+
111
281
  ## Planned v0.1 architecture constraints
112
282
 
113
283
  v0.1 planning must preserve these approved boundaries without treating module
package/docs/CI_CD.md CHANGED
@@ -67,3 +67,79 @@ observations. This was proven on `validation/v0.3-pre-release`
67
67
  hash-verified candidate tarball (SHA-256
68
68
  `da6cbc18a98d005b84a2f12c06f5c01d390006b53c02176a6c9cdc6d9b9d24d0`) - the
69
69
  release candidate for `v0.3.0`.
70
+
71
+ ## v0.4 readiness coverage
72
+
73
+ The packed-candidate readiness gap that previously existed for the v0.4
74
+ `compare` public command - identified during the v0.4 implementation/
75
+ documentation audit as `V0_4_READINESS_VALIDATION_GAP_EXISTS` - has been
76
+ closed: `scripts/ci/runPackedObservationSmoke.mjs` now also installs the
77
+ candidate tarball, observes a deterministic before/after fixture through
78
+ the installed `observe`, and runs the installed `compare` against both
79
+ resulting artifacts, asserting (through the *installed package's own*
80
+ exported `isValidComparisonArtifact`/`isValidObservationArtifact`, resolved
81
+ from the consumer's own `node_modules` rather than the source checkout):
82
+ comparison artifact kind/schema `1.0.0`, a `comparable` result with a real
83
+ `moved`+`resized` target difference and a real relationship-family
84
+ transition (`does-not-overlap`→`overlaps` and `left-of`→
85
+ `horizontally-overlapping` simultaneously - the same pair the v0.4
86
+ relationship-family matching regression concerns), an explicit
87
+ `--config-file` non-causal dependency-evidence result, an `incomparable`
88
+ result (differing viewport) with the expected blocking comparability
89
+ reason and no fabricated ordinary differences, comparison-manifest path
90
+ privacy (no absolute operational path leakage), retained (never copied)
91
+ before/after screenshot references, a `manifest.json`-only comparison
92
+ directory, and byte-identical source-observation manifests/screenshots
93
+ before and after comparing - on every platform in the matrix, using the
94
+ same single hash-verified candidate tarball as the legacy/semantic/scroll
95
+ observations. This preserves every pre-existing v0.1-v0.3 packed
96
+ observation proof unchanged and adds the v0.4 proof additively, in the same
97
+ script and the same candidate, per the established same-candidate
98
+ architecture (no second `npm pack`, no per-platform rebuild).
99
+
100
+ ## v0.5 readiness coverage
101
+
102
+ The packed-readiness coverage gap identified during the v0.5 implementation-
103
+ completeness audit as `V0_5_READINESS_VALIDATION_GAP_EXISTS` - the packed
104
+ candidate smoke exercised `observe`/`compare` but not `approve-baseline`/
105
+ `save-change-contract`/`evaluate-contract` - has been **corrected locally**:
106
+ `scripts/ci/runPackedObservationSmoke.mjs` now also installs the same
107
+ candidate tarball, observes a deterministic navigation/workspace/rail
108
+ fixture through the installed `observe`, and drives the complete installed
109
+ `approve-baseline` → `save-change-contract` → `observe` (candidate) →
110
+ `compare` → `evaluate-contract` sequence, proving both a fully successful
111
+ contract change (overall `PASS`, `--enforce` still exits `0`) and the
112
+ milestone-signature failure (a locally successful requested/expected-
113
+ dependent change alongside a real protected right-rail regression and a
114
+ real preserved navigation-clipping regression, overall `FAIL`) - including
115
+ `--enforce` producing a nonzero exit for the identical `FAIL` evidence
116
+ (`evaluationRequestId` and `clauseResults` unchanged), full source
117
+ observation/comparison/contract-artifact immutability, no copied
118
+ screenshots, and no repository-root artifact leakage. All product behavior
119
+ under test is invoked through the installed tarball's own executable, never
120
+ imported from the source checkout. This addition is exercised in every
121
+ existing matrix lane (`windows-latest`, `ubuntu-latest`, `macos-latest`)
122
+ because all three already invoke this same script against the same
123
+ candidate tarball - no workflow YAML change was required.
124
+
125
+ This correction has since been proven cross-platform on the validation
126
+ branch `validation/v0.5-pre-release`, first tested at commit
127
+ `90255a9175503664f3e65d4114ee205176b7040a`: GitHub Actions run
128
+ [`31727856546`](https://github.com/dailephd/my-frontend-observer/actions/runs/31727856546)
129
+ passed completely - the candidate job (Linux, Node 24) and all three
130
+ `matrix-smoke` lanes (`windows-latest` Node v24.18.1, `ubuntu-latest` Node
131
+ v24.19.0, `macos-latest` Node v24.18.0) - using one shared candidate
132
+ tarball, `my-frontend-observer-0.4.0.tgz`, SHA-256
133
+ `9533a53e475614cd80a29dfa8b0f85e533e3ad736596579f559479e90e78941a`,
134
+ independently hash-verified by each of the three matrix lanes before any of
135
+ them ran the smoke (no lane built its own tarball). Every lane's
136
+ `smoke-summary.json` reported byte-identical v0.5 evidence: the installed
137
+ candidate's `approve-baseline`/`save-change-contract`/`evaluate-contract`
138
+ `--help` all present, a real successful contract change (overall `PASS`,
139
+ `--enforce` exits `0`), and the real milestone-signature failure (requested/
140
+ expected-dependent `pass`, protected/preserved `fail`, overall `FAIL`,
141
+ identical `evaluationRequestId`/`clauseResults` with `--enforce` exiting `1`
142
+ and without it exiting `0`), plus source-artifact immutability, no copied
143
+ screenshot, no path leakage, and no repository-root leakage - alongside
144
+ every pre-existing v0.1-v0.4 packed observe/compare/scroll assertion, still
145
+ passing unchanged on all three platforms.
package/docs/COMMANDS.md CHANGED
@@ -225,6 +225,305 @@ persists honestly - typically as `partial` - carrying the same
225
225
  `target-missing`/`target-ambiguous`/`browser-evidence-unavailable` diagnostic
226
226
  that any other unresolved configured target would produce.
227
227
 
228
+ ## `compare`
229
+
230
+ **Current status: shipped as part of the published `my-frontend-observer@0.4.0`
231
+ package.** Comparison schema is `1.0.0`, independent of and never reused for
232
+ the observation schema (`1.2.0`).
233
+
234
+ `my-frontend-observer compare` (or `node dist/cli.js compare` from a source
235
+ checkout) reads two already-persisted observation artifacts and derives
236
+ before/after evidence purely from their existing content:
237
+
238
+ ```text
239
+ my-frontend-observer compare --before <observation-artifact-root> --after <observation-artifact-root> --output <directory> [options]
240
+ ```
241
+
242
+ Required:
243
+
244
+ - `--before <path>` — root directory of the "before" persisted observation
245
+ artifact (the directory containing its `manifest.json`, as produced by
246
+ `observe`).
247
+ - `--after <path>` — root directory of the "after" persisted observation
248
+ artifact.
249
+ - `--output <directory>` — portable, relative output location for the
250
+ comparison artifact (same contract as `observe --output`).
251
+
252
+ Options:
253
+
254
+ - `--config-file <json-file>` — loads a comparison configuration directly
255
+ (no wrapper field): `{ "geometryTolerancePx": <0-10>,
256
+ "expectedDependencies": [...] }`. Without it, `geometryTolerancePx`
257
+ defaults to `0.5` CSS px with no declared dependencies. As with
258
+ `--targets-file`/`--scroll-scenario-file`, `--config-file` only validates
259
+ file readability, JSON validity, and a non-array object root; every
260
+ semantic rule (tolerance bounds, dependency property/direction
261
+ vocabulary, dependency source marker) is enforced by the same domain
262
+ validator the comparison engine itself uses.
263
+ - `--help` — show `compare` usage.
264
+
265
+ **Comparison never launches a browser.** It reads two manifests through the
266
+ existing observation-artifact reader, runs the pure comparison engine, and
267
+ persists a portable `manifest.json` — no navigation, no target
268
+ re-resolution, no Chromium process.
269
+
270
+ On success the command prints exactly:
271
+
272
+ ```text
273
+ Comparison: <comparison-id>
274
+ State: <comparable|comparable-with-warnings|incomparable>
275
+ Artifact: <comparison-artifact-root>
276
+ Differences: <count>
277
+ Relationship changes: <count>
278
+ Diagnostics: <count>
279
+ ```
280
+
281
+ and exits `0` — **including when `State` is `incomparable`**: comparison
282
+ determining that two observations should not be treated as equivalent
283
+ frontend states is itself a successful outcome, not a failure. The command
284
+ exits nonzero only for invalid CLI syntax, an unreadable/malformed/
285
+ structurally-invalid source artifact, invalid comparison configuration, or
286
+ a failed artifact write.
287
+
288
+ ### Comparability
289
+
290
+ Before any rendered difference is calculated, the engine evaluates whether
291
+ the two observations are comparable at all:
292
+
293
+ - **Hard incompatibilities** (force `incomparable`): different logical page
294
+ URL, different viewport, different browser engine, or a mismatched scroll
295
+ scenario configuration (no scenario vs. a scenario, or two different
296
+ scenario configurations).
297
+ - **Warnings** (still `comparable-with-warnings`, comparison proceeds):
298
+ different producer package version, different browser version, or a
299
+ changed/added/removed configured target.
300
+ - **Unassessed dimensions** the observer does not yet model (theme,
301
+ authenticated state, application state) are always recorded, never
302
+ silently claimed identical.
303
+
304
+ An `incomparable` result still persists a structurally valid
305
+ `ComparisonArtifact`: the comparability reasons are recorded, and ordinary
306
+ rendered differences/relationship changes stay empty rather than fabricated.
307
+
308
+ ### Difference and relationship evidence
309
+
310
+ For a `comparable`/`comparable-with-warnings` result, the manifest's
311
+ `differences` and `relationshipChanges` arrays carry structured before/
312
+ after evidence: appeared/disappeared targets (only for a stable target name
313
+ configured on both sides — a target added/removed from configuration is
314
+ recorded separately as a `configurationChanges` entry, never fabricated as
315
+ appeared/disappeared), moved/resized targets, visibility changes, clipping
316
+ changes, actual dimensional overflow changes, DOM containment changes,
317
+ page-size changes, scroll-owner changes, and layout-relationship
318
+ transitions (e.g. `does-not-overlap` → `overlaps`, or
319
+ `document-width-fits-viewport` → `document-width-exceeds-viewport`) reused
320
+ verbatim from the same canonical relationship engine `observe` output feeds
321
+ Batch 2's `deriveLayoutRelationships`.
322
+
323
+ ### Explicit dependency evidence (non-causal)
324
+
325
+ `--config-file`'s `expectedDependencies` lets you declare an expected
326
+ layout relationship such as "`navigation.width` decreases →
327
+ `workspace.width` increases" using only the frozen `x`/`y`/`width`/`height`
328
+ property vocabulary and `increase`/`decrease`/`change`/`unchanged`
329
+ direction vocabulary. Each declaration is evaluated independently against
330
+ the two observations and persists exactly one outcome: `consistent`,
331
+ `not-observed`, `contradictory-to-declaration`, or `unavailable`. **The
332
+ observer never infers a dependency from co-change, and never emits a
333
+ causal claim, a PASS/FAIL verdict, or a change-contract decision** — v0.4
334
+ produces comparison evidence; whether that evidence satisfies some
335
+ contract is v0.5+ scope.
336
+
337
+ ### Path privacy
338
+
339
+ `--before`, `--after`, `--config-file`, and `--output` are operational
340
+ filesystem input only. None of them affect `comparisonRequestId`, and none
341
+ of them are written into the persisted manifest — the manifest instead
342
+ retains logical source references (`observationId`, `requestId`,
343
+ `producer`, `observationSchemaVersion`, and the source `screenshot.path`).
344
+ Two semantically identical observation/config pairs read from different
345
+ filesystem locations produce the same `comparisonRequestId`; each execution
346
+ still gets a fresh `comparisonId`.
347
+
348
+ ### Source observations remain immutable
349
+
350
+ Comparison is read-only with respect to its inputs: it never modifies
351
+ either source observation's `manifest.json` or `screenshot.png`, and it
352
+ never copies screenshot bytes into the comparison directory — the
353
+ comparison artifact directory contains `manifest.json` only.
354
+
355
+ Example:
356
+
357
+ ```powershell
358
+ node dist/cli.js compare `
359
+ --before observations/<before-id> `
360
+ --after observations/<after-id> `
361
+ --output comparisons `
362
+ --config-file .\comparison-config.json
363
+ ```
364
+
365
+ ## `approve-baseline`
366
+
367
+ **Current status: shipped as part of the published `my-frontend-observer@0.5.0`
368
+ package.** Frontend contract schema is `1.0.0`, independent of the
369
+ observation (`1.2.0`) and comparison (`1.0.0`) schemas.
370
+
371
+ `my-frontend-observer approve-baseline` is the *only* baseline-approval
372
+ operation in the observer — approval is never inferred from a successful
373
+ comparison or evaluation, and no command automatically supersedes or selects
374
+ a baseline. It explicitly approves and persists one already-authored
375
+ `PersistentBaselineContract` against the observation it claims to approve:
376
+
377
+ ```text
378
+ my-frontend-observer approve-baseline --observation <observation-artifact-root> --contract-file <json-file> --output <directory>
379
+ ```
380
+
381
+ Required:
382
+
383
+ - `--observation <path>` — root directory of the persisted observation
384
+ artifact this baseline claims to approve (read through the existing
385
+ observation-artifact reader).
386
+ - `--contract-file <json-file>` — local JSON file containing one raw
387
+ `PersistentBaselineContract` (no wrapper field). As with `--config-file`,
388
+ only file readability/JSON-validity/non-array-object-root is checked here;
389
+ every structural rule (artifact kind, schema version, clause shape) is
390
+ enforced by the existing frozen domain validator.
391
+ - `--output <directory>` — portable, relative output location for the
392
+ baseline artifact.
393
+
394
+ Before persisting, the application layer verifies the contract's frozen
395
+ `sourceObservation` reference (`observationId`, `requestId`, `producer`,
396
+ `observationSchemaVersion`) actually matches the supplied observation
397
+ artifact's stable identity — approving a baseline against an unrelated
398
+ observation is rejected, even if both artifacts are individually valid. Any
399
+ `supersedesBaselineId` already authored in the contract is preserved exactly
400
+ as supplied; this command never discovers a prior baseline, infers
401
+ supersession, or deletes anything.
402
+
403
+ On success the command prints exactly:
404
+
405
+ ```text
406
+ Baseline: <baseline-id>
407
+ State: approved
408
+ Artifact: <baseline-artifact-root>
409
+ Clauses: <count>
410
+ Supersedes: <baseline-id|none>
411
+ ```
412
+
413
+ and exits `0`. It exits nonzero for invalid CLI syntax, an unreadable/
414
+ malformed/structurally-invalid contract file, a `PerChangeContract` passed
415
+ where a baseline is expected, a source-observation mismatch, an existing
416
+ artifact collision (baseline identities are never overwritten), or a failed
417
+ artifact write.
418
+
419
+ ## `save-change-contract`
420
+
421
+ **Current status: shipped as part of the published `my-frontend-observer@0.5.0`
422
+ package.**
423
+
424
+ `my-frontend-observer save-change-contract` validates and persists one
425
+ already-authored `PerChangeContract` so it can later be evaluated — this is
426
+ persistence only, never approval:
427
+
428
+ ```text
429
+ my-frontend-observer save-change-contract --contract-file <json-file> --output <directory>
430
+ ```
431
+
432
+ Required:
433
+
434
+ - `--contract-file <json-file>` — local JSON file containing one raw
435
+ `PerChangeContract` (no wrapper field).
436
+ - `--output <directory>` — portable, relative output location for the
437
+ change-contract artifact.
438
+
439
+ Domain/application validation rejects a `PersistentBaselineContract` passed
440
+ here, malformed clauses, an unsupported authored category, an authored
441
+ `category: "unexpected"` (the derived-only fifth classification can never be
442
+ authored as a permission), and invalid tolerance/mode fields — none of this
443
+ is duplicated in CLI code. Any `supersedesBaselineClauseIds` already
444
+ authored on a clause is preserved exactly; resolving those references
445
+ against a particular baseline remains `evaluate-contract`'s responsibility.
446
+
447
+ On success the command prints exactly:
448
+
449
+ ```text
450
+ Change contract: <contract-id>
451
+ Artifact: <contract-artifact-root>
452
+ Clauses: <count>
453
+ Supersedes baseline clauses: <count>
454
+ ```
455
+
456
+ and exits `0`. It exits nonzero for invalid CLI syntax, an unreadable/
457
+ malformed/structurally-invalid contract file, a persistent baseline contract
458
+ passed here, an existing artifact collision, or a failed artifact write.
459
+
460
+ ## `evaluate-contract`
461
+
462
+ **Current status: shipped as part of the published `my-frontend-observer@0.5.0`
463
+ package.** Frontend contract evaluation artifact schema is `1.0.0`, its own
464
+ independent family.
465
+
466
+ `my-frontend-observer evaluate-contract` executes the canonical Batch 2
467
+ evaluator against already-persisted evidence/contracts and persists the
468
+ result:
469
+
470
+ ```text
471
+ my-frontend-observer evaluate-contract --before <observation-artifact-root> --after <observation-artifact-root> --comparison <comparison-artifact-root> --baseline <baseline-contract-artifact-root> --change <per-change-contract-artifact-root> --output <directory> [--enforce]
472
+ ```
473
+
474
+ Required:
475
+
476
+ - `--before <path>` / `--after <path>` — root directories of the persisted
477
+ before/after observation artifacts.
478
+ - `--comparison <path>` — root directory of the already-persisted comparison
479
+ artifact for that before/after pair.
480
+ - `--baseline <path>` — root directory of the already-approved baseline
481
+ contract artifact.
482
+ - `--change <path>` — root directory of the already-persisted per-change
483
+ contract artifact.
484
+ - `--output <directory>` — portable, relative output location for the
485
+ evaluation artifact.
486
+
487
+ Options:
488
+
489
+ - `--enforce` — makes a `FAIL` verdict produce a nonzero process exit
490
+ status. A `FAIL` evaluation is always persisted and printed identically
491
+ with or without this flag; `--enforce` changes only the process exit code
492
+ — never evaluation identity, contents, or persistence.
493
+
494
+ **`evaluate-contract` never launches a browser, never re-resolves targets,
495
+ and never recomputes comparison or relationship evidence** — it reads the
496
+ already-persisted before/after observations and comparison exactly as given
497
+ (through the existing observation-artifact reader and a new comparison
498
+ reader) and calls the canonical `evaluateFrontendContract` exactly once.
499
+
500
+ A `FAIL` verdict (a found regression or unsatisfied contract clause) is a
501
+ successful, persisted evaluation outcome — not an execution error. On
502
+ success (evaluation constructed and persisted, verdict `PASS`, or verdict
503
+ `FAIL` without `--enforce`), the command prints exactly:
504
+
505
+ ```text
506
+ Evaluation: <evaluation-id>
507
+ Verdict: <PASS|FAIL>
508
+ Artifact: <evaluation-artifact-root>
509
+ Clauses: <total-clause-result-count>
510
+ Unexpected: <unexpected-change-count>
511
+ Enforced: <yes|no>
512
+ ```
513
+
514
+ and exits `0`; with `--enforce` and verdict `FAIL`, it prints the same
515
+ result and exits nonzero. It exits nonzero and persists nothing for invalid
516
+ CLI syntax or an unreadable/malformed/incoherent source artifact (evaluation
517
+ could not even be constructed) — distinct from a legitimate persisted `FAIL`.
518
+
519
+ The evaluation artifact directory contains `manifest.json` only — no
520
+ screenshot is copied. Operational `--before`/`--after`/`--comparison`/
521
+ `--baseline`/`--change`/`--output` paths never enter the persisted
522
+ `evaluationRequestId` or any other semantic field; two semantically
523
+ identical evaluations invoked from different filesystem locations share the
524
+ same `evaluationRequestId` even though each execution gets a fresh
525
+ `evaluationId`.
526
+
228
527
  ## Foundation commands
229
528
 
230
529
  - `npm install` — install dependencies (includes the `playwright` runtime