my-frontend-observer 0.2.0 → 0.4.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 (67) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +51 -13
  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/observationPersistence.js +1 -0
  7. package/dist/application/observationPersistence.js.map +1 -1
  8. package/dist/artifacts/artifactReader.d.ts +19 -0
  9. package/dist/artifacts/artifactReader.js +36 -0
  10. package/dist/artifacts/artifactReader.js.map +1 -0
  11. package/dist/artifacts/comparisonArtifactWriter.d.ts +41 -0
  12. package/dist/artifacts/comparisonArtifactWriter.js +67 -0
  13. package/dist/artifacts/comparisonArtifactWriter.js.map +1 -0
  14. package/dist/browser/chromiumAdapter.js +58 -4
  15. package/dist/browser/chromiumAdapter.js.map +1 -1
  16. package/dist/browser/evidenceCapture.d.ts +29 -2
  17. package/dist/browser/evidenceCapture.js +43 -19
  18. package/dist/browser/evidenceCapture.js.map +1 -1
  19. package/dist/browser/scrollCapture.d.ts +32 -0
  20. package/dist/browser/scrollCapture.js +163 -0
  21. package/dist/browser/scrollCapture.js.map +1 -0
  22. package/dist/browser/types.d.ts +3 -1
  23. package/dist/cli.js +299 -2
  24. package/dist/cli.js.map +1 -1
  25. package/dist/domain/comparison.d.ts +198 -0
  26. package/dist/domain/comparison.js +324 -0
  27. package/dist/domain/comparison.js.map +1 -0
  28. package/dist/domain/comparisonEngine.d.ts +48 -0
  29. package/dist/domain/comparisonEngine.js +694 -0
  30. package/dist/domain/comparisonEngine.js.map +1 -0
  31. package/dist/domain/comparisonIdentity.d.ts +13 -0
  32. package/dist/domain/comparisonIdentity.js +46 -0
  33. package/dist/domain/comparisonIdentity.js.map +1 -0
  34. package/dist/domain/evidence.d.ts +2 -0
  35. package/dist/domain/evidence.js +4 -0
  36. package/dist/domain/evidence.js.map +1 -1
  37. package/dist/domain/identity.d.ts +8 -3
  38. package/dist/domain/identity.js +9 -3
  39. package/dist/domain/identity.js.map +1 -1
  40. package/dist/domain/relationships.d.ts +168 -0
  41. package/dist/domain/relationships.js +343 -0
  42. package/dist/domain/relationships.js.map +1 -0
  43. package/dist/domain/schema.d.ts +116 -2
  44. package/dist/domain/schema.js +186 -2
  45. package/dist/domain/schema.js.map +1 -1
  46. package/dist/domain/scrollEvidence.d.ts +51 -0
  47. package/dist/domain/scrollEvidence.js +134 -0
  48. package/dist/domain/scrollEvidence.js.map +1 -0
  49. package/dist/index.d.ts +17 -4
  50. package/dist/index.js +9 -2
  51. package/dist/index.js.map +1 -1
  52. package/dist/request/request.d.ts +29 -0
  53. package/dist/request/request.js +108 -0
  54. package/dist/request/request.js.map +1 -1
  55. package/docs/ARCHITECTURE.md +110 -4
  56. package/docs/CI_CD.md +62 -6
  57. package/docs/COMMANDS.md +247 -6
  58. package/docs/CONTRACTS.md +172 -7
  59. package/docs/CURRENT_STATE.md +148 -10
  60. package/docs/DEVELOPMENT.md +52 -12
  61. package/docs/PROJECT_OVERVIEW.md +18 -11
  62. package/docs/QUICKSTART.md +7 -1
  63. package/docs/RELEASE.md +14 -7
  64. package/docs/ROADMAP.md +9 -0
  65. package/docs/SECURITY.md +14 -1
  66. package/docs/WORKFLOWS.md +90 -22
  67. package/package.json +1 -1
package/docs/COMMANDS.md CHANGED
@@ -29,6 +29,10 @@ Options:
29
29
  - `--targets-file <json-file>` — loads structured semantic observation
30
30
  targets from a local JSON file instead of `--target`. Cannot be combined
31
31
  with `--target`. See "Structured semantic targets" below.
32
+ - `--scroll-scenario-file <json-file>` — loads one bounded runtime scroll
33
+ scenario from a local JSON file. May be combined with either `--target` or
34
+ `--targets-file` (it is independent of target configuration). See "Scroll
35
+ scenario (`--scroll-scenario-file`)" below.
32
36
  - `--output <directory>` — portable, relative output location for the
