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.
- package/CHANGELOG.md +112 -0
- package/README.md +59 -7
- package/dist/application/comparisonService.d.ts +56 -0
- package/dist/application/comparisonService.js +77 -0
- package/dist/application/comparisonService.js.map +1 -0
- package/dist/application/frontendContractEvaluationService.d.ts +49 -0
- package/dist/application/frontendContractEvaluationService.js +112 -0
- package/dist/application/frontendContractEvaluationService.js.map +1 -0
- package/dist/application/frontendContractPersistenceService.d.ts +56 -0
- package/dist/application/frontendContractPersistenceService.js +91 -0
- package/dist/application/frontendContractPersistenceService.js.map +1 -0
- package/dist/artifacts/artifactReader.d.ts +19 -0
- package/dist/artifacts/artifactReader.js +36 -0
- package/dist/artifacts/artifactReader.js.map +1 -0
- package/dist/artifacts/comparisonArtifactReader.d.ts +18 -0
- package/dist/artifacts/comparisonArtifactReader.js +35 -0
- package/dist/artifacts/comparisonArtifactReader.js.map +1 -0
- package/dist/artifacts/comparisonArtifactWriter.d.ts +41 -0
- package/dist/artifacts/comparisonArtifactWriter.js +67 -0
- package/dist/artifacts/comparisonArtifactWriter.js.map +1 -0
- package/dist/artifacts/frontendContractArtifactReader.d.ts +24 -0
- package/dist/artifacts/frontendContractArtifactReader.js +47 -0
- package/dist/artifacts/frontendContractArtifactReader.js.map +1 -0
- package/dist/artifacts/frontendContractArtifactWriter.d.ts +34 -0
- package/dist/artifacts/frontendContractArtifactWriter.js +70 -0
- package/dist/artifacts/frontendContractArtifactWriter.js.map +1 -0
- package/dist/artifacts/frontendContractEvaluationArtifactReader.d.ts +17 -0
- package/dist/artifacts/frontendContractEvaluationArtifactReader.js +34 -0
- package/dist/artifacts/frontendContractEvaluationArtifactReader.js.map +1 -0
- package/dist/artifacts/frontendContractEvaluationArtifactWriter.d.ts +32 -0
- package/dist/artifacts/frontendContractEvaluationArtifactWriter.js +58 -0
- package/dist/artifacts/frontendContractEvaluationArtifactWriter.js.map +1 -0
- package/dist/cli.js +712 -3
- package/dist/cli.js.map +1 -1
- package/dist/domain/comparison.d.ts +198 -0
- package/dist/domain/comparison.js +324 -0
- package/dist/domain/comparison.js.map +1 -0
- package/dist/domain/comparisonEngine.d.ts +48 -0
- package/dist/domain/comparisonEngine.js +694 -0
- package/dist/domain/comparisonEngine.js.map +1 -0
- package/dist/domain/comparisonIdentity.d.ts +13 -0
- package/dist/domain/comparisonIdentity.js +46 -0
- package/dist/domain/comparisonIdentity.js.map +1 -0
- package/dist/domain/evidence.d.ts +2 -0
- package/dist/domain/evidence.js +4 -0
- package/dist/domain/evidence.js.map +1 -1
- package/dist/domain/frontendContractEvaluation.d.ts +57 -0
- package/dist/domain/frontendContractEvaluation.js +454 -0
- package/dist/domain/frontendContractEvaluation.js.map +1 -0
- package/dist/domain/frontendContractEvaluationArtifact.d.ts +65 -0
- package/dist/domain/frontendContractEvaluationArtifact.js +108 -0
- package/dist/domain/frontendContractEvaluationArtifact.js.map +1 -0
- package/dist/domain/frontendContractIdentity.d.ts +39 -0
- package/dist/domain/frontendContractIdentity.js +70 -0
- package/dist/domain/frontendContractIdentity.js.map +1 -0
- package/dist/domain/frontendContracts.d.ts +188 -0
- package/dist/domain/frontendContracts.js +260 -0
- package/dist/domain/frontendContracts.js.map +1 -0
- package/dist/domain/relationships.d.ts +168 -0
- package/dist/domain/relationships.js +343 -0
- package/dist/domain/relationships.js.map +1 -0
- package/dist/index.d.ts +36 -1
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/docs/ARCHITECTURE.md +173 -3
- package/docs/CI_CD.md +76 -0
- package/docs/COMMANDS.md +299 -0
- package/docs/CONTRACTS.md +270 -6
- package/docs/CURRENT_STATE.md +183 -8
- package/docs/DEVELOPMENT.md +83 -12
- package/docs/PROJECT_OVERVIEW.md +18 -8
- package/docs/QUICKSTART.md +10 -0
- package/docs/RELEASE.md +17 -8
- package/docs/ROADMAP.md +10 -0
- package/docs/SECURITY.md +26 -1
- package/docs/WORKFLOWS.md +113 -6
- package/package.json +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { isValidPersistentBaselineContract, isValidPerChangeContract } from '../domain/frontendContracts.js';
|
|
3
|
+
import { DIAGNOSTIC_SEVERITY } from '../domain/diagnostics.js';
|
|
4
|
+
import { normalizeOutputLocation } from '../request/paths.js';
|
|
5
|
+
import { writePersistentBaselineContract, writePerChangeContract } from '../artifacts/frontendContractArtifactWriter.js';
|
|
6
|
+
import { readObservationArtifact } from '../artifacts/artifactReader.js';
|
|
7
|
+
import { MANIFEST_FILENAME as OBSERVATION_MANIFEST_FILENAME } from '../artifacts/artifactWriter.js';
|
|
8
|
+
function invalidRequest(message) {
|
|
9
|
+
return { ok: false, diagnostics: [{ code: 'invalid-request', severity: DIAGNOSTIC_SEVERITY['invalid-request'], message }] };
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The one canonical application-level baseline-approval use case: `approve-baseline`
|
|
13
|
+
* is the *only* place a baseline is ever approved (see governing scope) - this
|
|
14
|
+
* function is never called from `observe`/`compare`/`evaluate-contract`, and a
|
|
15
|
+
* `PASS` evaluation never triggers it. Validates the raw (still-`unknown`,
|
|
16
|
+
* freshly-parsed-JSON) baseline through the existing frozen
|
|
17
|
+
* `isValidPersistentBaselineContract` structural gate (never a second
|
|
18
|
+
* validator), reads the supplied observation through the existing
|
|
19
|
+
* `readObservationArtifact` reader, verifies the baseline's frozen
|
|
20
|
+
* `sourceObservation` reference actually corresponds to that observation's
|
|
21
|
+
* stable identity, and - only then - persists exactly once through the
|
|
22
|
+
* existing contract writer. Never mutates the observation. Never infers,
|
|
23
|
+
* creates, or removes `supersedesBaselineId` - whatever the caller already
|
|
24
|
+
* authored is preserved exactly.
|
|
25
|
+
*/
|
|
26
|
+
export async function approveAndPersistBaseline(rawContract, observationRoot, options) {
|
|
27
|
+
const validation = isValidPersistentBaselineContract(rawContract);
|
|
28
|
+
if (!validation.valid)
|
|
29
|
+
return invalidRequest(`baseline contract is invalid: ${validation.reason}`);
|
|
30
|
+
const contract = rawContract;
|
|
31
|
+
const observationRead = await readObservationArtifact(path.join(observationRoot, OBSERVATION_MANIFEST_FILENAME));
|
|
32
|
+
if (!observationRead.ok)
|
|
33
|
+
return invalidRequest(`observation artifact: ${observationRead.reason}`);
|
|
34
|
+
const observation = observationRead.artifact;
|
|
35
|
+
const ref = contract.sourceObservation;
|
|
36
|
+
const matchesObservation = ref.observationId === observation.observationId &&
|
|
37
|
+
ref.requestId === observation.requestId &&
|
|
38
|
+
ref.producer.name === observation.producer.name &&
|
|
39
|
+
ref.producer.version === observation.producer.version &&
|
|
40
|
+
ref.observationSchemaVersion === observation.schemaVersion;
|
|
41
|
+
if (!matchesObservation) {
|
|
42
|
+
return invalidRequest('baseline contract sourceObservation does not match the supplied observation artifact; refusing to approve an unrelated observation');
|
|
43
|
+
}
|
|
44
|
+
const normalized = normalizeOutputLocation(options.outputLocation);
|
|
45
|
+
if (!normalized.ok)
|
|
46
|
+
return { ok: false, diagnostics: [normalized.diagnostic] };
|
|
47
|
+
const persisted = await writePersistentBaselineContract(contract, normalized.value, options.cwd === undefined ? {} : { cwd: options.cwd });
|
|
48
|
+
if (!persisted.ok)
|
|
49
|
+
return { ok: false, diagnostics: persisted.diagnostics };
|
|
50
|
+
return {
|
|
51
|
+
ok: true,
|
|
52
|
+
baselineId: contract.baselineId,
|
|
53
|
+
...(contract.supersedesBaselineId === undefined ? {} : { supersedesBaselineId: contract.supersedesBaselineId }),
|
|
54
|
+
artifactRoot: persisted.artifactRoot,
|
|
55
|
+
manifestPath: persisted.manifestPath,
|
|
56
|
+
clauseCount: contract.clauses.length,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The one canonical application-level per-change-contract persistence use
|
|
61
|
+
* case (`save-change-contract`) - persistence only, never approval
|
|
62
|
+
* terminology or semantics. Validates the raw contract through the existing
|
|
63
|
+
* frozen `isValidPerChangeContract` gate (which already rejects a
|
|
64
|
+
* `PersistentBaselineContract` passed here, an authored `category:
|
|
65
|
+
* "unexpected"`, and every other structural rule - never duplicated here),
|
|
66
|
+
* then persists exactly once. `supersedesBaselineClauseIds` is preserved
|
|
67
|
+
* exactly as authored; this function performs no baseline-reference
|
|
68
|
+
* resolution (that remains the evaluator's responsibility).
|
|
69
|
+
*/
|
|
70
|
+
export async function persistPerChangeContract(rawContract, options) {
|
|
71
|
+
const validation = isValidPerChangeContract(rawContract);
|
|
72
|
+
if (!validation.valid)
|
|
73
|
+
return invalidRequest(`per-change contract is invalid: ${validation.reason}`);
|
|
74
|
+
const contract = rawContract;
|
|
75
|
+
const normalized = normalizeOutputLocation(options.outputLocation);
|
|
76
|
+
if (!normalized.ok)
|
|
77
|
+
return { ok: false, diagnostics: [normalized.diagnostic] };
|
|
78
|
+
const persisted = await writePerChangeContract(contract, normalized.value, options.cwd === undefined ? {} : { cwd: options.cwd });
|
|
79
|
+
if (!persisted.ok)
|
|
80
|
+
return { ok: false, diagnostics: persisted.diagnostics };
|
|
81
|
+
const supersedesBaselineClauseCount = contract.clauses.reduce((count, clause) => count + (clause.supersedesBaselineClauseIds?.length ?? 0), 0);
|
|
82
|
+
return {
|
|
83
|
+
ok: true,
|
|
84
|
+
contractId: contract.contractId,
|
|
85
|
+
artifactRoot: persisted.artifactRoot,
|
|
86
|
+
manifestPath: persisted.manifestPath,
|
|
87
|
+
clauseCount: contract.clauses.length,
|
|
88
|
+
supersedesBaselineClauseCount,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=frontendContractPersistenceService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontendContractPersistenceService.js","sourceRoot":"","sources":["../../src/application/frontendContractPersistenceService.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,iCAAiC,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAE7G,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,+BAA+B,EAAE,sBAAsB,EAAE,MAAM,gDAAgD,CAAC;AAEzH,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,iBAAiB,IAAI,6BAA6B,EAAE,MAAM,gCAAgC,CAAC;AA6BpG,SAAS,cAAc,CAAC,OAAe;IACrC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,QAAQ,EAAE,mBAAmB,CAAC,iBAAiB,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;AAC9H,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,WAAoB,EAAE,eAAuB,EAAE,OAAuC;IACpI,MAAM,UAAU,GAAG,iCAAiC,CAAC,WAAW,CAAC,CAAC;IAClE,IAAI,CAAC,UAAU,CAAC,KAAK;QAAE,OAAO,cAAc,CAAC,iCAAiC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;IACnG,MAAM,QAAQ,GAAG,WAAyC,CAAC;IAE3D,MAAM,eAAe,GAAG,MAAM,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,6BAA6B,CAAC,CAAC,CAAC;IACjH,IAAI,CAAC,eAAe,CAAC,EAAE;QAAE,OAAO,cAAc,CAAC,yBAAyB,eAAe,CAAC,MAAM,EAAE,CAAC,CAAC;IAClG,MAAM,WAAW,GAAG,eAAe,CAAC,QAAQ,CAAC;IAE7C,MAAM,GAAG,GAAG,QAAQ,CAAC,iBAAiB,CAAC;IACvC,MAAM,kBAAkB,GACtB,GAAG,CAAC,aAAa,KAAK,WAAW,CAAC,aAAa;QAC/C,GAAG,CAAC,SAAS,KAAK,WAAW,CAAC,SAAS;QACvC,GAAG,CAAC,QAAQ,CAAC,IAAI,KAAK,WAAW,CAAC,QAAQ,CAAC,IAAI;QAC/C,GAAG,CAAC,QAAQ,CAAC,OAAO,KAAK,WAAW,CAAC,QAAQ,CAAC,OAAO;QACrD,GAAG,CAAC,wBAAwB,KAAK,WAAW,CAAC,aAAa,CAAC;IAC7D,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACxB,OAAO,cAAc,CAAC,oIAAoI,CAAC,CAAC;IAC9J,CAAC;IAED,MAAM,UAAU,GAAG,uBAAuB,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IACnE,IAAI,CAAC,UAAU,CAAC,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;IAE/E,MAAM,SAAS,GAAG,MAAM,+BAA+B,CAAC,QAAQ,EAAE,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC3I,IAAI,CAAC,SAAS,CAAC,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,CAAC,WAAW,EAAE,CAAC;IAE5E,OAAO;QACL,EAAE,EAAE,IAAI;QACR,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,GAAG,CAAC,QAAQ,CAAC,oBAAoB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,oBAAoB,EAAE,QAAQ,CAAC,oBAAoB,EAAE,CAAC;QAC/G,YAAY,EAAE,SAAS,CAAC,YAAY;QACpC,YAAY,EAAE,SAAS,CAAC,YAAY;QACpC,WAAW,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM;KACrC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,WAAoB,EAAE,OAAuC;IAC1G,MAAM,UAAU,GAAG,wBAAwB,CAAC,WAAW,CAAC,CAAC;IACzD,IAAI,CAAC,UAAU,CAAC,KAAK;QAAE,OAAO,cAAc,CAAC,mCAAmC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;IACrG,MAAM,QAAQ,GAAG,WAAgC,CAAC;IAElD,MAAM,UAAU,GAAG,uBAAuB,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IACnE,IAAI,CAAC,UAAU,CAAC,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;IAE/E,MAAM,SAAS,GAAG,MAAM,sBAAsB,CAAC,QAAQ,EAAE,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAClI,IAAI,CAAC,SAAS,CAAC,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,CAAC,WAAW,EAAE,CAAC;IAE5E,MAAM,6BAA6B,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC,2BAA2B,EAAE,MAAM,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAE/I,OAAO;QACL,EAAE,EAAE,IAAI;QACR,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,YAAY,EAAE,SAAS,CAAC,YAAY;QACpC,YAAY,EAAE,SAAS,CAAC,YAAY;QACpC,WAAW,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM;QACpC,6BAA6B;KAC9B,CAAC;AACJ,CAAC"}
|
|
@@ -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,18 @@
|
|
|
1
|
+
import type { ComparisonArtifact } from '../domain/comparison.js';
|
|
2
|
+
export type ReadComparisonArtifactResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
artifact: ComparisonArtifact;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
reason: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* v0.5 Batch 3 addition: no canonical comparison-artifact reader existed
|
|
11
|
+
* before this batch (only `readObservationArtifact` did). Narrow counterpart
|
|
12
|
+
* to `artifacts/comparisonArtifactWriter.ts#writeComparisonArtifact`: reads
|
|
13
|
+
* one `manifest.json`, parses it, and validates it through the same
|
|
14
|
+
* `isValidComparisonArtifact` structural gate the writer uses - never a
|
|
15
|
+
* second validator, never a schema upgrade. Comparison schema remains
|
|
16
|
+
* `1.0.0`; this reader changes no comparison semantics.
|
|
17
|
+
*/
|
|
18
|
+
export declare function readComparisonArtifact(manifestPath: string): Promise<ReadComparisonArtifactResult>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { isValidComparisonArtifact } from '../domain/comparison.js';
|
|
3
|
+
/**
|
|
4
|
+
* v0.5 Batch 3 addition: no canonical comparison-artifact reader existed
|
|
5
|
+
* before this batch (only `readObservationArtifact` did). Narrow counterpart
|
|
6
|
+
* to `artifacts/comparisonArtifactWriter.ts#writeComparisonArtifact`: reads
|
|
7
|
+
* one `manifest.json`, parses it, and validates it through the same
|
|
8
|
+
* `isValidComparisonArtifact` structural gate the writer uses - never a
|
|
9
|
+
* second validator, never a schema upgrade. Comparison schema remains
|
|
10
|
+
* `1.0.0`; this reader changes no comparison semantics.
|
|
11
|
+
*/
|
|
12
|
+
export async function readComparisonArtifact(manifestPath) {
|
|
13
|
+
let raw;
|
|
14
|
+
try {
|
|
15
|
+
raw = await readFile(manifestPath, 'utf8');
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
19
|
+
return { ok: false, reason: `failed to read comparison artifact manifest at "${manifestPath}": ${message}` };
|
|
20
|
+
}
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(raw);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27
|
+
return { ok: false, reason: `comparison artifact manifest at "${manifestPath}" is not valid JSON: ${message}` };
|
|
28
|
+
}
|
|
29
|
+
const validation = isValidComparisonArtifact(parsed);
|
|
30
|
+
if (!validation.valid) {
|
|
31
|
+
return { ok: false, reason: `comparison artifact manifest at "${manifestPath}" failed structural validation: ${validation.reason}` };
|
|
32
|
+
}
|
|
33
|
+
return { ok: true, artifact: parsed };
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=comparisonArtifactReader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comparisonArtifactReader.js","sourceRoot":"","sources":["../../src/artifacts/comparisonArtifactReader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAIpE;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,YAAoB;IAC/D,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,mDAAmD,YAAY,MAAM,OAAO,EAAE,EAAE,CAAC;IAC/G,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,oCAAoC,YAAY,wBAAwB,OAAO,EAAE,EAAE,CAAC;IAClH,CAAC;IAED,MAAM,UAAU,GAAG,yBAAyB,CAAC,MAAM,CAAC,CAAC;IACrD,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACtB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oCAAoC,YAAY,mCAAmC,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC;IACvI,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAA4B,EAAE,CAAC;AAC9D,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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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PersistentBaselineContract, PerChangeContract } from '../domain/frontendContracts.js';
|
|
2
|
+
export type ReadPersistentBaselineContractResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
contract: PersistentBaselineContract;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
reason: string;
|
|
8
|
+
};
|
|
9
|
+
export type ReadPerChangeContractResult = {
|
|
10
|
+
ok: true;
|
|
11
|
+
contract: PerChangeContract;
|
|
12
|
+
} | {
|
|
13
|
+
ok: false;
|
|
14
|
+
reason: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Narrow canonical reader counterpart to `frontendContractArtifactWriter.ts#writePersistentBaselineContract`:
|
|
18
|
+
* reads one `manifest.json`, parses it, and validates it through the same
|
|
19
|
+
* `isValidPersistentBaselineContract` structural gate the writer uses - never
|
|
20
|
+
* a second validator, never a schema upgrade, never a guessed missing field.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readPersistentBaselineContract(manifestPath: string): Promise<ReadPersistentBaselineContractResult>;
|
|
23
|
+
/** Narrow canonical reader counterpart to `frontendContractArtifactWriter.ts#writePerChangeContract`. */
|
|
24
|
+
export declare function readPerChangeContract(manifestPath: string): Promise<ReadPerChangeContractResult>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { isValidPersistentBaselineContract, isValidPerChangeContract } from '../domain/frontendContracts.js';
|
|
3
|
+
async function readJson(manifestPath) {
|
|
4
|
+
let raw;
|
|
5
|
+
try {
|
|
6
|
+
raw = await readFile(manifestPath, 'utf8');
|
|
7
|
+
}
|
|
8
|
+
catch (err) {
|
|
9
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
10
|
+
return { ok: false, reason: `failed to read artifact manifest at "${manifestPath}": ${message}` };
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return { ok: true, value: JSON.parse(raw) };
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
17
|
+
return { ok: false, reason: `artifact manifest at "${manifestPath}" is not valid JSON: ${message}` };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Narrow canonical reader counterpart to `frontendContractArtifactWriter.ts#writePersistentBaselineContract`:
|
|
22
|
+
* reads one `manifest.json`, parses it, and validates it through the same
|
|
23
|
+
* `isValidPersistentBaselineContract` structural gate the writer uses - never
|
|
24
|
+
* a second validator, never a schema upgrade, never a guessed missing field.
|
|
25
|
+
*/
|
|
26
|
+
export async function readPersistentBaselineContract(manifestPath) {
|
|
27
|
+
const parsed = await readJson(manifestPath);
|
|
28
|
+
if (!parsed.ok)
|
|
29
|
+
return parsed;
|
|
30
|
+
const validation = isValidPersistentBaselineContract(parsed.value);
|
|
31
|
+
if (!validation.valid) {
|
|
32
|
+
return { ok: false, reason: `baseline contract manifest at "${manifestPath}" failed structural validation: ${validation.reason}` };
|
|
33
|
+
}
|
|
34
|
+
return { ok: true, contract: parsed.value };
|
|
35
|
+
}
|
|
36
|
+
/** Narrow canonical reader counterpart to `frontendContractArtifactWriter.ts#writePerChangeContract`. */
|
|
37
|
+
export async function readPerChangeContract(manifestPath) {
|
|
38
|
+
const parsed = await readJson(manifestPath);
|
|
39
|
+
if (!parsed.ok)
|
|
40
|
+
return parsed;
|
|
41
|
+
const validation = isValidPerChangeContract(parsed.value);
|
|
42
|
+
if (!validation.valid) {
|
|
43
|
+
return { ok: false, reason: `per-change contract manifest at "${manifestPath}" failed structural validation: ${validation.reason}` };
|
|
44
|
+
}
|
|
45
|
+
return { ok: true, contract: parsed.value };
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=frontendContractArtifactReader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontendContractArtifactReader.js","sourceRoot":"","sources":["../../src/artifacts/frontendContractArtifactReader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,iCAAiC,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAK7G,KAAK,UAAU,QAAQ,CAAC,YAAoB;IAC1C,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,wCAAwC,YAAY,MAAM,OAAO,EAAE,EAAE,CAAC;IACpG,CAAC;IACD,IAAI,CAAC;QACH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;IAC9C,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,yBAAyB,YAAY,wBAAwB,OAAO,EAAE,EAAE,CAAC;IACvG,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAAC,YAAoB;IACvE,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,CAAC;IAC5C,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,MAAM,CAAC;IAC9B,MAAM,UAAU,GAAG,iCAAiC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACnE,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACtB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kCAAkC,YAAY,mCAAmC,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC;IACrI,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,KAAmC,EAAE,CAAC;AAC5E,CAAC;AAED,yGAAyG;AACzG,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,YAAoB;IAC9D,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,CAAC;IAC5C,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,MAAM,CAAC;IAC9B,MAAM,UAAU,GAAG,wBAAwB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC1D,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACtB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oCAAoC,YAAY,mCAAmC,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC;IACvI,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,KAA0B,EAAE,CAAC;AACnE,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { PersistentBaselineContract, PerChangeContract } from '../domain/frontendContracts.js';
|
|
2
|
+
import type { Diagnostic } from '../domain/diagnostics.js';
|
|
3
|
+
export declare const FRONTEND_CONTRACT_MANIFEST_FILENAME = "manifest.json";
|
|
4
|
+
/**
|
|
5
|
+
* Mirrors `PersistedComparisonResult`'s shape/semantics - a frontend contract
|
|
6
|
+
* directory has no screenshot file either, and this writer never touches
|
|
7
|
+
* `artifacts/artifactWriter.ts` or `artifacts/comparisonArtifactWriter.ts`.
|
|
8
|
+
*/
|
|
9
|
+
export type PersistedFrontendContractResult = {
|
|
10
|
+
ok: true;
|
|
11
|
+
artifactRoot: string;
|
|
12
|
+
manifestPath: string;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
diagnostics: Diagnostic[];
|
|
16
|
+
};
|
|
17
|
+
export interface WriteFrontendContractArtifactOptions {
|
|
18
|
+
/** Base directory that `outputLocation` is resolved against. Defaults to `process.cwd()`. */
|
|
19
|
+
cwd?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Persists one already-valid `PersistentBaselineContract` as
|
|
23
|
+
* `<outputLocation>/<baselineId>/manifest.json`. Never mutates or removes any
|
|
24
|
+
* other baseline artifact - `supersedesBaselineId` (if present) is written
|
|
25
|
+
* exactly as supplied; this writer never inspects, rewrites, or deletes the
|
|
26
|
+
* baseline it refers to.
|
|
27
|
+
*/
|
|
28
|
+
export declare function writePersistentBaselineContract(contract: PersistentBaselineContract, outputLocation: string, options?: WriteFrontendContractArtifactOptions): Promise<PersistedFrontendContractResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Persists one already-valid `PerChangeContract` as
|
|
31
|
+
* `<outputLocation>/<contractId>/manifest.json`, symmetric with baseline
|
|
32
|
+
* persistence above (both share `CONTRACT_ARTIFACT_KIND`/`CONTRACT_SCHEMA_VERSION`).
|
|
33
|
+
*/
|
|
34
|
+
export declare function writePerChangeContract(contract: PerChangeContract, outputLocation: string, options?: WriteFrontendContractArtifactOptions): Promise<PersistedFrontendContractResult>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { mkdir, writeFile, rename, rm, access } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isValidPersistentBaselineContract, isValidPerChangeContract } from '../domain/frontendContracts.js';
|
|
4
|
+
import { DIAGNOSTIC_SEVERITY } from '../domain/diagnostics.js';
|
|
5
|
+
export const FRONTEND_CONTRACT_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
|
+
* Shared atomic-write body for both contract classes, following the same
|
|
20
|
+
* discipline as `writeObservationArtifact`/`writeComparisonArtifact`: sibling
|
|
21
|
+
* temporary directory, single `manifest.json`, then one atomic rename. An
|
|
22
|
+
* existing directory at the final identity is a genuine collision (never a
|
|
23
|
+
* destructive overwrite of prior baseline/contract history) and is rejected.
|
|
24
|
+
*/
|
|
25
|
+
async function writeContractArtifact(value, id, kindLabel, outputLocation, options) {
|
|
26
|
+
const cwd = options.cwd ?? process.cwd();
|
|
27
|
+
const outputRoot = path.resolve(cwd, outputLocation);
|
|
28
|
+
const finalRoot = path.join(outputRoot, id);
|
|
29
|
+
const tempRoot = path.join(outputRoot, `.tmp-${id}`);
|
|
30
|
+
if (await pathExists(finalRoot)) {
|
|
31
|
+
return writeFailure(`a ${kindLabel} artifact already exists at "${id}"; refusing to overwrite it`, { artifactRoot: finalRoot });
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
35
|
+
await mkdir(tempRoot, { recursive: true });
|
|
36
|
+
await writeFile(path.join(tempRoot, FRONTEND_CONTRACT_MANIFEST_FILENAME), JSON.stringify(value, null, 2), 'utf8');
|
|
37
|
+
await rename(tempRoot, finalRoot);
|
|
38
|
+
return { ok: true, artifactRoot: finalRoot, manifestPath: path.join(finalRoot, FRONTEND_CONTRACT_MANIFEST_FILENAME) };
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
await rm(tempRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
42
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
43
|
+
return writeFailure(`failed to persist ${kindLabel} artifact: ${message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Persists one already-valid `PersistentBaselineContract` as
|
|
48
|
+
* `<outputLocation>/<baselineId>/manifest.json`. Never mutates or removes any
|
|
49
|
+
* other baseline artifact - `supersedesBaselineId` (if present) is written
|
|
50
|
+
* exactly as supplied; this writer never inspects, rewrites, or deletes the
|
|
51
|
+
* baseline it refers to.
|
|
52
|
+
*/
|
|
53
|
+
export async function writePersistentBaselineContract(contract, outputLocation, options = {}) {
|
|
54
|
+
const validation = isValidPersistentBaselineContract(contract);
|
|
55
|
+
if (!validation.valid)
|
|
56
|
+
return writeFailure(`refusing to persist an invalid PersistentBaselineContract: ${validation.reason}`);
|
|
57
|
+
return writeContractArtifact(contract, contract.baselineId, 'baseline contract', outputLocation, options);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Persists one already-valid `PerChangeContract` as
|
|
61
|
+
* `<outputLocation>/<contractId>/manifest.json`, symmetric with baseline
|
|
62
|
+
* persistence above (both share `CONTRACT_ARTIFACT_KIND`/`CONTRACT_SCHEMA_VERSION`).
|
|
63
|
+
*/
|
|
64
|
+
export async function writePerChangeContract(contract, outputLocation, options = {}) {
|
|
65
|
+
const validation = isValidPerChangeContract(contract);
|
|
66
|
+
if (!validation.valid)
|
|
67
|
+
return writeFailure(`refusing to persist an invalid PerChangeContract: ${validation.reason}`);
|
|
68
|
+
return writeContractArtifact(contract, contract.contractId, 'per-change contract', outputLocation, options);
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=frontendContractArtifactWriter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontendContractArtifactWriter.js","sourceRoot":"","sources":["../../src/artifacts/frontendContractArtifactWriter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FrontendContractEvaluationArtifact } from '../domain/frontendContractEvaluationArtifact.js';
|
|
2
|
+
export type ReadFrontendContractEvaluationArtifactResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
artifact: FrontendContractEvaluationArtifact;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
reason: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Narrow canonical reader counterpart to
|
|
11
|
+
* `frontendContractEvaluationArtifactWriter.ts#writeFrontendContractEvaluationArtifact`:
|
|
12
|
+
* reads one `manifest.json`, parses it, and validates it through the same
|
|
13
|
+
* `isValidFrontendContractEvaluationArtifact` structural gate the writer
|
|
14
|
+
* uses. Read-only: never mutates the file, never recomputes or reinterprets
|
|
15
|
+
* the evaluation, never repairs a malformed artifact.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readFrontendContractEvaluationArtifact(manifestPath: string): Promise<ReadFrontendContractEvaluationArtifactResult>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { isValidFrontendContractEvaluationArtifact } from '../domain/frontendContractEvaluationArtifact.js';
|
|
3
|
+
/**
|
|
4
|
+
* Narrow canonical reader counterpart to
|
|
5
|
+
* `frontendContractEvaluationArtifactWriter.ts#writeFrontendContractEvaluationArtifact`:
|
|
6
|
+
* reads one `manifest.json`, parses it, and validates it through the same
|
|
7
|
+
* `isValidFrontendContractEvaluationArtifact` structural gate the writer
|
|
8
|
+
* uses. Read-only: never mutates the file, never recomputes or reinterprets
|
|
9
|
+
* the evaluation, never repairs a malformed artifact.
|
|
10
|
+
*/
|
|
11
|
+
export async function readFrontendContractEvaluationArtifact(manifestPath) {
|
|
12
|
+
let raw;
|
|
13
|
+
try {
|
|
14
|
+
raw = await readFile(manifestPath, 'utf8');
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
18
|
+
return { ok: false, reason: `failed to read evaluation artifact manifest at "${manifestPath}": ${message}` };
|
|
19
|
+
}
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = JSON.parse(raw);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
26
|
+
return { ok: false, reason: `evaluation artifact manifest at "${manifestPath}" is not valid JSON: ${message}` };
|
|
27
|
+
}
|
|
28
|
+
const validation = isValidFrontendContractEvaluationArtifact(parsed);
|
|
29
|
+
if (!validation.valid) {
|
|
30
|
+
return { ok: false, reason: `evaluation artifact manifest at "${manifestPath}" failed structural validation: ${validation.reason}` };
|
|
31
|
+
}
|
|
32
|
+
return { ok: true, artifact: parsed };
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=frontendContractEvaluationArtifactReader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontendContractEvaluationArtifactReader.js","sourceRoot":"","sources":["../../src/artifacts/frontendContractEvaluationArtifactReader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,yCAAyC,EAAE,MAAM,iDAAiD,CAAC;AAI5G;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,sCAAsC,CAAC,YAAoB;IAC/E,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,mDAAmD,YAAY,MAAM,OAAO,EAAE,EAAE,CAAC;IAC/G,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,oCAAoC,YAAY,wBAAwB,OAAO,EAAE,EAAE,CAAC;IAClH,CAAC;IAED,MAAM,UAAU,GAAG,yCAAyC,CAAC,MAAM,CAAC,CAAC;IACrE,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QACtB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oCAAoC,YAAY,mCAAmC,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC;IACvI,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAA4C,EAAE,CAAC;AAC9E,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { FrontendContractEvaluationArtifact } from '../domain/frontendContractEvaluationArtifact.js';
|
|
2
|
+
import type { Diagnostic } from '../domain/diagnostics.js';
|
|
3
|
+
export declare const EVALUATION_MANIFEST_FILENAME = "manifest.json";
|
|
4
|
+
export type PersistedFrontendContractEvaluationResult = {
|
|
5
|
+
ok: true;
|
|
6
|
+
artifactRoot: string;
|
|
7
|
+
manifestPath: string;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
diagnostics: Diagnostic[];
|
|
11
|
+
};
|
|
12
|
+
export interface WriteFrontendContractEvaluationArtifactOptions {
|
|
13
|
+
/** Base directory that `outputLocation` is resolved against. Defaults to `process.cwd()`. */
|
|
14
|
+
cwd?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Persists one already-built, already-valid `FrontendContractEvaluationArtifact`
|
|
18
|
+
* as `<outputLocation>/<evaluationId>/manifest.json` only - no screenshot is
|
|
19
|
+
* copied (the artifact's `before`/`after` fields already point back to the
|
|
20
|
+
* source observations). Same atomic-write discipline as
|
|
21
|
+
* `artifactWriter.ts#writeObservationArtifact`/`comparisonArtifactWriter.ts#writeComparisonArtifact`
|
|
22
|
+
* without sharing their implementation: sibling temporary directory, then one
|
|
23
|
+
* atomic rename. `evaluationId` is fresh per execution (see
|
|
24
|
+
* `frontendContractIdentity.ts#buildFrontendContractInstanceIdentity`), so an
|
|
25
|
+
* existing directory at the final path is a genuine collision and is
|
|
26
|
+
* rejected rather than overwritten - a legitimate PASS or FAIL evaluation
|
|
27
|
+
* result is never silently replaced. A `FAIL` verdict is a legitimate,
|
|
28
|
+
* persistable evaluation result, not a write failure; only an unpersistable
|
|
29
|
+
* artifact (invalid shape, or a real filesystem error) is reported as
|
|
30
|
+
* `ok: false`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function writeFrontendContractEvaluationArtifact(artifact: FrontendContractEvaluationArtifact, outputLocation: string, options?: WriteFrontendContractEvaluationArtifactOptions): Promise<PersistedFrontendContractEvaluationResult>;
|