my-frontend-observer 0.3.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 (44) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +32 -8
  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/artifacts/artifactReader.d.ts +19 -0
  7. package/dist/artifacts/artifactReader.js +36 -0
  8. package/dist/artifacts/artifactReader.js.map +1 -0
  9. package/dist/artifacts/comparisonArtifactWriter.d.ts +41 -0
  10. package/dist/artifacts/comparisonArtifactWriter.js +67 -0
  11. package/dist/artifacts/comparisonArtifactWriter.js.map +1 -0
  12. package/dist/cli.js +220 -1
  13. package/dist/cli.js.map +1 -1
  14. package/dist/domain/comparison.d.ts +198 -0
  15. package/dist/domain/comparison.js +324 -0
  16. package/dist/domain/comparison.js.map +1 -0
  17. package/dist/domain/comparisonEngine.d.ts +48 -0
  18. package/dist/domain/comparisonEngine.js +694 -0
  19. package/dist/domain/comparisonEngine.js.map +1 -0
  20. package/dist/domain/comparisonIdentity.d.ts +13 -0
  21. package/dist/domain/comparisonIdentity.js +46 -0
  22. package/dist/domain/comparisonIdentity.js.map +1 -0
  23. package/dist/domain/evidence.d.ts +2 -0
  24. package/dist/domain/evidence.js +4 -0
  25. package/dist/domain/evidence.js.map +1 -1
  26. package/dist/domain/relationships.d.ts +168 -0
  27. package/dist/domain/relationships.js +343 -0
  28. package/dist/domain/relationships.js.map +1 -0
  29. package/dist/index.d.ts +14 -1
  30. package/dist/index.js +8 -1
  31. package/dist/index.js.map +1 -1
  32. package/docs/ARCHITECTURE.md +76 -3
  33. package/docs/CI_CD.md +29 -0
  34. package/docs/COMMANDS.md +137 -0
  35. package/docs/CONTRACTS.md +96 -4
  36. package/docs/CURRENT_STATE.md +102 -9
  37. package/docs/DEVELOPMENT.md +33 -10
  38. package/docs/PROJECT_OVERVIEW.md +16 -12
  39. package/docs/QUICKSTART.md +5 -0
  40. package/docs/RELEASE.md +10 -8
  41. package/docs/ROADMAP.md +4 -0
  42. package/docs/SECURITY.md +14 -1
  43. package/docs/WORKFLOWS.md +56 -6
  44. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 - 2026-08-12