33
37
  observation artifact (same contract as the request's `outputLocation`; no
34
38
  drive letter, no leading `/`, no `..` segments).
@@ -59,7 +63,7 @@ diagnostics print one per line as `[code] message`.
59
63
 
60
64
  ### Structured semantic targets (`--targets-file`)
61
65
 
62
- **Current status: shipped as part of the published `my-frontend-observer@0.2.0`
66
+ **Current status: shipped as part of the published `my-frontend-observer@0.3.0`
63
67
  package.** `--target` (CSS shorthand) remains fully supported alongside it.
64
68
 
65
69
  `--targets-file <json-file>` is the public entry point to the v0.2 canonical
@@ -124,6 +128,240 @@ my-frontend-observer observe `
124
128
  --output observations
125
129
  ```
126
130
 
131
+ ### Scroll scenario (`--scroll-scenario-file`)
132
+
133
+ **Current status: shipped as part of the published `my-frontend-observer@0.3.0`
134
+ package.** Observation schema is `1.2.0`.
135
+
136
+ `--scroll-scenario-file <json-file>` is the public entry point to the v0.3
137
+ runtime scroll-scenario contract established in `src/request/request.ts`
138
+ (`ScrollScenario`/`ScrollAction`) and executed in `src/browser/`. It supplies
139
+ exactly the value of the normalized request's `scrollScenario` field - the
140
+ file root *is* the scenario object itself, with no wrapper field (unlike
141
+ `--targets-file`'s `{ "targets": [...] }` root).
142
+
143
+ A request supports **zero or one** scroll scenario. There are exactly two
144
+ supported action kinds:
145
+
146
+ Window scrolling:
147
+
148
+ ```json
149
+ {
150
+ "action": {
151
+ "kind": "window-scroll-by",
152
+ "deltaX": 0,
153
+ "deltaY": 600
154
+ }
155
+ }
156
+ ```
157
+
158
+ Target scrolling (the `target` value must be the stable `name` of one of the
159
+ observation's own configured targets - never a CSS selector, DOM id, or
160
+ source symbol):
161
+
162
+ ```json
163
+ {
164
+ "action": {
165
+ "kind": "target-scroll-by",
166
+ "target": "tool-workspace",
167
+ "deltaX": 0,
168
+ "deltaY": 400
169
+ }
170
+ }
171
+ ```
172
+
173
+ `deltaX`/`deltaY` are signed integers bounded to `[-20000, 20000]`; at least
174
+ one must be non-zero (both zero is rejected). Every scroll/action rule -
175
+ supported action kind, required fields, delta types/bounds, the both-zero
176
+ rule, and the stable-target-name reference for `target-scroll-by` - is
177
+ enforced by the same `normalizeRequest()` validator used everywhere else, not
178
+ duplicated in CLI code; `--scroll-scenario-file` itself only validates that
179
+ the file is readable, is valid JSON, and has a non-array object root.
180
+
181
+ The observer performs the requested scroll immediately (no smooth-scroll
182
+ animation), waits exactly two `requestAnimationFrame` cycles, and captures a
183
+ final runtime snapshot - the same final state that the observation's ordinary
184
+ `pageEvidence`, `targetEvidence`, and `screenshot.png` describe. The actual
185
+ resulting scroll position is browser-authoritative and may be clamped by
186
+ document/element boundaries; a scenario that produces no movement (already at
187
+ a boundary, or a non-scrollable target) is still a valid, successfully
188
+ persisted observation, never a fabricated failure.
189
+
190
+ Usable with either target input mode:
191
+
192
+ ```powershell
193
+ my-frontend-observer observe `
194
+ --url http://localhost:3000/ `
195
+ --target workspace=.workspace `
196
+ --scroll-scenario-file .\scroll.json `
197
+ --output observations
198
+ ```
199
+
200
+ ```powershell
201
+ my-frontend-observer observe `
202
+ --url http://localhost:3000/ `
203
+ --targets-file .\targets.json `
204
+ --scroll-scenario-file .\scroll.json `
205
+ --output observations
206
+ ```
207
+
208
+ `--target` and `--targets-file` remain mutually exclusive with each other,
209
+ exactly as before; `--scroll-scenario-file` is independent of both and is
210
+ never itself a third mutually-exclusive target mode. `window-scroll-by`
211
+ requires no configured target at all.
212
+
213
+ The path may be relative (resolved from the current working directory) or
214
+ absolute; it is operational input only - like `--targets-file`'s path, it
215
+ never affects the observation's request identity and is never written into
216
+ `manifest.json`. Two different scenario files with identical content produce
217
+ the same `requestId`; only the requested scenario *configuration*
218
+ participates in identity, never the runtime outcome (actual scroll
219
+ distance, clamping, or scroll-owner result).
220
+
221
+ If a `target-scroll-by` scenario's configured action target cannot be
222
+ uniquely resolved at runtime (missing, ambiguous, or otherwise unavailable),
223
+ the scroll is not performed, no movement is fabricated, and the observation
224
+ persists honestly - typically as `partial` - carrying the same
225
+ `target-missing`/`target-ambiguous`/`browser-evidence-unavailable` diagnostic
226
+ that any other unresolved configured target would produce.
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
+
127
365
  ## Foundation commands
128
366
 
129
367
  - `npm install` — install dependencies (includes the `playwright` runtime
@@ -146,8 +384,11 @@ my-frontend-observer observe `
146
384
  - `npm run build` — clean and compile `src/` (including `src/cli.ts`) to
147
385
  `dist/`.
148
386
  - `npm run check:docs` — validate canonical documents and roadmap structure.
149
- - `npm pack --dry-run` — inspect the private package inventory without
150
- publishing. The real tarball has been installed and exercised in a clean
151
- temporary consumer directory (real Chromium install, real `observe` run,
152
- real artifact) as part of v0.1 validation; this is local package
153
- validation, not a release/publication step.
387
+ - `npm pack --dry-run` — inspect the public package's tarball inventory
388
+ before publishing. The real tarball has been installed and exercised in a
389
+ clean temporary consumer directory (real Chromium install, real `observe`
390
+ run, real artifact) on Windows, Linux, and macOS as part of v0.1
391
+ validation, again for v0.2's packed semantic `--targets-file` behavior,
392
+ and again for v0.3's packed `--scroll-scenario-file` window/target scroll
393
+ behavior (`scripts/ci/runPackedObservationSmoke.mjs`); this is local
394
+ package validation, not a release/publication step.
package/docs/CONTRACTS.md CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  ## Current contracts
4
4
 
5
- The observation artifact contract is published as `my-frontend-observer@0.2.0`
5
+ The observation artifact contract is published as `my-frontend-observer@0.4.0`
6
6
  and proven both from the source checkout and from the packed npm tarball,
7
- on Windows, Linux, and macOS. The observation schema is `1.1.0` (see "v0.2
8
- target contract" below, shipped as part of this release):
7
+ on Windows, Linux, and macOS. The observation schema is `1.2.0` (see "v0.2
8
+ target contract" and "v0.3 scroll scenario contract" below):
9
9
 
10
- - artifact kind `my-frontend-observer/observation`, schema version `1.1.0`
10
+ - artifact kind `my-frontend-observer/observation`, schema version `1.2.0`
11
11
  (independent of the package version);
12
12
  - one artifact root per observation, `<outputLocation>/<observationId>/`,
13
13
  containing exactly `manifest.json` (the full `ObservationArtifact`, with
@@ -81,9 +81,174 @@ supplies the structured `{ "targets": [...] }` collection (see
81
81
  existing `--target id=css-selector` shorthand - the two are mutually
82
82
  exclusive per invocation, and both converge on the same
83
83
  `normalizeRequest()`/browser-resolver/artifact path, so a semantic
84
- observation produces exactly the same `manifest.json` shape (schema `1.1.0`)
85
- as a CSS-shorthand one. `--targets-file`'s local input path is never part of
86
- the persisted request identity or artifact.
84
+ observation produces exactly the same `manifest.json` shape as a
85
+ CSS-shorthand one. Schema `1.1.0` was the v0.2 published artifact schema;
86
+ the current v0.4 package emits schema `1.2.0` for both target-input modes
87
+ (target semantics are unchanged from v0.2 - see the v0.3 scroll scenario
88
+ contract below for what schema `1.2.0` actually adds). `--targets-file`'s
89
+ local input path is never part of the persisted request identity or
90
+ artifact.
91
+
92
+ ## v0.3 scroll scenario contract (shipped as part of this release)
93
+
94
+ v0.3 introduces one optional, additive request/evidence concern: a bounded
95
+ runtime scroll scenario, schema `1.2.0`.
96
+
97
+ A normalized request may carry `scrollScenario: { action }` with exactly one
98
+ of two frozen action kinds:
99
+
100
+ - `{ "kind": "window-scroll-by", "deltaX": <int>, "deltaY": <int> }`
101
+ - `{ "kind": "target-scroll-by", "target": "<stable target name>", "deltaX": <int>, "deltaY": <int> }`
102
+
103
+ `deltaX`/`deltaY` are signed integers bounded to `[-20000, 20000]`; at least
104
+ one must be non-zero. `target-scroll-by.target` refers only to an existing
105
+ stable configured target `name` (never a selector) and resolves through the
106
+ same canonical `resolveConfiguredTargets` algorithm every v0.2 locator kind
107
+ already uses - there is no second target-resolution path. A request with no
108
+ scenario normalizes and identifies exactly as it did before v0.3.
109
+
110
+ Execution (both action kinds share one code path): perform the immediate,
111
+ non-smooth scroll (`window.scrollBy`/`element.scrollBy`, `behavior:
112
+ 'instant'`) on the already-navigated, already-ready page; wait exactly two
113
+ `requestAnimationFrame` cycles; capture a final runtime snapshot. No second
114
+ browser, page, or navigation is ever created. The resulting scroll position
115
+ is browser-authoritative and may be clamped by document/element boundaries;
116
+ a scenario producing no movement is still a valid, successfully persisted
117
+ observation.
118
+
119
+ The scenario evidence lives entirely inside the existing `manifest.json` as
120
+ one additional optional `scrollScenarioEvidence` field on `ObservationArtifact`
121
+ - there is no separate `scroll.json`/`scenario.json`. It contains:
122
+
123
+ - `initial`/`final`: bounded `ScrollRuntimeSnapshot`s (window `scrollX`/
124
+ `scrollY`; the browser's own scrolling-root/`documentElement`/`body`
125
+ metrics; per-configured-target `scrollTop`/`scrollLeft`/`scrollWidth`/
126
+ `scrollHeight`/`clientWidth`/`clientHeight`, actual overflow, bounding
127
+ rectangle, and viewport relation);
128
+ - `transition`: bounded before/after change evidence (window scroll deltas;
129
+ per-target `scrollTop`/`scrollLeft`/bounding-position/viewport-relation
130
+ changes; `enteredViewport`/`leftViewport`) - never a generic recursive
131
+ diff, and a target is simply omitted when either side's evidence isn't
132
+ itself usable (e.g. it never resolved);
133
+ - `scrollOwner`: one derived `EvidenceField<ScrollOwnerInterpretation>`
134
+ (`document` | `target:<stable-name>` | `none` | `indeterminate`), always
135
+ `source: "derived"` with non-empty `derivedFrom` naming the exact
136
+ contributing scroll-position measurements. Ownership is derived only from
137
+ observed `scrollTop`/`scrollLeft`/`window.scrollX`/`window.scrollY`
138
+ changes - never from bounding-rectangle movement (which moves for every
139
+ configured target whenever the document scrolls), computed overflow,
140
+ `position: fixed`/`sticky`, or DOM hierarchy.
141
+
142
+ Actual dimensional overflow (`scrollWidth > clientWidth` /
143
+ `scrollHeight > clientHeight`) is always reported separately from the
144
+ computed `overflow-x`/`overflow-y` CSS declaration; a declared
145
+ `overflow: auto` container with content that fits produces
146
+ `horizontalOverflow`/`verticalOverflow: false`. Viewport relation
147
+ (`above`/`intersecting`/`below`, `intersectsViewport`, `fullyWithinViewport`)
148
+ is derived only from bounding geometry plus viewport size, relative to the
149
+ browser viewport; a hidden/non-rendered target's viewport relation is
150
+ `not-applicable`, never a fabricated geometry claim - hidden and offscreen
151
+ remain distinct evidence concepts, and the existing `target-hidden`
152
+ diagnostic is unaffected.
153
+
154
+ The ordinary, already-existing `pageEvidence`/`targetEvidence`/
155
+ `screenshot.png` for a scenario observation always describe this same final
156
+ post-action state, never the pre-action state.
157
+
158
+ The scenario request participates in `requestId`; the runtime result
159
+ (actual scroll distance, clamping, or scroll-owner outcome) never does. The
160
+ public entry point is `my-frontend-observer observe --scroll-scenario-file
161
+ <json-file>` (see `docs/COMMANDS.md`); the file supplies the scenario value
162
+ directly, and its local path is operational input only, exactly like
163
+ `--targets-file`'s path - never persisted, never part of request identity.
164
+
165
+ ## v0.4 comparison contract (shipped as part of this release)
166
+
167
+ **Current status: shipped as part of the published `my-frontend-observer@0.4.0`
168
+ package.** Observation schema remains `1.2.0`. Comparison is a distinct
169
+ artifact kind and schema, never a bump to the observation schema:
170
+
171
+ - artifact kind: `my-frontend-observer/comparison`;
172
+ - comparison schema: `1.0.0`.
173
+
174
+ **Geometry tolerance**: `ComparisonConfig.geometryTolerancePx`, default
175
+ `0.5` CSS px, bounded `[0, 10]`. Suppresses insignificant subpixel noise
176
+ only - never a design contract, never permission for a change.
177
+
178
+ **Layout relationship graph**: `deriveLayoutRelationships(observation,
179
+ options?)` derives, per observation, a bounded `LayoutRelationshipGraph`
180
+ among configured targets only (≤20 targets, ≤190 unordered pairs):
181
+ horizontal order (`left-of`/`right-of`/`horizontally-overlapping`),
182
+ vertical order (`above`/`below`/`vertically-overlapping`), area overlap
183
+ (`overlaps`/`does-not-overlap`), relative width (`wider-than`/
184
+ `narrower-than`/`equal-width-within-tolerance`), geometric fit
185
+ (`fits-inside`/`does-not-fit-inside` - geometry-only, deliberately distinct
186
+ from DOM containment), vertical sequencing (`follows-vertically`), and one
187
+ page-level relationship (`document-width-fits-viewport`/
188
+ `document-width-exceeds-viewport`). Every relationship carries explicit
189
+ evidence-path provenance back to the source observation. A configured
190
+ target lacking usable geometry is listed as honestly unresolved
191
+ (`not-found`/`ambiguous`/`unavailable`/`hidden`), never fabricated as a
192
+ zero-sized region.
193
+
194
+ **Comparability**: evaluated before any rendered difference, using exactly
195
+ three states (`comparable`/`comparable-with-warnings`/`incomparable`) with
196
+ structured reasons, never a bare boolean. Hard incompatibilities (page URL,
197
+ viewport, browser engine, scroll-scenario configuration mismatch) force
198
+ `incomparable`; producer-version, browser-version, and target-configuration
199
+ differences are warning-only; theme/authenticated-state/application-state
200
+ identity are recorded as `unassessed` dimensions the observer does not yet
201
+ model - never silently claimed identical. An `incomparable` result still
202
+ persists a structurally valid `ComparisonArtifact` with empty rendered
203
+ differences, not a fabricated comparison.
204
+
205
+ **Difference categories**: `appeared`/`disappeared` (only for a stable
206
+ target name configured on both sides, transitioning between a definite
207
+ `not-found` and `matched` resolution status - never for a target merely
208
+ added/removed from configuration, which is its own separate
209
+ `configurationChanges` entry), `moved`/`resized` (tolerance-aware, a target
210
+ may be both), `visibility-changed`, `clipping-changed` (reusing the
211
+ canonical `deriveTargetClipping` helper, never re-derived), `horizontal-
212
+ overflow-changed`/`vertical-overflow-changed` (actual dimensional overflow,
213
+ reusing the existing `deriveOverflowEvidence` helper - never inferred from
214
+ a CSS declaration alone), `containment-changed` (reusing existing v0.2
215
+ `TargetContainment` evidence), `page-size-changed`, `scroll-owner-changed`
216
+ (comparing `scrollScenarioEvidence.scrollOwner` only when scenario
217
+ *configuration* already matched), `relative-position-changed` (a relation
218
+ in the horizontal-order/vertical-order/area-overlap families changed - kept
219
+ distinct from plain absolute target movement) and `relationship-changed`
220
+ (every other relationship-family transition). Relationship changes are
221
+ matched by structural identity (family + subject/related target, or the
222
+ page-level key), never by array position.
223
+
224
+ **Explicit dependency evidence**: `ComparisonConfig.expectedDependencies`
225
+ lets a caller declare an expected relationship between two targets' numeric
226
+ properties (`x`/`y`/`width`/`height`) and directions (`increase`/
227
+ `decrease`/`change`/`unchanged`), always carrying `source:
228
+ "explicit-config"`. The observer never synthesizes a declaration from
229
+ observed co-change. Each declaration evaluates independently to exactly one
230
+ of `consistent`/`not-observed`/`contradictory-to-declaration`/
231
+ `unavailable` - never a causal claim (no `causedBy`/`causalConfidence`/
232
+ `causalScore`/`dependencyStrength`) and never a PASS/FAIL/approval verdict.
233
+ That distinction (evidence vs. contract verdict) is the boundary between
234
+ v0.4 and v0.5+.
235
+
236
+ **Comparison identity**: `comparisonRequestId` is a pure, deterministic
237
+ function of `{beforeObservationId, afterObservationId, normalized
238
+ ComparisonConfig}` - direction-sensitive (`compare(A, B) !==
239
+ compare(B, A)`), and never includes an operational filesystem path.
240
+ `comparisonId` is fresh per execution (same pattern as `observationId`).
241
+
242
+ **Source references**: the comparison artifact retains enough logical
243
+ identity to trace back to its authoritative source observations
244
+ (`observationId`, `requestId`, `producer`, `observationSchemaVersion`, and
245
+ the source `screenshot.path`) without embedding the full
246
+ `ObservationArtifact` or copying screenshot bytes. The persisted comparison
247
+ directory contains `manifest.json` only.
248
+
249
+ The public entry point is `my-frontend-observer compare --before <root>
250
+ --after <root> --output <directory> [--config-file <json-file>]` (see
251
+ `docs/COMMANDS.md`) - comparison itself never launches a browser.
87
252
 
88
253
  ## Approved v0.1 design inputs
89
254
 
@@ -1,7 +1,8 @@
1
1
  # Current State
2
2
 
3
- The project is published at package version `0.2.0` (roadmap v0.2, Stable
4
- Semantic Targets and Region Identity; observation schema `1.1.0`).
3
+ The project is published at package version `0.4.0` (roadmap v0.4, Layout
4
+ Relationships, Dependency Evidence, and Before/After Comparison; observation
5
+ schema `1.2.0`; comparison schema `1.0.0`).
5
6
 
6
7
  ## Greenfield foundation established
7
8
 
@@ -175,16 +176,153 @@ schema `1.1.0`.
175
176
  `dist/cli.js` (not just the imported `runCli()` function) performs a real
176
177
  semantic `--targets-file` observation end to end.
177
178
 
179
+ ## v0.3 status (Runtime Scrolling, Overflow, and Visibility Behavior) - released as 0.3.0
180
+
181
+ v0.3 is implemented and released as package version `0.3.0`, observation
182
+ schema `1.2.0`. It was validated as a packed npm tarball in a clean
183
+ consumer environment on Windows, Linux, and macOS before release.
184
+
185
+ - **Batch 1** froze the `scrollScenario` request/identity/schema contract:
186
+ `ScrollScenario { action }` with exactly two action kinds
187
+ (`window-scroll-by`, `target-scroll-by`), signed-integer deltas bounded to
188
+ `[-20000, 20000]`, `target-scroll-by.target` referencing an existing stable
189
+ configured target name, scenario configuration participating in
190
+ `requestId` (runtime results never do), and the full bounded runtime
191
+ evidence model (`ScrollRuntimeSnapshot`, `ViewportRelationEvidence`,
192
+ `OverflowEvidence`, scenario transitions, `ScrollOwnerInterpretation`) in
193
+ schema `1.2.0` (up from `1.1.0`).
194
+ - **Batch 2** implemented real `window-scroll-by` execution
195
+ (`src/browser/scrollCapture.ts`, `src/domain/scrollEvidence.ts`): initial/
196
+ final runtime snapshots around an immediate `window.scrollBy({behavior:
197
+ 'instant'})` and exactly two `requestAnimationFrame` cycles, real vertical/
198
+ horizontal document scrolling, actual-vs-computed overflow, real viewport
199
+ relation, `enteredViewport`/`leftViewport`, and `document`/`none`
200
+ scroll-owner evidence - with ordinary final `pageEvidence`/`targetEvidence`
201
+ and the screenshot always describing the same final post-action state.
202
+ - **Batch 3** implemented real `target-scroll-by` execution against the same
203
+ canonical `resolveConfiguredTargets` resolution already used by every v0.2
204
+ locator kind: real nested vertical/horizontal element scrolling, boundary
205
+ clamping, non-scrollable/no-movement targets, and the completed
206
+ `document`/`target:<name>`/`none`/`indeterminate` scroll-owner derivation
207
+ (`src/domain/scrollEvidence.ts#deriveScrollOwner`) - proven never to
208
+ attribute ownership from bounding-rectangle movement alone in either
209
+ direction. An unresolved/ambiguous/hidden action target is never scrolled
210
+ and never fabricated as moved; the existing target diagnostics explain it
211
+ honestly and the observation still persists.
212
+ - **Batch 4** exposed the existing contract through the real public CLI:
213
+ `my-frontend-observer observe --scroll-scenario-file <json-file>` (see
214
+ `docs/COMMANDS.md`). The file supplies the `scrollScenario` value directly
215
+ (no wrapper field); the CLI/input layer only validates file readability,
216
+ JSON validity, and a non-array object root - every scenario/action rule
217
+ stays owned by the existing `normalizeRequest()`. Usable with either
218
+ `--target` or `--targets-file` (independent of target configuration, never
219
+ a third mutually-exclusive mode); the scenario-file path is operational
220
+ input only, never persisted and never part of request identity, exactly
221
+ like `--targets-file`'s path. CLI output/exit-code semantics are
222
+ unchanged. Proven via real Chromium (`tests/browser/cliObserve.test.ts`)
223
+ and the built `dist/cli.js` (`scripts/dev/builtCliScrollScenarioSmoke.mjs`).
224
+
225
+ ## v0.4 status (Layout Relationships, Dependency Evidence, and Before/After Comparison) - released as 0.4.0
226
+
227
+ v0.4 is implemented and released as package version `0.4.0`; observation
228
+ schema remains `1.2.0`; comparison schema is `1.0.0`. It was validated as a
229
+ packed npm tarball in a clean consumer environment on Windows, Linux, and
230
+ macOS - covering the legacy CSS-shorthand `--target` path, the structured
231
+ `--targets-file` path, both `--scroll-scenario-file` action kinds, and the
232
+ installed `compare` command (comparable and incomparable cases) - before
233
+ release.
234
+
235
+ - **Batch 1** froze the `my-frontend-observer/comparison` artifact contract
236
+ (schema `1.0.0`, independent of and never reused for the observation
237
+ schema): `ComparisonConfig` (geometry tolerance, default `0.5`px, bounded
238
+ `[0, 10]`px), the bounded layout-relationship vocabulary (horizontal/
239
+ vertical order, area overlap, relative width, geometric fit, vertical
240
+ sequencing, page-width fit, clipping), comparability states, the
241
+ before/after difference vocabulary, and the non-causal explicit
242
+ dependency-evidence contract, plus `comparisonRequestId`/`comparisonId`
243
+ identity (`src/domain/relationships.ts`, `src/domain/comparison.ts`,
244
+ `src/domain/comparisonIdentity.ts`). No derivation, comparison, or
245
+ persistence.
246
+ - **Batch 2** implemented the one canonical pure derivation engine,
247
+ `deriveLayoutRelationships(observation, options?)`
248
+ (`src/domain/relationships.ts`): consumes an existing `ObservationArtifact`
249
+ only (no Chromium, no re-resolution, no DOM access) and derives a bounded,
250
+ traceable `LayoutRelationshipGraph` among configured targets - stable
251
+ target identity, deterministic configured-target ordering, honest
252
+ unresolved-target handling (not-found/ambiguous/unavailable/hidden, never
253
+ a fabricated zero-sized region), and evidence-reference provenance for
254
+ every derived relationship. DOM containment is read directly from the
255
+ existing `TargetContainment` evidence rather than re-derived, and stays
256
+ distinct from geometric fit. A standalone `deriveTargetClipping(record)`
257
+ derives the frozen clipping concept per target from existing layout/style
258
+ evidence.
259
+ - **Batch 3** implemented the pure before/after comparison engine,
260
+ `compareObservations(before, after, config?)`
261
+ (`src/domain/comparisonEngine.ts`): validates both source observations,
262
+ evaluates comparability before any rendered difference is calculated
263
+ (hard page-URL/viewport/browser-engine/scroll-scenario mismatches;
264
+ producer/browser-version and target-configuration warnings), reuses
265
+ `deriveLayoutRelationships` unchanged for both sides, and derives target/
266
+ page differences (appeared/disappeared, moved, resized, visibility,
267
+ clipping, actual overflow, DOM containment, page size, scroll-owner) and
268
+ relationship changes (matched by family + subject/related target, never
269
+ array position) - all without launching Chromium, re-resolving targets, or
270
+ mutating either input observation. Explicit `ComparisonConfig.
271
+ expectedDependencies` are evaluated into non-causal
272
+ consistent/not-observed/contradictory-to-declaration/unavailable outcomes
273
+ only; the observer never infers a dependency from co-change. Comparison
274
+ identity reuses the existing Batch 1 `buildComparisonRequestIdentity`/
275
+ `buildComparisonIdentity` verbatim. Persistence
276
+ (`src/artifacts/comparisonArtifactWriter.ts#writeComparisonArtifact`,
277
+ atomic, `<outputLocation>/<comparisonId>/manifest.json` only, no copied
278
+ screenshots) and the application-level `compareAndPersist` use case
279
+ (`src/application/comparisonService.ts`) are implemented; a narrow
280
+ `readObservationArtifact` reader
281
+ (`src/artifacts/artifactReader.ts`) is established ahead of the Batch 4
282
+ CLI.
283
+ - **Batch 4** exposed the existing comparison workflow through the real
284
+ public CLI: `my-frontend-observer compare --before <observation-artifact-
285
+ root> --after <observation-artifact-root> --output <directory>
286
+ [--config-file <json-file>]` (see `docs/COMMANDS.md`). The CLI stays thin
287
+ - `src/cli.ts` parses arguments, optionally loads a config file (file
288
+ readability/JSON validity/object-root only, exactly like
289
+ `--targets-file`/`--scroll-scenario-file`), and delegates to one new
290
+ thin application-layer orchestration function,
291
+ `compareAndPersistFromArtifactRoots`
292
+ (`src/application/comparisonService.ts`), which reads both observation
293
+ roots through the existing `readObservationArtifact` reader and calls the
294
+ existing `compareAndPersist` exactly once - no comparability/geometry/
295
+ relationship/dependency logic lives in the CLI, and comparison itself
296
+ never launches Chromium (`src/cli.ts` still imports nothing from
297
+ `src/artifacts/` or `src/browser/`, matching the pre-existing observe-CLI
298
+ import-boundary test). `comparable`, `comparable-with-warnings`, and
299
+ `incomparable` all exit `0` - each is a successful comparison outcome;
300
+ only a genuine parse/read/domain/persistence failure exits nonzero.
301
+ Operational paths (`--before`/`--after`/`--config-file`/`--output`) never
302
+ affect `comparisonRequestId` and are never written into the persisted
303
+ manifest. Proven end-to-end via real Chromium
304
+ (`tests/browser/cliCompare.test.ts`) and the built `dist/cli.js`
305
+ (`scripts/dev/builtCliCompareSmoke.mjs`): unchanged/moved/resized/
306
+ appeared/disappeared/configuration-only-change/overlap/geometric-fit/
307
+ page-overflow/clipping/scroll-owner cases, plus an explicit
308
+ `--config-file` dependency-evidence case, all through the public command
309
+ surface.
310
+
311
+ v0.4's canonical relationship derivation, before/after comparison,
312
+ comparability, differences, relationship changes, explicit dependency
313
+ evidence, comparison persistence, and public `compare` CLI are all
314
+ implemented, exercised end-to-end, packed-validated cross-platform, and
315
+ released.
316
+
178
317
  ## Not implemented
179
318
 
180
- - No controlled-scroll behavior, layout/spatial relationship engine,
181
- before/after comparison, frontend contracts/change scope, source
182
- ownership, my-dev-kit runtime/static integration, orchestrator/lab
183
- product integration, viewer, or annotation - all remain v0.3+ and
184
- unimplemented.
185
- - No v0.3–v0.10 capability is implemented.
319
+ - v0.5+ frontend contracts/change scope (baseline approval, requested/
320
+ protected/preserved change scope, PASS/FAIL verdicts), source ownership,
321
+ my-dev-kit runtime/static integration, orchestrator/lab product
322
+ integration, viewer, and annotation all remain unimplemented.
186
323
 
187
324
  ## Next target
188
325
 
189
- v0.2 is implemented, validated, and released as `0.2.0`. v0.3 (Controlled
190
- Scroll and Overflow Scenarios) is the next planned version.
326
+ v0.1-v0.4 are implemented, validated, and released (`0.1.0`, `0.2.0`,
327
+ `0.3.0`, `0.4.0`). v0.5 (Executable Frontend Contracts and Explicit Change
328
+ Scope) is next.