@junejuly-lockstep/reconciler 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +182 -0
- package/dist/boot.d.ts +51 -0
- package/dist/boot.d.ts.map +1 -0
- package/dist/boot.js +162 -0
- package/dist/boot.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +180 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +55 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +140 -0
- package/dist/config.js.map +1 -0
- package/dist/contract.d.ts +61 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +154 -0
- package/dist/contract.js.map +1 -0
- package/dist/determinism.d.ts +23 -0
- package/dist/determinism.d.ts.map +1 -0
- package/dist/determinism.js +102 -0
- package/dist/determinism.js.map +1 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +35 -0
- package/dist/errors.js.map +1 -0
- package/dist/exitCodes.d.ts +33 -0
- package/dist/exitCodes.d.ts.map +1 -0
- package/dist/exitCodes.js +49 -0
- package/dist/exitCodes.js.map +1 -0
- package/dist/extract.d.ts +30 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +376 -0
- package/dist/extract.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +17 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +55 -0
- package/dist/init.js.map +1 -0
- package/dist/journeys.d.ts +34 -0
- package/dist/journeys.d.ts.map +1 -0
- package/dist/journeys.js +276 -0
- package/dist/journeys.js.map +1 -0
- package/dist/paths.d.ts +26 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +80 -0
- package/dist/paths.js.map +1 -0
- package/dist/reconcile.d.ts +63 -0
- package/dist/reconcile.d.ts.map +1 -0
- package/dist/reconcile.js +316 -0
- package/dist/reconcile.js.map +1 -0
- package/dist/report.d.ts +33 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +97 -0
- package/dist/report.js.map +1 -0
- package/dist/sync.d.ts +46 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +110 -0
- package/dist/sync.js.map +1 -0
- package/dist/upload.d.ts +33 -0
- package/dist/upload.d.ts.map +1 -0
- package/dist/upload.js +86 -0
- package/dist/upload.js.map +1 -0
- package/dist/util.d.ts +24 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +97 -0
- package/dist/util.js.map +1 -0
- package/dist/visual.d.ts +49 -0
- package/dist/visual.d.ts.map +1 -0
- package/dist/visual.js +145 -0
- package/dist/visual.js.map +1 -0
- package/dist/walk.d.ts +147 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +433 -0
- package/dist/walk.js.map +1 -0
- package/package.json +78 -0
- package/templates/example.journey.ts +90 -0
- package/templates/lockstep-reconcile.yml +219 -0
- package/templates/lockstep.config.example.ts +46 -0
- package/templates/msw-handlers.example.ts +113 -0
- package/templates/scenarios.example.ts +32 -0
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { LockstepConfig } from '@junejuly-lockstep/model';
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
export declare const WORKFLOW_PATH = ".github/workflows/lockstep-reconcile.yml";
|
|
4
|
+
export declare const API_KEY_ENV = "LOCKSTEP_API_KEY";
|
|
5
|
+
/** Helper for `lockstep.config.ts`: `export default defineConfig({...})` with types. */
|
|
6
|
+
export declare function defineConfig(config: z.input<typeof LockstepConfig>): z.input<typeof LockstepConfig>;
|
|
7
|
+
export interface LoadedConfig {
|
|
8
|
+
config: LockstepConfig;
|
|
9
|
+
/** Absolute path of the config file. */
|
|
10
|
+
file: string;
|
|
11
|
+
/** Directory the config lives in; every relative path in it resolves from here. */
|
|
12
|
+
rootDir: string;
|
|
13
|
+
}
|
|
14
|
+
/** Load `lockstep.config.ts` with jiti and validate it against `LockstepConfig`. */
|
|
15
|
+
export declare function loadConfig(configPath?: string, cwd?: string): Promise<LoadedConfig>;
|
|
16
|
+
export interface ContractCheckOptions {
|
|
17
|
+
/** Defaults to process.env. */
|
|
18
|
+
env?: NodeJS.ProcessEnv;
|
|
19
|
+
/** Skip the API key check (used by `lockstep validate --offline`). */
|
|
20
|
+
requireApiKey?: boolean;
|
|
21
|
+
/** Skip the workflow check (local dry runs). */
|
|
22
|
+
requireWorkflow?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface ContractReport {
|
|
25
|
+
violations: Array<{
|
|
26
|
+
item: string;
|
|
27
|
+
message: string;
|
|
28
|
+
}>;
|
|
29
|
+
warnings: string[];
|
|
30
|
+
journeyFiles: string[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Verify everything the contract requires beyond the config schema:
|
|
34
|
+
* journey files exist for the glob, the scenarios file exists, the workflow is committed,
|
|
35
|
+
* the API key is present, and the app under test points at a mock API origin.
|
|
36
|
+
*/
|
|
37
|
+
export declare function checkContract(loaded: LoadedConfig, opts?: ContractCheckOptions): Promise<ContractReport>;
|
|
38
|
+
/**
|
|
39
|
+
* Security rule: the reconciler never walks a real backend. When the app's API base is
|
|
40
|
+
* configured it must match `mockApiPattern`; anything else is refused before boot.
|
|
41
|
+
*/
|
|
42
|
+
export declare function checkMockApiBase(config: LockstepConfig): {
|
|
43
|
+
violation?: {
|
|
44
|
+
item: string;
|
|
45
|
+
message: string;
|
|
46
|
+
};
|
|
47
|
+
warning?: string;
|
|
48
|
+
};
|
|
49
|
+
/** Throw the first violation (with the rest attached) when the contract is not met. */
|
|
50
|
+
export declare function assertContract(report: ContractReport): void;
|
|
51
|
+
/** Convenience: load + check + assert in one call. */
|
|
52
|
+
export declare function loadAndCheck(configPath?: string, cwd?: string, opts?: ContractCheckOptions): Promise<LoadedConfig & {
|
|
53
|
+
report: ContractReport;
|
|
54
|
+
}>;
|
|
55
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAI7B,eAAO,MAAM,aAAa,6CAA6C,CAAC;AACxE,eAAO,MAAM,WAAW,qBAAqB,CAAC;AAE9C,wFAAwF;AACxF,wBAAgB,YAAY,CAC1B,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,GACrC,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAEhC;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,cAAc,CAAC;IACvB,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,oFAAoF;AACpF,wBAAsB,UAAU,CAC9B,UAAU,SAAuB,EACjC,GAAG,SAAgB,GAClB,OAAO,CAAC,YAAY,CAAC,CA4BvB;AAWD,MAAM,WAAW,oBAAoB;IACnC,+BAA+B;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,sEAAsE;IACtE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gDAAgD;IAChD,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,YAAY,EACpB,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,cAAc,CAAC,CAiDzB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,GAAG;IACxD,SAAS,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CA2BA;AAED,uFAAuF;AACvF,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAE3D;AAED,sDAAsD;AACtD,wBAAsB,YAAY,CAChC,UAAU,CAAC,EAAE,MAAM,EACnB,GAAG,CAAC,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,oBAAoB,GAC1B,OAAO,CAAC,YAAY,GAAG;IAAE,MAAM,EAAE,cAAc,CAAA;CAAE,CAAC,CAKpD"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import { LockstepConfig } from '@junejuly-lockstep/model';
|
|
5
|
+
import { ContractViolation } from './errors.js';
|
|
6
|
+
import { importTs, toPosix } from './util.js';
|
|
7
|
+
export const WORKFLOW_PATH = '.github/workflows/lockstep-reconcile.yml';
|
|
8
|
+
export const API_KEY_ENV = 'LOCKSTEP_API_KEY';
|
|
9
|
+
/** Helper for `lockstep.config.ts`: `export default defineConfig({...})` with types. */
|
|
10
|
+
export function defineConfig(config) {
|
|
11
|
+
return config;
|
|
12
|
+
}
|
|
13
|
+
/** Load `lockstep.config.ts` with jiti and validate it against `LockstepConfig`. */
|
|
14
|
+
export async function loadConfig(configPath = 'lockstep.config.ts', cwd = process.cwd()) {
|
|
15
|
+
const file = path.resolve(cwd, configPath);
|
|
16
|
+
if (!existsSync(file)) {
|
|
17
|
+
throw new ContractViolation('lockstep.config.ts', `no config file at ${toPosix(path.relative(cwd, file)) || configPath}; run \`lockstep init\` to create one`);
|
|
18
|
+
}
|
|
19
|
+
let mod;
|
|
20
|
+
try {
|
|
21
|
+
mod = await importTs(file);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
throw new ContractViolation('lockstep.config.ts', `failed to load: ${err instanceof Error ? err.message : String(err)}`);
|
|
25
|
+
}
|
|
26
|
+
const raw = unwrapDefault(mod);
|
|
27
|
+
const parsed = LockstepConfig.safeParse(raw);
|
|
28
|
+
if (!parsed.success) {
|
|
29
|
+
throw ContractViolation.fromList(parsed.error.issues.map((issue) => ({
|
|
30
|
+
item: `lockstep.config.ts:${issue.path.join('.') || '<root>'}`,
|
|
31
|
+
message: issue.message,
|
|
32
|
+
})));
|
|
33
|
+
}
|
|
34
|
+
return { config: parsed.data, file, rootDir: path.dirname(file) };
|
|
35
|
+
}
|
|
36
|
+
function unwrapDefault(mod) {
|
|
37
|
+
if (mod && typeof mod === 'object') {
|
|
38
|
+
const m = mod;
|
|
39
|
+
if ('default' in m && m['default'] && typeof m['default'] === 'object')
|
|
40
|
+
return m['default'];
|
|
41
|
+
if ('config' in m && m['config'] && typeof m['config'] === 'object')
|
|
42
|
+
return m['config'];
|
|
43
|
+
}
|
|
44
|
+
return mod;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Verify everything the contract requires beyond the config schema:
|
|
48
|
+
* journey files exist for the glob, the scenarios file exists, the workflow is committed,
|
|
49
|
+
* the API key is present, and the app under test points at a mock API origin.
|
|
50
|
+
*/
|
|
51
|
+
export async function checkContract(loaded, opts = {}) {
|
|
52
|
+
const env = opts.env ?? process.env;
|
|
53
|
+
const { config, rootDir } = loaded;
|
|
54
|
+
const violations = [];
|
|
55
|
+
const warnings = [];
|
|
56
|
+
const journeyFiles = (await fg(config.journeysGlob, { cwd: rootDir, onlyFiles: true, dot: false }))
|
|
57
|
+
.map(toPosix)
|
|
58
|
+
.sort();
|
|
59
|
+
if (journeyFiles.length === 0) {
|
|
60
|
+
violations.push({
|
|
61
|
+
item: 'journeys',
|
|
62
|
+
message: `no journey files match "${config.journeysGlob}"; each journey is a *.journey.ts exporting machine and meta`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (!existsSync(path.resolve(rootDir, config.scenariosPath))) {
|
|
66
|
+
violations.push({
|
|
67
|
+
item: 'scenarios',
|
|
68
|
+
message: `scenarios file "${config.scenariosPath}" does not exist; it must export named personas (seed plus overrides)`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (opts.requireWorkflow !== false && !existsSync(path.resolve(rootDir, WORKFLOW_PATH))) {
|
|
72
|
+
violations.push({
|
|
73
|
+
item: 'workflow',
|
|
74
|
+
message: `${WORKFLOW_PATH} is missing; run \`lockstep init\` and commit the shipped workflow`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (opts.requireApiKey !== false && !env[API_KEY_ENV]) {
|
|
78
|
+
violations.push({
|
|
79
|
+
item: 'apiKey',
|
|
80
|
+
message: `${API_KEY_ENV} is not set; add the project API key to repo secrets and pass it to the workflow`,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
else if (opts.requireApiKey !== false && !env[API_KEY_ENV].startsWith('lsk_')) {
|
|
84
|
+
violations.push({
|
|
85
|
+
item: 'apiKey',
|
|
86
|
+
message: `${API_KEY_ENV} does not look like a project key (expected lsk_ prefix)`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const mock = checkMockApiBase(config);
|
|
90
|
+
if (mock.violation)
|
|
91
|
+
violations.push(mock.violation);
|
|
92
|
+
if (mock.warning)
|
|
93
|
+
warnings.push(mock.warning);
|
|
94
|
+
return { violations, warnings, journeyFiles };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Security rule: the reconciler never walks a real backend. When the app's API base is
|
|
98
|
+
* configured it must match `mockApiPattern`; anything else is refused before boot.
|
|
99
|
+
*/
|
|
100
|
+
export function checkMockApiBase(config) {
|
|
101
|
+
let pattern;
|
|
102
|
+
try {
|
|
103
|
+
pattern = new RegExp(config.mockApiPattern, 'i');
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
return {
|
|
107
|
+
violation: {
|
|
108
|
+
item: 'mockApiPattern',
|
|
109
|
+
message: `mockApiPattern is not a valid regular expression: ${err instanceof Error ? err.message : String(err)}`,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (config.appApiBase === undefined) {
|
|
114
|
+
return {
|
|
115
|
+
warning: 'appApiBase is not set; the reconciler cannot verify the app under test talks to a mock origin. Set it so the walk is refused against a real backend.',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (!pattern.test(config.appApiBase)) {
|
|
119
|
+
return {
|
|
120
|
+
violation: {
|
|
121
|
+
item: 'appApiBase',
|
|
122
|
+
message: `refusing to walk: appApiBase "${config.appApiBase}" does not match mockApiPattern /${config.mockApiPattern}/. Walks run against schema-seeded mocks only; point the app at MSW or a localhost mock.`,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return {};
|
|
127
|
+
}
|
|
128
|
+
/** Throw the first violation (with the rest attached) when the contract is not met. */
|
|
129
|
+
export function assertContract(report) {
|
|
130
|
+
if (report.violations.length > 0)
|
|
131
|
+
throw ContractViolation.fromList(report.violations);
|
|
132
|
+
}
|
|
133
|
+
/** Convenience: load + check + assert in one call. */
|
|
134
|
+
export async function loadAndCheck(configPath, cwd, opts) {
|
|
135
|
+
const loaded = await loadConfig(configPath, cwd);
|
|
136
|
+
const report = await checkContract(loaded, opts);
|
|
137
|
+
assertContract(report);
|
|
138
|
+
return { ...loaded, report };
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type GraphQLSchema } from 'graphql';
|
|
2
|
+
import type { ContractSnapshot, LockstepConfig } from '@junejuly-lockstep/model';
|
|
3
|
+
export declare const NO_HASH = "none";
|
|
4
|
+
export declare const LOCKFILES: readonly ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"];
|
|
5
|
+
/** Parse SDL into a schema, raising a contract violation on syntax errors. */
|
|
6
|
+
export declare function parseSdl(sdl: string, label?: string): GraphQLSchema;
|
|
7
|
+
/** Canonical SDL: parsed, lexicographically sorted, re-printed. Formatting and ordering changes vanish. */
|
|
8
|
+
export declare function canonicalSdl(sdl: string): string;
|
|
9
|
+
/** sha256 of the canonical SDL. */
|
|
10
|
+
export declare function hashSdl(sdl: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* `AvailableSlotsQuery` -> `AvailableSlots`: codegen names the result type alias after the
|
|
13
|
+
* operation plus its kind; the key the snapshot stores is the operation name itself, which is
|
|
14
|
+
* also the `operationName` the app sends on the wire and the walker records per step. Reach
|
|
15
|
+
* (API, `domain/reach.ts`) intersects those two sets, so they must share a vocabulary.
|
|
16
|
+
*/
|
|
17
|
+
export declare function operationNameOfAlias(aliasName: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Per-operation response shape hashes from graphql-codegen output. Every exported
|
|
20
|
+
* `type FooQuery = {...}` / `type BarMutation = {...}` alias (not `*Variables`, not the
|
|
21
|
+
* schema root types) is hashed on its structural text with whitespace normalised and keyed by
|
|
22
|
+
* the operation name (`Foo`, `Bar`), never the alias.
|
|
23
|
+
*/
|
|
24
|
+
export declare function hashOperationShapes(codegenSource: string): Record<string, string>;
|
|
25
|
+
/** Whitespace-insensitive, trailing-separator-insensitive text of a type literal. */
|
|
26
|
+
export declare function normalizeShape(text: string): string;
|
|
27
|
+
export declare function findLockfile(rootDir: string): string | null;
|
|
28
|
+
/** sha256 of the lockfile with line endings normalised; `none` when the repo has no lockfile. */
|
|
29
|
+
export declare function hashLockfile(rootDir: string): {
|
|
30
|
+
hash: string;
|
|
31
|
+
file: string | null;
|
|
32
|
+
};
|
|
33
|
+
export interface SdlChange {
|
|
34
|
+
type: string;
|
|
35
|
+
criticality: string;
|
|
36
|
+
message: string;
|
|
37
|
+
path: string | null;
|
|
38
|
+
}
|
|
39
|
+
/** Human-readable change list between two SDL documents (graphql-inspector). */
|
|
40
|
+
export declare function diffSdl(oldSdl: string, newSdl: string): Promise<SdlChange[]>;
|
|
41
|
+
export declare function formatSdlChanges(changes: SdlChange[]): string;
|
|
42
|
+
export interface SnapshotOptions {
|
|
43
|
+
rootDir: string;
|
|
44
|
+
schema: LockstepConfig['schema'];
|
|
45
|
+
/** Where to write the human-readable change list and the current canonical SDL. */
|
|
46
|
+
outDir?: string;
|
|
47
|
+
/** Previous SDL text to diff against, when the caller has one. */
|
|
48
|
+
previousSdl?: string | null;
|
|
49
|
+
}
|
|
50
|
+
export interface SnapshotResult {
|
|
51
|
+
snapshot: ContractSnapshot;
|
|
52
|
+
lockfile: string | null;
|
|
53
|
+
changes: SdlChange[] | null;
|
|
54
|
+
/** Operation names whose shape hash differs from `previousOperations`, when provided. */
|
|
55
|
+
changedOperations: string[];
|
|
56
|
+
}
|
|
57
|
+
/** Build the `ContractSnapshot` for the payload and write the side files to `outDir`. */
|
|
58
|
+
export declare function snapshotContract(opts: SnapshotOptions & {
|
|
59
|
+
previousOperations?: Record<string, string> | null;
|
|
60
|
+
}): Promise<SnapshotResult>;
|
|
61
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAEA,OAAO,EAAqD,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC;AAGhG,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAIjF,eAAO,MAAM,OAAO,SAAS,CAAC;AAC9B,eAAO,MAAM,SAAS,4EAA6E,CAAC;AAEpG,8EAA8E;AAC9E,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,SAAW,GAAG,aAAa,CASrE;AAED,2GAA2G;AAC3G,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,mCAAmC;AACnC,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3C;AAKD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAYjF;AAED,qFAAqF;AACrF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMnD;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM3D;AAED,iGAAiG;AACjG,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAMnF;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,gFAAgF;AAChF,wBAAsB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAgBlF;AAQD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,MAAM,CAW7D;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC;IACjC,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC5B,yFAAyF;IACzF,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,yFAAyF;AACzF,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,eAAe,GAAG;IAAE,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;CAAE,GAC7E,OAAO,CAAC,cAAc,CAAC,CA4CzB"}
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { buildSchema, lexicographicSortSchema, printSchema } from 'graphql';
|
|
4
|
+
import { diff as inspectorDiff, CriticalityLevel } from '@graphql-inspector/core';
|
|
5
|
+
import { Project, SyntaxKind } from 'ts-morph';
|
|
6
|
+
import { ContractViolation } from './errors.js';
|
|
7
|
+
import { ensureDir, normalizeNewlines, sha256 } from './util.js';
|
|
8
|
+
export const NO_HASH = 'none';
|
|
9
|
+
export const LOCKFILES = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock', 'bun.lockb'];
|
|
10
|
+
/** Parse SDL into a schema, raising a contract violation on syntax errors. */
|
|
11
|
+
export function parseSdl(sdl, label = 'schema') {
|
|
12
|
+
try {
|
|
13
|
+
return buildSchema(sdl, { assumeValidSDL: true });
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
throw new ContractViolation(`schema:${label}`, `SDL does not parse: ${err instanceof Error ? err.message : String(err)}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Canonical SDL: parsed, lexicographically sorted, re-printed. Formatting and ordering changes vanish. */
|
|
20
|
+
export function canonicalSdl(sdl) {
|
|
21
|
+
return printSchema(lexicographicSortSchema(parseSdl(sdl)));
|
|
22
|
+
}
|
|
23
|
+
/** sha256 of the canonical SDL. */
|
|
24
|
+
export function hashSdl(sdl) {
|
|
25
|
+
return sha256(canonicalSdl(sdl));
|
|
26
|
+
}
|
|
27
|
+
const OPERATION_SUFFIX = /(Query|Mutation|Subscription)$/;
|
|
28
|
+
const ROOT_TYPES = new Set(['Query', 'Mutation', 'Subscription']);
|
|
29
|
+
/**
|
|
30
|
+
* `AvailableSlotsQuery` -> `AvailableSlots`: codegen names the result type alias after the
|
|
31
|
+
* operation plus its kind; the key the snapshot stores is the operation name itself, which is
|
|
32
|
+
* also the `operationName` the app sends on the wire and the walker records per step. Reach
|
|
33
|
+
* (API, `domain/reach.ts`) intersects those two sets, so they must share a vocabulary.
|
|
34
|
+
*/
|
|
35
|
+
export function operationNameOfAlias(aliasName) {
|
|
36
|
+
return aliasName.replace(OPERATION_SUFFIX, '');
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Per-operation response shape hashes from graphql-codegen output. Every exported
|
|
40
|
+
* `type FooQuery = {...}` / `type BarMutation = {...}` alias (not `*Variables`, not the
|
|
41
|
+
* schema root types) is hashed on its structural text with whitespace normalised and keyed by
|
|
42
|
+
* the operation name (`Foo`, `Bar`), never the alias.
|
|
43
|
+
*/
|
|
44
|
+
export function hashOperationShapes(codegenSource) {
|
|
45
|
+
const project = new Project({ useInMemoryFileSystem: true, skipLoadingLibFiles: true });
|
|
46
|
+
const sf = project.createSourceFile('codegen.ts', codegenSource);
|
|
47
|
+
const out = {};
|
|
48
|
+
for (const alias of sf.getDescendantsOfKind(SyntaxKind.TypeAliasDeclaration)) {
|
|
49
|
+
const name = alias.getName();
|
|
50
|
+
if (!OPERATION_SUFFIX.test(name) || ROOT_TYPES.has(name) || name.endsWith('Variables'))
|
|
51
|
+
continue;
|
|
52
|
+
const shape = alias.getTypeNode()?.getText() ?? '';
|
|
53
|
+
out[operationNameOfAlias(name)] = sha256(normalizeShape(shape));
|
|
54
|
+
}
|
|
55
|
+
return Object.fromEntries(Object.entries(out).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
|
|
56
|
+
}
|
|
57
|
+
/** Whitespace-insensitive, trailing-separator-insensitive text of a type literal. */
|
|
58
|
+
export function normalizeShape(text) {
|
|
59
|
+
return text
|
|
60
|
+
.replace(/\s+/g, ' ')
|
|
61
|
+
.replace(/\s*([{}:;,|<>()[\]?=])\s*/g, '$1')
|
|
62
|
+
.replace(/[;,]}/g, '}')
|
|
63
|
+
.trim();
|
|
64
|
+
}
|
|
65
|
+
export function findLockfile(rootDir) {
|
|
66
|
+
for (const name of LOCKFILES) {
|
|
67
|
+
const p = path.join(rootDir, name);
|
|
68
|
+
if (existsSync(p))
|
|
69
|
+
return p;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/** sha256 of the lockfile with line endings normalised; `none` when the repo has no lockfile. */
|
|
74
|
+
export function hashLockfile(rootDir) {
|
|
75
|
+
const file = findLockfile(rootDir);
|
|
76
|
+
if (!file)
|
|
77
|
+
return { hash: NO_HASH, file: null };
|
|
78
|
+
const content = readFileSync(file);
|
|
79
|
+
const text = file.endsWith('.lockb') ? content : normalizeNewlines(content.toString('utf8'));
|
|
80
|
+
return { hash: sha256(text), file: path.basename(file) };
|
|
81
|
+
}
|
|
82
|
+
/** Human-readable change list between two SDL documents (graphql-inspector). */
|
|
83
|
+
export async function diffSdl(oldSdl, newSdl) {
|
|
84
|
+
const changes = await inspectorDiff(parseSdl(oldSdl, 'previous'), parseSdl(newSdl, 'current'));
|
|
85
|
+
return changes
|
|
86
|
+
.map((c) => ({
|
|
87
|
+
type: String(c.type),
|
|
88
|
+
criticality: c.criticality.level,
|
|
89
|
+
message: c.message,
|
|
90
|
+
path: c.path ?? null,
|
|
91
|
+
}))
|
|
92
|
+
.sort((a, b) => rank(b.criticality) - rank(a.criticality) || (a.path ?? '').localeCompare(b.path ?? ''));
|
|
93
|
+
}
|
|
94
|
+
function rank(level) {
|
|
95
|
+
if (level === CriticalityLevel.Breaking)
|
|
96
|
+
return 2;
|
|
97
|
+
if (level === CriticalityLevel.Dangerous)
|
|
98
|
+
return 1;
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
export function formatSdlChanges(changes) {
|
|
102
|
+
if (changes.length === 0)
|
|
103
|
+
return '# GraphQL contract changes\n\nNo changes.\n';
|
|
104
|
+
const lines = ['# GraphQL contract changes', ''];
|
|
105
|
+
for (const level of ['BREAKING', 'DANGEROUS', 'NON_BREAKING']) {
|
|
106
|
+
const group = changes.filter((c) => c.criticality === level);
|
|
107
|
+
if (!group.length)
|
|
108
|
+
continue;
|
|
109
|
+
lines.push(`## ${level.toLowerCase().replace('_', '-')} (${group.length})`, '');
|
|
110
|
+
for (const c of group)
|
|
111
|
+
lines.push(`- ${c.message}${c.path ? ` (\`${c.path}\`)` : ''}`);
|
|
112
|
+
lines.push('');
|
|
113
|
+
}
|
|
114
|
+
return lines.join('\n');
|
|
115
|
+
}
|
|
116
|
+
/** Build the `ContractSnapshot` for the payload and write the side files to `outDir`. */
|
|
117
|
+
export async function snapshotContract(opts) {
|
|
118
|
+
const lock = hashLockfile(opts.rootDir);
|
|
119
|
+
let sdlHash = NO_HASH;
|
|
120
|
+
let operations = {};
|
|
121
|
+
let changes = null;
|
|
122
|
+
if (opts.schema) {
|
|
123
|
+
const sdlPath = path.resolve(opts.rootDir, opts.schema.sdlPath);
|
|
124
|
+
if (!existsSync(sdlPath))
|
|
125
|
+
throw new ContractViolation('schema.sdlPath', `${opts.schema.sdlPath} does not exist`);
|
|
126
|
+
const sdl = readFileSync(sdlPath, 'utf8');
|
|
127
|
+
sdlHash = hashSdl(sdl);
|
|
128
|
+
const codegenPath = path.resolve(opts.rootDir, opts.schema.codegenOutput);
|
|
129
|
+
if (!existsSync(codegenPath)) {
|
|
130
|
+
throw new ContractViolation('schema.codegenOutput', `${opts.schema.codegenOutput} does not exist; run graphql-codegen`);
|
|
131
|
+
}
|
|
132
|
+
operations = hashOperationShapes(readFileSync(codegenPath, 'utf8'));
|
|
133
|
+
if (opts.outDir) {
|
|
134
|
+
ensureDir(opts.outDir);
|
|
135
|
+
writeFileSync(path.join(opts.outDir, 'schema.canonical.graphql'), canonicalSdl(sdl));
|
|
136
|
+
if (opts.previousSdl) {
|
|
137
|
+
changes = await diffSdl(opts.previousSdl, sdl);
|
|
138
|
+
writeFileSync(path.join(opts.outDir, 'contract-changes.md'), formatSdlChanges(changes));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const changedOperations = opts.previousOperations
|
|
143
|
+
? Object.keys({ ...opts.previousOperations, ...operations })
|
|
144
|
+
.filter((k) => opts.previousOperations[k] !== operations[k])
|
|
145
|
+
.sort()
|
|
146
|
+
: [];
|
|
147
|
+
return {
|
|
148
|
+
snapshot: { sdlHash, operations, lockfileHash: lock.hash },
|
|
149
|
+
lockfile: lock.file,
|
|
150
|
+
changes,
|
|
151
|
+
changedOperations,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The determinism contract. Every rule here is enforced in `walk.ts`; the list is exported so
|
|
3
|
+
* `lockstep validate` and the README print the same words.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DETERMINISM_RULES: ReadonlyArray<{
|
|
6
|
+
rule: string;
|
|
7
|
+
how: string;
|
|
8
|
+
}>;
|
|
9
|
+
export interface HashMap {
|
|
10
|
+
[relPath: string]: string;
|
|
11
|
+
}
|
|
12
|
+
/** sha256 of every PNG under `dir`, keyed by posix relative path. */
|
|
13
|
+
export declare function hashPngs(dir: string): Promise<HashMap>;
|
|
14
|
+
export interface HashComparison {
|
|
15
|
+
identical: string[];
|
|
16
|
+
different: string[];
|
|
17
|
+
onlyInFirst: string[];
|
|
18
|
+
onlyInSecond: string[];
|
|
19
|
+
deterministic: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function compareHashMaps(a: HashMap, b: HashMap): HashComparison;
|
|
22
|
+
export declare function formatDeterminismReport(c: HashComparison): string;
|
|
23
|
+
//# sourceMappingURL=determinism.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"determinism.d.ts","sourceRoot":"","sources":["../src/determinism.ts"],"names":[],"mappings":"AAKA;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,aAAa,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CA0C1E,CAAC;AAEF,MAAM,WAAW,OAAO;IACtB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AAED,qEAAqE;AACrE,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAK5D;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,cAAc,CAkBtE;AAED,wBAAgB,uBAAuB,CAAC,CAAC,EAAE,cAAc,GAAG,MAAM,CAqBjE"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import { sha256, toPosix } from './util.js';
|
|
5
|
+
/**
|
|
6
|
+
* The determinism contract. Every rule here is enforced in `walk.ts`; the list is exported so
|
|
7
|
+
* `lockstep validate` and the README print the same words.
|
|
8
|
+
*/
|
|
9
|
+
export const DETERMINISM_RULES = [
|
|
10
|
+
{
|
|
11
|
+
rule: 'Frozen clock',
|
|
12
|
+
how: 'page.clock.install + setFixedTime at FROZEN_EPOCH_MS (2026-01-15T09:30:00Z); Date.now() never moves',
|
|
13
|
+
},
|
|
14
|
+
{ rule: 'Fixed viewport', how: '1280x800, deviceScaleFactor 1, not mobile, no touch' },
|
|
15
|
+
{
|
|
16
|
+
rule: 'No motion',
|
|
17
|
+
how: 'reducedMotion: reduce, CSS injected before first paint disabling animations, transitions, caret, smooth scroll; screenshots taken with animations: disabled',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
rule: 'Fonts awaited',
|
|
21
|
+
how: 'document.fonts.ready plus two animation frames before every screenshot; Chromium font hinting and subpixel positioning disabled',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
rule: 'Full tile raster',
|
|
25
|
+
how: '--disable-partial-raster: a tile whose content changed after first paint (an app that renders its root once its data arrived) is rastered whole, never only inside the damage rect, whose edges would otherwise differ by a unit of anti-aliasing from walk to walk',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
rule: 'Settled after the root appears',
|
|
29
|
+
how: 'network idle, fonts and four animation frames again once the state root is visible (an app that renders after fetching through a mock worker shows its root after the first settle); the pointer is parked at (0,0) after every click so no hover depends on where it landed',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
rule: 'Fixed locale and zone',
|
|
33
|
+
how: 'locale en-US, timezoneId UTC, TZ=UTC in the app process, colorScheme light, sRGB colour profile',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
rule: 'Seeded mocks only',
|
|
37
|
+
how: 'LOCKSTEP_SCENARIO/LOCKSTEP_SEED env plus ?lockstep_scenario= query; appApiBase must match mockApiPattern or the run is refused',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
rule: 'Deterministic ordering',
|
|
41
|
+
how: 'scenarios by name, journeys by key, paths by target state then event sequence, files by path',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
rule: 'Fresh context per path',
|
|
45
|
+
how: 'a new browser context per path: no storage, cookies or cache leak between paths',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
rule: 'Full-page CSS-pixel shots',
|
|
49
|
+
how: 'fullPage: true, scale: css, caret hidden, scrollbars hidden',
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
/** sha256 of every PNG under `dir`, keyed by posix relative path. */
|
|
53
|
+
export async function hashPngs(dir) {
|
|
54
|
+
const files = (await fg(['**/*.png'], { cwd: dir, onlyFiles: true })).map(toPosix).sort();
|
|
55
|
+
const out = {};
|
|
56
|
+
for (const f of files)
|
|
57
|
+
out[f] = sha256(readFileSync(path.join(dir, f)));
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
export function compareHashMaps(a, b) {
|
|
61
|
+
const identical = [];
|
|
62
|
+
const different = [];
|
|
63
|
+
const onlyInFirst = [];
|
|
64
|
+
const onlyInSecond = [];
|
|
65
|
+
for (const k of Object.keys(a).sort()) {
|
|
66
|
+
if (!(k in b))
|
|
67
|
+
onlyInFirst.push(k);
|
|
68
|
+
else if (a[k] === b[k])
|
|
69
|
+
identical.push(k);
|
|
70
|
+
else
|
|
71
|
+
different.push(k);
|
|
72
|
+
}
|
|
73
|
+
for (const k of Object.keys(b).sort())
|
|
74
|
+
if (!(k in a))
|
|
75
|
+
onlyInSecond.push(k);
|
|
76
|
+
return {
|
|
77
|
+
identical,
|
|
78
|
+
different,
|
|
79
|
+
onlyInFirst,
|
|
80
|
+
onlyInSecond,
|
|
81
|
+
deterministic: different.length === 0 && onlyInFirst.length === 0 && onlyInSecond.length === 0,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function formatDeterminismReport(c) {
|
|
85
|
+
const lines = [
|
|
86
|
+
`Determinism check: ${c.deterministic ? 'PASS' : 'FAIL'}`,
|
|
87
|
+
` identical ${c.identical.length}, different ${c.different.length}, missing in second walk ${c.onlyInFirst.length}, extra in second walk ${c.onlyInSecond.length}`,
|
|
88
|
+
];
|
|
89
|
+
for (const f of c.different)
|
|
90
|
+
lines.push(` differs: ${f}`);
|
|
91
|
+
for (const f of c.onlyInFirst)
|
|
92
|
+
lines.push(` missing: ${f}`);
|
|
93
|
+
for (const f of c.onlyInSecond)
|
|
94
|
+
lines.push(` extra: ${f}`);
|
|
95
|
+
if (!c.deterministic) {
|
|
96
|
+
lines.push('', ' Byte-identical screenshots are a hard requirement. Usual causes: unseeded data, Date.now() read');
|
|
97
|
+
lines.push(' outside the page (server components), animations started after first paint, lazy-loaded fonts or');
|
|
98
|
+
lines.push(' images without fixed dimensions, randomised ids rendered into the DOM. See README "Determinism rules".');
|
|
99
|
+
}
|
|
100
|
+
return lines.join('\n');
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=determinism.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"determinism.js","sourceRoot":"","sources":["../src/determinism.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A named violation of the client codebase contract (SPEC.md "Client codebase contract").
|
|
3
|
+
* `item` names the missing or invalid piece so CI output says exactly what to fix.
|
|
4
|
+
*/
|
|
5
|
+
export declare class ContractViolation extends Error {
|
|
6
|
+
readonly item: string;
|
|
7
|
+
readonly items: Array<{
|
|
8
|
+
item: string;
|
|
9
|
+
message: string;
|
|
10
|
+
}>;
|
|
11
|
+
constructor(item: string, message: string, more?: Array<{
|
|
12
|
+
item: string;
|
|
13
|
+
message: string;
|
|
14
|
+
}>);
|
|
15
|
+
static fromList(items: Array<{
|
|
16
|
+
item: string;
|
|
17
|
+
message: string;
|
|
18
|
+
}>): ContractViolation;
|
|
19
|
+
}
|
|
20
|
+
/** Something outside the client's contract failed: network, browser, filesystem, the platform API. */
|
|
21
|
+
export declare class InfrastructureError extends Error {
|
|
22
|
+
readonly cause_: unknown;
|
|
23
|
+
constructor(message: string, cause?: unknown);
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;gBAE7C,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAM;IAY9F,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,iBAAiB;CAKpF;AAED,sGAAsG;AACtG,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;gBAEb,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAS7C"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A named violation of the client codebase contract (SPEC.md "Client codebase contract").
|
|
3
|
+
* `item` names the missing or invalid piece so CI output says exactly what to fix.
|
|
4
|
+
*/
|
|
5
|
+
export class ContractViolation extends Error {
|
|
6
|
+
item;
|
|
7
|
+
items;
|
|
8
|
+
constructor(item, message, more = []) {
|
|
9
|
+
const items = [{ item, message }, ...more];
|
|
10
|
+
super(items.length === 1
|
|
11
|
+
? `Contract violation [${item}]: ${message}`
|
|
12
|
+
: `${items.length} contract violations:\n${items.map((i) => ` - [${i.item}] ${i.message}`).join('\n')}`);
|
|
13
|
+
this.name = 'ContractViolation';
|
|
14
|
+
this.item = item;
|
|
15
|
+
this.items = items;
|
|
16
|
+
}
|
|
17
|
+
static fromList(items) {
|
|
18
|
+
const [first, ...rest] = items;
|
|
19
|
+
if (!first)
|
|
20
|
+
throw new Error('ContractViolation.fromList needs at least one item');
|
|
21
|
+
return new ContractViolation(first.item, first.message, rest);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Something outside the client's contract failed: network, browser, filesystem, the platform API. */
|
|
25
|
+
export class InfrastructureError extends Error {
|
|
26
|
+
cause_;
|
|
27
|
+
constructor(message, cause) {
|
|
28
|
+
super(cause instanceof Error && !message.includes(cause.message)
|
|
29
|
+
? `${message}: ${cause.message}`
|
|
30
|
+
: message);
|
|
31
|
+
this.name = 'InfrastructureError';
|
|
32
|
+
this.cause_ = cause;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=errors.js.map
|