4
+
5
+ Layout Relationships, Dependency Evidence, and Before/After Comparison.
6
+
7
+ - New comparison artifact kind `my-frontend-observer/comparison`, schema
8
+ `1.0.0` - independent of and never reused for the observation schema.
9
+ - Canonical layout-relationship derivation from a single observation:
10
+ horizontal order (`left-of`/`right-of`/`horizontally-overlapping`),
11
+ vertical order (`above`/`below`/`vertically-overlapping`), area overlap,
12
+ relative width, geometric fit (kept explicitly distinct from DOM
13
+ containment), vertical sequencing (`follows-vertically`), and document-
14
+ width fit/exceeds-viewport - bounded to configured targets, with explicit
15
+ evidence-path provenance and honest unresolved-target handling.
16
+ - Comparability analysis, evaluated before any rendered difference:
17
+ `comparable` / `comparable-with-warnings` / `incomparable`, with
18
+ structured reasons (hard mismatches on page URL, viewport, browser
19
+ engine, or scroll-scenario configuration; warnings for producer/browser
20
+ version and target-configuration differences; theme/authenticated-state/
21
+ application-state recorded as unassessed, never silently equal).
22
+ - Before/after target and page differences: appeared/disappeared (never
23
+ confused with a target added/removed from configuration), moved, resized,
24
+ visibility changes, clipping changes (reusing one canonical clipping
25
+ derivation), actual horizontal/vertical dimensional-overflow changes, DOM
26
+ containment changes, page-size changes, and scroll-owner changes - each a
27
+ structured record with before/after values, deltas where meaningful, and
28
+ supporting evidence references.
29
+ - Relationship-change detection between two observations, matched by
30
+ relationship family and subject/related target (never array position),
31
+ including a `relative-position-changed` distinction from plain absolute
32
+ target movement.
33
+ - Explicit, non-causal expected-dependency evidence: a caller may declare an
34
+ expected relationship between two targets' `x`/`y`/`width`/`height`
35
+ properties and `increase`/`decrease`/`change`/`unchanged` directions; each
36
+ declaration evaluates independently to `consistent` / `not-observed` /
37
+ `contradictory-to-declaration` / `unavailable`. The observer never infers
38
+ a dependency from observed co-change and never produces a causal claim.
39
+ - Deterministic, direction-sensitive comparison identity
40
+ (`comparisonRequestId`) plus a fresh `comparisonId` per execution;
41
+ operational filesystem paths never affect identity and are never written
42
+ into the persisted manifest.
43
+ - Atomic comparison-artifact persistence: `<outputLocation>/<comparisonId>/
44
+ manifest.json` only - no screenshot bytes are copied; the manifest
45
+ retains logical references to the source observations' own
46
+ `screenshot.path`. Source observations are never modified.
47
+ - New public `compare` command: `my-frontend-observer compare --before
48
+ <observation-artifact-root> --after <observation-artifact-root> --output
49
+ <directory> [--config-file <json-file>]`. Reads two already-persisted
50
+ observation artifacts and never launches a browser. `comparable`,
51
+ `comparable-with-warnings`, and `incomparable` all persist successfully
52
+ and exit `0`; only invalid syntax, an unreadable/invalid source artifact,
53
+ invalid configuration, or a failed write exits nonzero.
54
+
3
55
  ## 0.3.0 - 2026-08-12
4
56
 
5
57
  Runtime Scrolling, Overflow, and Visibility Behavior.
package/README.md CHANGED
@@ -6,12 +6,15 @@ in [docs/PROJECT_DESCRIPTION.md](docs/PROJECT_DESCRIPTION.md).
6
6
 
7
7
  ## Current status
8
8
 
9
- `v0.3.0`, Runtime Scrolling, Overflow, and Visibility Behavior, is the
10
- current published release: `my-frontend-observer observe` launches a real,
11
- sandboxed Chromium browser, enforces a loopback-only safety policy,
12
- captures a viewport screenshot plus bounded page/target evidence, and
13
- persists it as one portable `manifest.json` + `screenshot.png` artifact
14
- (observation schema `1.2.0`).
9
+ `v0.4.0`, Layout Relationships, Dependency Evidence, and Before/After
10
+ Comparison, is the current published release: `my-frontend-observer observe`
11
+ launches a real, sandboxed Chromium browser, enforces a loopback-only safety
12
+ policy, captures a viewport screenshot plus bounded page/target evidence,
13
+ and persists it as one portable `manifest.json` + `screenshot.png` artifact
14
+ (observation schema `1.2.0`). `my-frontend-observer compare` reads two
15
+ already-persisted observation artifacts and derives before/after evidence
16
+ purely from their existing content, persisting a comparison artifact
17
+ (comparison schema `1.0.0`).
15
18
 
16
19
  Install:
17
20
 
@@ -67,6 +70,27 @@ in
67
70
  [docs/COMMANDS.md](docs/COMMANDS.md#scroll-scenario---scroll-scenario-file)
68
71
  for the exact JSON format and flag reference.
69
72
 
73
+ ### Comparison
74
+
75
+ `my-frontend-observer compare` reads two already-persisted observation
76
+ artifacts and derives before/after evidence purely from their existing
77
+ content - it never launches a browser:
78
+
79
+ ```powershell
80
+ my-frontend-observer compare `
81
+ --before observations/<before-observation-id> `
82
+ --after observations/<after-observation-id> `
83
+ --output comparisons
84
+ ```
85
+
86
+ (From a source checkout, use `node dist/cli.js compare ...` instead.)
87
+
88
+ This prints a concise result (`Comparison:`/`State:`/`Artifact:`/
89
+ `Differences:`/`Relationship changes:`/`Diagnostics:`) and exits `0` -
90
+ including when the two observations turn out to be `incomparable`, which is
91
+ itself a successful comparison outcome. See
92
+ [docs/COMMANDS.md](docs/COMMANDS.md) for the full flag reference.
93
+
70
94
  Validation:
71
95
 
72
96
  ```powershell
@@ -84,8 +108,8 @@ Planning authorities:
84
108
  intent and responsibility boundaries.
85
109
  - [Project Milestones](docs/PROJECT_MILESTONES.md): complete ordered capability
86
110
  design and cross-milestone rules.
87
- - [ROADMAP](docs/ROADMAP.md): version-level requirements; v0.1, v0.2, and
88
- v0.3 are released, v0.4 is next.
111
+ - [ROADMAP](docs/ROADMAP.md): version-level requirements; v0.1-v0.4 are
112
+ released, v0.5 is next.
89
113
  - [Current State](docs/CURRENT_STATE.md): retained scaffold and release state.
90
114
 
91
115
  No sibling ecosystem repository is a runtime dependency of the retained
@@ -0,0 +1,56 @@
1
+ import type { ObservationArtifact } from '../domain/schema.js';
2
+ import type { ComparabilityState, ComparisonConfig } from '../domain/comparison.js';
3
+ import type { Diagnostic } from '../domain/diagnostics.js';
4
+ import type { WriteComparisonArtifactOptions } from '../artifacts/comparisonArtifactWriter.js';
5
+ export type { PersistedComparisonResult } from '../artifacts/comparisonArtifactWriter.js';
6
+ /** Default comparison output location, mirroring `request.ts`'s `DEFAULT_OUTPUT_LOCATION` for observations - distinct directories so comparisons never collide with source observations. */
7
+ export declare const DEFAULT_COMPARISON_OUTPUT_LOCATION = "comparisons";
8
+ export interface CompareAndPersistOptions {
9
+ config?: Partial<ComparisonConfig>;
10
+ /** Portable, relative output location (validated the same way as an observation's `outputLocation`). Defaults to `DEFAULT_COMPARISON_OUTPUT_LOCATION`. */
11
+ outputLocation?: string;
12
+ cwd?: WriteComparisonArtifactOptions['cwd'];
13
+ }
14
+ /**
15
+ * Observer-owned summary of one full compare-and-persist attempt, with just
16
+ * enough information for a presentation layer (a future Batch 4 CLI) to
17
+ * report a result without seeing a raw filesystem exception or persistence
18
+ * implementation detail.
19
+ */
20
+ export type ApplicationComparisonResult = {
21
+ ok: true;
22
+ comparisonId: string;
23
+ comparisonRequestId: string;
24
+ comparability: ComparabilityState;
25
+ artifactRoot: string;
26
+ manifestPath: string;
27
+ differenceCount: number;
28
+ relationshipChangeCount: number;
29
+ diagnosticsCount: number;
30
+ } | {
31
+ ok: false;
32
+ diagnostics: Diagnostic[];
33
+ };
34
+ /**
35
+ * The one canonical application-level comparison use case: takes two
36
+ * already-validated/in-memory `ObservationArtifact`s, runs the existing pure
37
+ * `compareObservations` exactly once, and - only for a structurally valid
38
+ * result - persists it exactly once through the existing comparison
39
+ * writer. Never launches Chromium, never re-resolves targets, never mutates
40
+ * either source observation.
41
+ */
42
+ export declare function compareAndPersist(before: ObservationArtifact, after: ObservationArtifact, options?: CompareAndPersistOptions): Promise<ApplicationComparisonResult>;
43
+ /**
44
+ * The canonical Batch 4 CLI-facing orchestration: reads two already-persisted
45
+ * observation artifacts by their root directory (each expected to contain
46
+ * `manifest.json`, mirroring `writeObservationArtifact`'s own layout) through
47
+ * the existing `readObservationArtifact` reader, then delegates to
48
+ * `compareAndPersist` exactly once. A thin wrapper only - artifact reading
49
+ * stays owned by `artifacts/artifactReader.ts`, comparison stays owned by
50
+ * `domain/comparisonEngine.ts`, and persistence stays owned by
51
+ * `artifacts/comparisonArtifactWriter.ts`. Kept in `application/` (not
52
+ * `artifacts/`) so `src/cli.ts` can depend on it without importing the
53
+ * artifacts layer directly - see `tests/unit/cli.test.ts`'s import-boundary
54
+ * assertion. Never launches a browser, never re-resolves targets.
55
+ */
56
+ export declare function compareAndPersistFromArtifactRoots(beforeRoot: string, afterRoot: string, options?: CompareAndPersistOptions): Promise<ApplicationComparisonResult>;
@@ -0,0 +1,77 @@
1
+ import path from 'node:path';
2
+ import { compareObservations } from '../domain/comparisonEngine.js';
3
+ import { DIAGNOSTIC_SEVERITY } from '../domain/diagnostics.js';
4
+ import { normalizeOutputLocation } from '../request/paths.js';
5
+ import { writeComparisonArtifact } from '../artifacts/comparisonArtifactWriter.js';
6
+ import { readObservationArtifact } from '../artifacts/artifactReader.js';
7
+ import { MANIFEST_FILENAME as OBSERVATION_MANIFEST_FILENAME } from '../artifacts/artifactWriter.js';
8
+ /** Default comparison output location, mirroring `request.ts`'s `DEFAULT_OUTPUT_LOCATION` for observations - distinct directories so comparisons never collide with source observations. */
9
+ export const DEFAULT_COMPARISON_OUTPUT_LOCATION = 'comparisons';
10
+ /**
11
+ * The one canonical application-level comparison use case: takes two
12
+ * already-validated/in-memory `ObservationArtifact`s, runs the existing pure
13
+ * `compareObservations` exactly once, and - only for a structurally valid
14
+ * result - persists it exactly once through the existing comparison
15
+ * writer. Never launches Chromium, never re-resolves targets, never mutates
16
+ * either source observation.
17
+ */
18
+ export async function compareAndPersist(before, after, options = {}) {
19
+ const result = compareObservations(before, after, options.config ?? {});
20
+ if (!result.ok) {
21
+ return {
22
+ ok: false,
23
+ diagnostics: [{ code: 'invalid-request', severity: DIAGNOSTIC_SEVERITY['invalid-request'], message: result.reason }],
24
+ };
25
+ }
26
+ const rawOutputLocation = options.outputLocation ?? DEFAULT_COMPARISON_OUTPUT_LOCATION;
27
+ const normalized = normalizeOutputLocation(rawOutputLocation);
28
+ if (!normalized.ok) {
29
+ return { ok: false, diagnostics: [normalized.diagnostic] };
30
+ }
31
+ const persisted = await writeComparisonArtifact(result.artifact, normalized.value, options.cwd === undefined ? {} : { cwd: options.cwd });
32
+ if (!persisted.ok) {
33
+ return { ok: false, diagnostics: persisted.diagnostics };
34
+ }
35
+ return {
36
+ ok: true,
37
+ comparisonId: result.artifact.comparisonId,
38
+ comparisonRequestId: result.artifact.comparisonRequestId,
39
+ comparability: result.artifact.comparability.state,
40
+ artifactRoot: persisted.artifactRoot,
41
+ manifestPath: persisted.manifestPath,
42
+ differenceCount: result.artifact.differences.length,
43
+ relationshipChangeCount: result.artifact.relationshipChanges.length,
44
+ diagnosticsCount: result.artifact.diagnostics.length,
45
+ };
46
+ }
47
+ /**
48
+ * The canonical Batch 4 CLI-facing orchestration: reads two already-persisted
49
+ * observation artifacts by their root directory (each expected to contain
50
+ * `manifest.json`, mirroring `writeObservationArtifact`'s own layout) through
51
+ * the existing `readObservationArtifact` reader, then delegates to
52
+ * `compareAndPersist` exactly once. A thin wrapper only - artifact reading
53
+ * stays owned by `artifacts/artifactReader.ts`, comparison stays owned by
54
+ * `domain/comparisonEngine.ts`, and persistence stays owned by
55
+ * `artifacts/comparisonArtifactWriter.ts`. Kept in `application/` (not
56
+ * `artifacts/`) so `src/cli.ts` can depend on it without importing the
57
+ * artifacts layer directly - see `tests/unit/cli.test.ts`'s import-boundary
58
+ * assertion. Never launches a browser, never re-resolves targets.
59
+ */
60
+ export async function compareAndPersistFromArtifactRoots(beforeRoot, afterRoot, options = {}) {
61
+ const beforeRead = await readObservationArtifact(path.join(beforeRoot, OBSERVATION_MANIFEST_FILENAME));
62
+ if (!beforeRead.ok) {
63
+ return {
64
+ ok: false,
65
+ diagnostics: [{ code: 'invalid-request', severity: DIAGNOSTIC_SEVERITY['invalid-request'], message: `--before observation artifact: ${beforeRead.reason}` }],
66
+ };
67
+ }
68
+ const afterRead = await readObservationArtifact(path.join(afterRoot, OBSERVATION_MANIFEST_FILENAME));
69
+ if (!afterRead.ok) {
70
+ return {
71
+ ok: false,
72
+ diagnostics: [{ code: 'invalid-request', severity: DIAGNOSTIC_SEVERITY['invalid-request'], message: `--after observation artifact: ${afterRead.reason}` }],
73
+ };
74
+ }
75
+ return compareAndPersist(beforeRead.artifact, afterRead.artifact, options);
76
+ }
77
+ //# sourceMappingURL=comparisonService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comparisonService.js","sourceRoot":"","sources":["../../src/application/comparisonService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { ObservationArtifact } from '../domain/schema.js';
2
+ export type ReadObservationArtifactResult = {
3
+ ok: true;
4
+ artifact: ObservationArtifact;
5
+ } | {
6
+ ok: false;
7
+ reason: string;
8
+ };
9
+ /**
10
+ * Narrow canonical reader counterpart to `artifactWriter.ts#writeObservationArtifact`:
11
+ * reads one `manifest.json`, parses it, and validates it through the same
12
+ * `isValidObservationArtifact` structural gate the writer itself uses -
13
+ * never a second validator. Read-only: never mutates the file, never
14
+ * launches a browser, never resolves the sibling screenshot's bytes (the
15
+ * manifest's `screenshot.path` field is returned as-is for the caller to
16
+ * resolve if it needs to). Established here as the canonical reader ahead of
17
+ * the Batch 4 `compare` CLI's `--before`/`--after` path arguments.
18
+ */
19
+ export declare function readObservationArtifact(manifestPath: string): Promise<ReadObservationArtifactResult>;
@@ -0,0 +1,36 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { isValidObservationArtifact } from '../domain/schema.js';
3
+ /**
4
+ * Narrow canonical reader counterpart to `artifactWriter.ts#writeObservationArtifact`:
5
+ * reads one `manifest.json`, parses it, and validates it through the same
6
+ * `isValidObservationArtifact` structural gate the writer itself uses -
7
+ * never a second validator. Read-only: never mutates the file, never
8
+ * launches a browser, never resolves the sibling screenshot's bytes (the
9
+ * manifest's `screenshot.path` field is returned as-is for the caller to
10
+ * resolve if it needs to). Established here as the canonical reader ahead of
11
+ * the Batch 4 `compare` CLI's `--before`/`--after` path arguments.
12
+ */
13
+ export async function readObservationArtifact(manifestPath) {
14
+ let raw;
15
+ try {
16
+ raw = await readFile(manifestPath, 'utf8');
17
+ }
18
+ catch (err) {
19
+ const message = err instanceof Error ? err.message : String(err);
20
+ return { ok: false, reason: `failed to read observation artifact manifest at "${manifestPath}": ${message}` };
21
+ }
22
+ let parsed;
23
+ try {
24
+ parsed = JSON.parse(raw);
25
+ }
26
+ catch (err) {
27
+ const message = err instanceof Error ? err.message : String(err);
28
+ return { ok: false, reason: `observation artifact manifest at "${manifestPath}" is not valid JSON: ${message}` };
29
+ }
30
+ const validation = isValidObservationArtifact(parsed);
31
+ if (!validation.valid) {
32
+ return { ok: false, reason: `observation artifact manifest at "${manifestPath}" failed structural validation: ${validation.reason}` };
33
+ }
34
+ return { ok: true, artifact: parsed };
35
+ }
36
+ //# sourceMappingURL=artifactReader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifactReader.js","sourceRoot":"","sources":["../../src/artifacts/artifactReader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAIjE;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,YAAoB;IAChE,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oDAAoD,YAAY,MAAM,OAAO,EAAE,EAAE,CAAC;IAChH,CAAC;IAED,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,qCAAqC,YAAY,wBAAwB,OAAO,EAAE,EAAE,CAAC;IACnH,CAAC;IAED,MAAM,UAAU,GAAG,0BAA0B,CAAC,MAAM,CAAC,CAAC;IACtD,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACtB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,qCAAqC,YAAY,mCAAmC,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC;IACxI,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAA6B,EAAE,CAAC;AAC/D,CAAC"}
@@ -0,0 +1,41 @@
1
+ import type { ComparisonArtifact } from '../domain/comparison.js';
2
+ import type { Diagnostic } from '../domain/diagnostics.js';
3
+ export declare const COMPARISON_MANIFEST_FILENAME = "manifest.json";
4
+ /**
5
+ * Observer-owned result of persisting one ComparisonArtifact. Mirrors
6
+ * `PersistedObservationResult`'s shape/semantics but is its own type: a
7
+ * comparison directory has no screenshot file, and this writer never touches
8
+ * `src/artifacts/artifactWriter.ts` or its observation-persistence
9
+ * guarantees.
10
+ */
11
+ export type PersistedComparisonResult = {
12
+ ok: true;
13
+ artifactRoot: string;
14
+ manifestPath: string;
15
+ } | {
16
+ ok: false;
17
+ diagnostics: Diagnostic[];
18
+ };
19
+ export interface WriteComparisonArtifactOptions {
20
+ /** Base directory that `outputLocation` is resolved against. Defaults to `process.cwd()`. */
21
+ cwd?: string;
22
+ }
23
+ /**
24
+ * Persists one already-assembled, already-validated `ComparisonArtifact` as
25
+ * one portable artifact directory: `<outputLocation>/<comparisonId>/manifest.json`
26
+ * only - no screenshot is copied (the manifest's `before`/`after.screenshot`
27
+ * fields already point back to the source observations; see
28
+ * `domain/comparisonEngine.ts#sourceReference`), and no `relationships.json`/
29
+ * `differences.json`/`diff.png` side files are produced.
30
+ *
31
+ * Follows the same atomic-write discipline as
32
+ * `artifacts/artifactWriter.ts#writeObservationArtifact` (sibling temporary
33
+ * directory, then one atomic rename onto the final name) without sharing its
34
+ * implementation - a comparison directory has a different, smaller content
35
+ * shape, and this function never touches observation persistence. A fresh
36
+ * `comparisonId` per execution normally prevents collisions; an existing
37
+ * directory at the final path is treated as a genuine collision and
38
+ * rejected rather than overwritten, so prior comparison evidence is never
39
+ * destructively replaced.
40
+ */
41
+ export declare function writeComparisonArtifact(artifact: ComparisonArtifact, outputLocation: string, options?: WriteComparisonArtifactOptions): Promise<PersistedComparisonResult>;
@@ -0,0 +1,67 @@
1
+ import { mkdir, writeFile, rename, rm, access } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { isValidComparisonArtifact } from '../domain/comparison.js';
4
+ import { DIAGNOSTIC_SEVERITY } from '../domain/diagnostics.js';
5
+ export const COMPARISON_MANIFEST_FILENAME = 'manifest.json';
6
+ function writeFailure(message, details) {
7
+ const diagnostic = {
8
+ code: 'artifact-write-failure',
9
+ severity: DIAGNOSTIC_SEVERITY['artifact-write-failure'],
10
+ message,
11
+ ...(details === undefined ? {} : { details }),
12
+ };
13
+ return { ok: false, diagnostics: [diagnostic] };
14
+ }
15
+ async function pathExists(candidate) {
16
+ return access(candidate).then(() => true, () => false);
17
+ }
18
+ /**
19
+ * Persists one already-assembled, already-validated `ComparisonArtifact` as
20
+ * one portable artifact directory: `<outputLocation>/<comparisonId>/manifest.json`
21
+ * only - no screenshot is copied (the manifest's `before`/`after.screenshot`
22
+ * fields already point back to the source observations; see
23
+ * `domain/comparisonEngine.ts#sourceReference`), and no `relationships.json`/
24
+ * `differences.json`/`diff.png` side files are produced.
25
+ *
26
+ * Follows the same atomic-write discipline as
27
+ * `artifacts/artifactWriter.ts#writeObservationArtifact` (sibling temporary
28
+ * directory, then one atomic rename onto the final name) without sharing its
29
+ * implementation - a comparison directory has a different, smaller content
30
+ * shape, and this function never touches observation persistence. A fresh
31
+ * `comparisonId` per execution normally prevents collisions; an existing
32
+ * directory at the final path is treated as a genuine collision and
33
+ * rejected rather than overwritten, so prior comparison evidence is never
34
+ * destructively replaced.
35
+ */
36
+ export async function writeComparisonArtifact(artifact, outputLocation, options = {}) {
37
+ const validation = isValidComparisonArtifact(artifact);
38
+ if (!validation.valid) {
39
+ return writeFailure(`refusing to persist an invalid ComparisonArtifact: ${validation.reason}`);
40
+ }
41
+ const cwd = options.cwd ?? process.cwd();
42
+ const outputRoot = path.resolve(cwd, outputLocation);
43
+ const finalRoot = path.join(outputRoot, artifact.comparisonId);
44
+ const tempRoot = path.join(outputRoot, `.tmp-${artifact.comparisonId}`);
45
+ if (await pathExists(finalRoot)) {
46
+ return writeFailure(`a comparison artifact already exists at "${artifact.comparisonId}"; refusing to overwrite it`, {
47
+ artifactRoot: finalRoot,
48
+ });
49
+ }
50
+ try {
51
+ await rm(tempRoot, { recursive: true, force: true });
52
+ await mkdir(tempRoot, { recursive: true });
53
+ await writeFile(path.join(tempRoot, COMPARISON_MANIFEST_FILENAME), JSON.stringify(artifact, null, 2), 'utf8');
54
+ await rename(tempRoot, finalRoot);
55
+ return {
56
+ ok: true,
57
+ artifactRoot: finalRoot,
58
+ manifestPath: path.join(finalRoot, COMPARISON_MANIFEST_FILENAME),
59
+ };
60
+ }
61
+ catch (err) {
62
+ await rm(tempRoot, { recursive: true, force: true }).catch(() => undefined);
63
+ const message = err instanceof Error ? err.message : String(err);
64
+ return writeFailure(`failed to persist comparison artifact: ${message}`);
65
+ }
66
+ }
67
+ //# sourceMappingURL=comparisonArtifactWriter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comparisonArtifactWriter.js","sourceRoot":"","sources":["../../src/artifacts/comparisonArtifactWriter.ts"],"names":[],"mappings":"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"}