fidelity-kit 0.1.0 → 0.2.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/README.md +133 -5
- package/dist/commands/build.d.ts +2 -0
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +6 -2
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/dev.d.ts +7 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +9 -2
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/hash.d.ts +2 -0
- package/dist/commands/hash.d.ts.map +1 -1
- package/dist/commands/hash.js +7 -3
- package/dist/commands/hash.js.map +1 -1
- package/dist/commands/process.d.ts +6 -0
- package/dist/commands/process.d.ts.map +1 -1
- package/dist/commands/process.js +35 -7
- package/dist/commands/process.js.map +1 -1
- package/dist/commands/serve.d.ts +3 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +2 -1
- package/dist/commands/serve.js.map +1 -1
- package/dist/core/compare.d.ts +4 -2
- package/dist/core/compare.d.ts.map +1 -1
- package/dist/core/compare.js +7 -5
- package/dist/core/compare.js.map +1 -1
- package/dist/core/hash.d.ts +2 -0
- package/dist/core/hash.d.ts.map +1 -1
- package/dist/core/hash.js +37 -10
- package/dist/core/hash.js.map +1 -1
- package/dist/core/hash.test.js +8 -1
- package/dist/core/hash.test.js.map +1 -1
- package/dist/core/process.d.ts +52 -3
- package/dist/core/process.d.ts.map +1 -1
- package/dist/core/process.js +450 -51
- package/dist/core/process.js.map +1 -1
- package/dist/core/process.test.d.ts +2 -0
- package/dist/core/process.test.d.ts.map +1 -0
- package/dist/core/process.test.js +269 -0
- package/dist/core/process.test.js.map +1 -0
- package/dist/core/progress.d.ts +8 -0
- package/dist/core/progress.d.ts.map +1 -0
- package/dist/core/progress.js +2 -0
- package/dist/core/progress.js.map +1 -0
- package/dist/core/scan.d.ts +8 -1
- package/dist/core/scan.d.ts.map +1 -1
- package/dist/core/scan.js +72 -7
- package/dist/core/scan.js.map +1 -1
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/schema.js +8 -2
- package/dist/core/schema.js.map +1 -1
- package/dist/core/suite.test.js +61 -2
- package/dist/core/suite.test.js.map +1 -1
- package/dist/progress.d.ts +12 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +46 -0
- package/dist/progress.js.map +1 -0
- package/dist/progress.test.d.ts +2 -0
- package/dist/progress.test.d.ts.map +1 -0
- package/dist/progress.test.js +36 -0
- package/dist/progress.test.js.map +1 -0
- package/dist/run.d.ts +4 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +34 -6
- package/dist/run.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +25 -2
- package/dist/server.js.map +1 -1
- package/dist/server.test.js +55 -2
- package/dist/server.test.js.map +1 -1
- package/dist/watch.d.ts +9 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +96 -0
- package/dist/watch.js.map +1 -0
- package/dist/watch.test.d.ts +2 -0
- package/dist/watch.test.d.ts.map +1 -0
- package/dist/watch.test.js +138 -0
- package/dist/watch.test.js.map +1 -0
- package/package.json +11 -1
- package/viewer/assets/{ViewControls-hn4PfGV3.js → ViewControls-Ck9smy3A.js} +2 -2
- package/viewer/assets/_-Bn1LyOgm.js +1 -0
- package/viewer/assets/{index-D9G4C6Ns.js → index-BZCd8VsK.js} +2 -2
- package/viewer/assets/index-VKD_ZeZu.css +2 -0
- package/viewer/assets/{routes-DmeR6gGl.js → routes-sEPFkb5w.js} +1 -1
- package/viewer/index.html +2 -2
- package/dist/.tsbuildinfo +0 -1
- package/viewer/assets/_-BAkV94Fq.js +0 -1
- package/viewer/assets/index-D8YJNCs_.css +0 -2
package/dist/core/hash.test.js
CHANGED
|
@@ -22,7 +22,14 @@ async function suite() {
|
|
|
22
22
|
const read = async (root) => JSON.parse(await readFile(join(root, HASHES_FILE), 'utf8'));
|
|
23
23
|
test('hash is deterministic, sorted, covers only served images, and is incremental', async () => {
|
|
24
24
|
const root = await suite();
|
|
25
|
-
|
|
25
|
+
const progress = [];
|
|
26
|
+
expect(await hashSuite(root, { onProgress: (update) => progress.push(update) })).toEqual({
|
|
27
|
+
hashed: 3,
|
|
28
|
+
reused: 0,
|
|
29
|
+
total: 3,
|
|
30
|
+
});
|
|
31
|
+
expect(progress.at(-1)).toEqual({ phase: 'Writing hashes', completed: 1, total: 1, unit: 'files' });
|
|
32
|
+
expect(progress.find((update) => update.phase === 'Hashing' && update.completed === 3)).toBeDefined();
|
|
26
33
|
const first = await readFile(join(root, HASHES_FILE), 'utf8');
|
|
27
34
|
const json = JSON.parse(first);
|
|
28
35
|
expect(json.version).toBe(1);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hash.test.js","sourceRoot":"","sources":["../../src/core/hash.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC/E,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"hash.test.js","sourceRoot":"","sources":["../../src/core/hash.test.ts"],"names":[],"mappings":"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"}
|
package/dist/core/process.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type ImageMetrics } from './compare.js';
|
|
2
2
|
import { type Suite } from './scan.js';
|
|
3
|
+
import type { FidelityConfig } from './schema.js';
|
|
4
|
+
import type { ProgressCallback } from './progress.js';
|
|
3
5
|
export interface MetricsRecord extends ImageMetrics {
|
|
4
6
|
width: number;
|
|
5
7
|
height: number;
|
|
@@ -16,9 +18,56 @@ export interface ProcessResult {
|
|
|
16
18
|
error: string;
|
|
17
19
|
}[];
|
|
18
20
|
}
|
|
19
|
-
|
|
20
|
-
export declare function processSuite(root: string, opts?: {
|
|
21
|
+
export interface ProcessOptions {
|
|
21
22
|
force?: boolean;
|
|
23
|
+
concurrency?: number;
|
|
22
24
|
onCompute?: (file: string) => void;
|
|
23
|
-
|
|
25
|
+
onProgress?: ProgressCallback;
|
|
26
|
+
}
|
|
27
|
+
/** A fixed configuration, small per-scene dependency maps, and one bounded comparison queue. */
|
|
28
|
+
export declare class SuiteProcessor {
|
|
29
|
+
readonly root: string;
|
|
30
|
+
private opts;
|
|
31
|
+
readonly config: FidelityConfig;
|
|
32
|
+
private suite;
|
|
33
|
+
private scenes;
|
|
34
|
+
private metrics;
|
|
35
|
+
private pending;
|
|
36
|
+
private unsettled;
|
|
37
|
+
private epochs;
|
|
38
|
+
private dirty;
|
|
39
|
+
private active;
|
|
40
|
+
private limit;
|
|
41
|
+
private concurrency;
|
|
42
|
+
private result;
|
|
43
|
+
private flushing?;
|
|
44
|
+
private force;
|
|
45
|
+
private indexDirty;
|
|
46
|
+
private progressCompleted;
|
|
47
|
+
private progressTotal;
|
|
48
|
+
private reportComparing;
|
|
49
|
+
private constructor();
|
|
50
|
+
static create(root: string, opts?: ProcessOptions): Promise<SuiteProcessor>;
|
|
51
|
+
/** Only source inputs qualify. Generated deltas, metrics, index and temporary files never do. */
|
|
52
|
+
sceneForInput(path: string): string | null;
|
|
53
|
+
/** Invalidate synchronously, including work already running; reconciliation happens at the batch boundary. */
|
|
54
|
+
notify(paths: Iterable<string>, defer?: boolean): void;
|
|
55
|
+
/** Release one coalesced watcher batch without delaying invalidation of running work. */
|
|
56
|
+
settle(): void;
|
|
57
|
+
get needsFlush(): boolean;
|
|
58
|
+
removeDirectory(path: string, defer?: boolean): void;
|
|
59
|
+
/** Comparisons start as scenes are discovered; no second walk or metrics reread is needed. */
|
|
60
|
+
initialize(process?: boolean): Promise<ProcessResult>;
|
|
61
|
+
flush(): Promise<ProcessResult>;
|
|
62
|
+
private reconcile;
|
|
63
|
+
private current;
|
|
64
|
+
private pump;
|
|
65
|
+
private readInputs;
|
|
66
|
+
private processPair;
|
|
67
|
+
private drain;
|
|
68
|
+
}
|
|
69
|
+
/** Process discovered scenes through the same scheduler used by watch mode. */
|
|
70
|
+
export declare function processSuite(root: string, opts?: ProcessOptions): Promise<ProcessResult>;
|
|
71
|
+
/** One-shot compatibility helper. Watch mode retains its SuiteProcessor instead. */
|
|
72
|
+
export declare function processChanges(root: string, paths: Iterable<string>, onProgress?: ProgressCallback): Promise<ProcessResult>;
|
|
24
73
|
//# sourceMappingURL=process.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/core/process.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/core/process.ts"],"names":[],"mappings":"AAKA,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAoE,KAAK,KAAK,EAAE,MAAM,WAAW,CAAC;AAEzG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB;AACD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;CAAE,CAAC;AAC5E,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC3C;AACD,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B;AA4DD,gGAAgG;AAChG,qBAAa,cAAc;IA6BvB,QAAQ,CAAC,IAAI,EAAE,MAAM;IAErB,OAAO,CAAC,IAAI;IA9Bd,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,KAAK,CAAmB;IAChC,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,KAAK,CAA4B;IACzC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,QAAQ,CAAC,CAAyB;IAC1C,OAAO,CAAC,KAAK,CAAU;IACvB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,aAAa,CAAK;IAE1B,OAAO,CAAC,eAAe;IASvB,OAAO,eAYN;IAED,OAAa,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,cAAmB,2BAE1D;IAED,iGAAiG;IACjG,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWzC;IAED,8GAA8G;IAC9G,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,UAAQ,QAO5C;IAED,yFAAyF;IACzF,MAAM,SAGL;IAED,IAAI,UAAU,YAEb;IAED,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,UAAQ,QAO1C;IAED,8FAA8F;IACxF,UAAU,CAAC,OAAO,UAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CAyBvD;IAED,KAAK,IAAI,OAAO,CAAC,aAAa,CAAC,CAM9B;YAEa,SAAS;IA8DvB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,IAAI;YAkBE,UAAU;YAQV,WAAW;YAuFX,KAAK;CAiEpB;AAED,+EAA+E;AAC/E,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,cAAmB,GAAG,OAAO,CAAC,aAAa,CAAC,CAElG;AAED,oFAAoF;AACpF,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,EACvB,UAAU,CAAC,EAAE,gBAAgB,GAC5B,OAAO,CAAC,aAAa,CAAC,CASxB"}
|
package/dist/core/process.js
CHANGED
|
@@ -1,65 +1,464 @@
|
|
|
1
|
-
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
|
-
import {
|
|
1
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { availableParallelism } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import pLimit from 'p-limit';
|
|
3
6
|
import { compareImages } from './compare.js';
|
|
4
|
-
import {
|
|
7
|
+
import { readConfig, scanScene, scanSuite } from './scan.js';
|
|
5
8
|
import { deltaFile, imageFile, metricsFile } from './paths.js';
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
9
|
+
const emptyResult = () => ({ computed: 0, skipped: 0, failed: [] });
|
|
10
|
+
const signatureKey = (inputs) => JSON.stringify(inputs);
|
|
11
|
+
async function signature(path) {
|
|
12
|
+
try {
|
|
13
|
+
const s = await stat(path);
|
|
14
|
+
return s.isFile() ? { mtimeMs: s.mtimeMs, size: s.size } : null;
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR')
|
|
18
|
+
return null;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function validMetrics(value) {
|
|
23
|
+
if (!value || typeof value !== 'object')
|
|
24
|
+
return false;
|
|
25
|
+
const r = value;
|
|
26
|
+
return ((r.psnr === null || Number.isFinite(r.psnr)) &&
|
|
27
|
+
[r.rmse, r.mae, r.maxError].every((n) => Number.isFinite(n) && n >= 0 && n <= 1) &&
|
|
28
|
+
Number.isInteger(r.width) &&
|
|
29
|
+
r.width > 0 &&
|
|
30
|
+
Number.isInteger(r.height) &&
|
|
31
|
+
r.height > 0 &&
|
|
32
|
+
typeof r.generatedAt === 'string');
|
|
33
|
+
}
|
|
34
|
+
async function atomicWrite(file, data) {
|
|
35
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
36
|
+
try {
|
|
37
|
+
await writeFile(tmp, data);
|
|
38
|
+
await rename(tmp, file);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
await rm(tmp, { force: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** A fixed configuration, small per-scene dependency maps, and one bounded comparison queue. */
|
|
45
|
+
export class SuiteProcessor {
|
|
46
|
+
root;
|
|
47
|
+
opts;
|
|
48
|
+
config;
|
|
49
|
+
suite;
|
|
50
|
+
scenes = new Map();
|
|
51
|
+
metrics = {};
|
|
52
|
+
pending = new Set();
|
|
53
|
+
unsettled = new Set();
|
|
54
|
+
epochs = new Map();
|
|
55
|
+
dirty = new Set();
|
|
56
|
+
active = new Set();
|
|
57
|
+
limit;
|
|
58
|
+
concurrency;
|
|
59
|
+
result = emptyResult();
|
|
60
|
+
flushing;
|
|
61
|
+
force;
|
|
62
|
+
indexDirty = false;
|
|
63
|
+
progressCompleted = 0;
|
|
64
|
+
progressTotal = 0;
|
|
65
|
+
reportComparing() {
|
|
66
|
+
this.opts.onProgress?.({
|
|
67
|
+
phase: 'Comparing',
|
|
68
|
+
completed: this.progressCompleted,
|
|
69
|
+
total: this.progressTotal,
|
|
70
|
+
unit: 'pairs',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
constructor(root, config, opts) {
|
|
74
|
+
this.root = root;
|
|
75
|
+
this.opts = opts;
|
|
76
|
+
this.config = config;
|
|
77
|
+
this.suite = { config, hasReadme: false, root: { path: '', hasReadme: false, groups: [], scenes: [] } };
|
|
78
|
+
this.concurrency = opts.concurrency ?? availableParallelism();
|
|
79
|
+
if (!Number.isInteger(this.concurrency) || this.concurrency < 1)
|
|
80
|
+
throw new Error('Concurrency must be a positive integer');
|
|
81
|
+
this.limit = pLimit(this.concurrency);
|
|
82
|
+
this.force = opts.force ?? false;
|
|
83
|
+
}
|
|
84
|
+
static async create(root, opts = {}) {
|
|
85
|
+
return new SuiteProcessor(root, await readConfig(root), opts);
|
|
86
|
+
}
|
|
87
|
+
/** Only source inputs qualify. Generated deltas, metrics, index and temporary files never do. */
|
|
88
|
+
sceneForInput(path) {
|
|
89
|
+
const parts = path.split('/');
|
|
90
|
+
if (parts.some((p) => !p || p.startsWith('.')))
|
|
91
|
+
return null;
|
|
92
|
+
const file = parts.pop();
|
|
93
|
+
if (file === 'scene.json' && parts.length)
|
|
94
|
+
return parts.join('/');
|
|
95
|
+
if (file === 'README.md')
|
|
96
|
+
return parts.join('/');
|
|
97
|
+
if (file.includes('.vs-') && file.endsWith('.delta.avif'))
|
|
98
|
+
return null;
|
|
99
|
+
const output = parts.pop();
|
|
100
|
+
if (!parts.length || !this.config.outputs.some((o) => o.id === output))
|
|
101
|
+
return null;
|
|
102
|
+
if (!this.config.renderers.some((r) => imageFile(r.id) === file))
|
|
103
|
+
return null;
|
|
104
|
+
return parts.join('/');
|
|
105
|
+
}
|
|
106
|
+
/** Invalidate synchronously, including work already running; reconciliation happens at the batch boundary. */
|
|
107
|
+
notify(paths, defer = false) {
|
|
108
|
+
for (const path of paths) {
|
|
109
|
+
const scene = this.sceneForInput(path);
|
|
110
|
+
if (scene === null)
|
|
111
|
+
continue;
|
|
112
|
+
(defer ? this.unsettled : this.pending).add(scene);
|
|
113
|
+
this.epochs.set(scene, (this.epochs.get(scene) ?? 0) + 1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Release one coalesced watcher batch without delaying invalidation of running work. */
|
|
117
|
+
settle() {
|
|
118
|
+
for (const scene of this.unsettled)
|
|
119
|
+
this.pending.add(scene);
|
|
120
|
+
this.unsettled.clear();
|
|
121
|
+
}
|
|
122
|
+
get needsFlush() {
|
|
123
|
+
return !!(this.pending.size || this.active.size || this.indexDirty);
|
|
124
|
+
}
|
|
125
|
+
removeDirectory(path, defer = false) {
|
|
126
|
+
for (const scene of this.scenes.keys()) {
|
|
127
|
+
if (scene === path || scene.startsWith(`${path}/`) || path.startsWith(`${scene}/`)) {
|
|
128
|
+
(defer ? this.unsettled : this.pending).add(scene);
|
|
129
|
+
this.epochs.set(scene, (this.epochs.get(scene) ?? 0) + 1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Comparisons start as scenes are discovered; no second walk or metrics reread is needed. */
|
|
134
|
+
async initialize(process = true) {
|
|
135
|
+
if (!process) {
|
|
136
|
+
const index = JSON.parse(await readFile(join(this.root, 'index.json'), 'utf8'));
|
|
137
|
+
this.metrics = index.metrics;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
this.suite = await scanSuite(this.root, {
|
|
141
|
+
config: this.config,
|
|
142
|
+
onProgress: this.opts.onProgress,
|
|
143
|
+
onScene: async (scene) => {
|
|
144
|
+
await this.reconcile(scene.path, scene, process);
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
this.dirty.clear();
|
|
150
|
+
await Promise.all(this.active);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
if (!process) {
|
|
154
|
+
const valid = new Set([...this.scenes.values()].flatMap((scene) => [...scene.pairs.keys()]));
|
|
155
|
+
for (const key of Object.keys(this.metrics))
|
|
156
|
+
if (!valid.has(key))
|
|
157
|
+
delete this.metrics[key];
|
|
158
|
+
}
|
|
159
|
+
this.indexDirty = process;
|
|
160
|
+
// Events collected during discovery are reconciled even when the initial processing was disabled.
|
|
161
|
+
return this.flush();
|
|
162
|
+
}
|
|
163
|
+
flush() {
|
|
164
|
+
if (!this.flushing)
|
|
165
|
+
this.flushing = this.drain().finally(() => {
|
|
166
|
+
this.flushing = undefined;
|
|
167
|
+
});
|
|
168
|
+
return this.flushing;
|
|
169
|
+
}
|
|
170
|
+
async reconcile(rel, node, process = true) {
|
|
171
|
+
const old = this.scenes.get(rel);
|
|
172
|
+
const pairs = new Map();
|
|
173
|
+
if (node) {
|
|
174
|
+
for (const [output, renderers] of Object.entries(node.images)) {
|
|
175
|
+
const images = new Map(await Promise.all(renderers.map(async (r) => {
|
|
176
|
+
const path = `${rel}/${output}/${imageFile(r)}`;
|
|
177
|
+
return [r, { path, signature: await signature(join(this.root, path)) }];
|
|
178
|
+
})));
|
|
179
|
+
for (const ref of this.config.renderers.filter((r) => r.reference && images.has(r.id))) {
|
|
180
|
+
for (const renderer of renderers.filter((r) => r !== ref.id)) {
|
|
181
|
+
const a = images.get(ref.id);
|
|
182
|
+
const b = images.get(renderer);
|
|
183
|
+
if (!a.signature || !b.signature)
|
|
184
|
+
continue;
|
|
185
|
+
const key = `${rel}/${output}/${metricsFile(renderer, ref.id)}`;
|
|
186
|
+
const inputs = { reference: a.signature, renderer: b.signature };
|
|
187
|
+
const pair = old?.pairs.get(key) ?? {
|
|
188
|
+
key,
|
|
189
|
+
scene: rel,
|
|
190
|
+
reference: a.path,
|
|
191
|
+
renderer: b.path,
|
|
192
|
+
delta: `${rel}/${output}/${deltaFile(renderer, ref.id)}`,
|
|
193
|
+
inputs,
|
|
194
|
+
running: false,
|
|
195
|
+
force: this.force,
|
|
196
|
+
};
|
|
197
|
+
if (signatureKey(pair.inputs) !== signatureKey(inputs)) {
|
|
198
|
+
pair.inputs = inputs;
|
|
199
|
+
pair.attempted = undefined;
|
|
200
|
+
delete this.metrics[key];
|
|
201
|
+
}
|
|
202
|
+
pairs.set(key, pair);
|
|
203
|
+
if (process && pair.attempted !== signatureKey(inputs)) {
|
|
204
|
+
if (!this.dirty.has(pair) && !pair.running) {
|
|
205
|
+
this.progressTotal++;
|
|
206
|
+
this.reportComparing();
|
|
207
|
+
}
|
|
208
|
+
this.dirty.add(pair);
|
|
209
|
+
}
|
|
26
210
|
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
this.scenes.set(rel, { node, pairs });
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
this.scenes.delete(rel);
|
|
217
|
+
this.epochs.delete(rel);
|
|
218
|
+
}
|
|
219
|
+
for (const [key, pair] of old?.pairs ?? []) {
|
|
220
|
+
if (!pairs.has(key)) {
|
|
221
|
+
this.dirty.delete(pair);
|
|
222
|
+
delete this.metrics[key];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
replaceScene(this.suite.root, rel, node);
|
|
226
|
+
this.indexDirty = true;
|
|
227
|
+
if (process)
|
|
228
|
+
this.pump();
|
|
229
|
+
}
|
|
230
|
+
current(pair, epoch) {
|
|
231
|
+
return this.scenes.get(pair.scene)?.pairs.get(pair.key) === pair && (this.epochs.get(pair.scene) ?? 0) === epoch;
|
|
232
|
+
}
|
|
233
|
+
pump() {
|
|
234
|
+
for (const pair of this.dirty) {
|
|
235
|
+
if (this.active.size >= this.concurrency)
|
|
236
|
+
break;
|
|
237
|
+
if (pair.running || this.unsettled.has(pair.scene))
|
|
238
|
+
continue;
|
|
239
|
+
this.dirty.delete(pair);
|
|
240
|
+
if (pair.attempted === signatureKey(pair.inputs))
|
|
241
|
+
continue;
|
|
242
|
+
pair.running = true;
|
|
243
|
+
const task = this.limit(() => this.processPair(pair)).finally(() => {
|
|
244
|
+
this.progressCompleted++;
|
|
245
|
+
this.reportComparing();
|
|
246
|
+
pair.running = false;
|
|
247
|
+
this.active.delete(task);
|
|
248
|
+
this.pump();
|
|
249
|
+
});
|
|
250
|
+
this.active.add(task);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async readInputs(pair) {
|
|
254
|
+
const [reference, renderer] = await Promise.all([
|
|
255
|
+
signature(join(this.root, pair.reference)),
|
|
256
|
+
signature(join(this.root, pair.renderer)),
|
|
257
|
+
]);
|
|
258
|
+
return reference && renderer ? { reference, renderer } : null;
|
|
259
|
+
}
|
|
260
|
+
async processPair(pair) {
|
|
261
|
+
const epoch = this.epochs.get(pair.scene) ?? 0;
|
|
262
|
+
const inputs = pair.inputs;
|
|
263
|
+
const key = signatureKey(inputs);
|
|
264
|
+
const metricsPath = join(this.root, pair.key);
|
|
265
|
+
// Bump the recipe version when comparison or encoding semantics change.
|
|
266
|
+
const source = { version: 1, delta: this.config.delta, ...inputs };
|
|
267
|
+
const stillCurrent = async () => {
|
|
268
|
+
const actual = await this.readInputs(pair);
|
|
269
|
+
return this.current(pair, epoch) && signatureKey(actual) === key;
|
|
270
|
+
};
|
|
271
|
+
const retry = () => {
|
|
272
|
+
if (this.scenes.get(pair.scene)?.pairs.get(pair.key) === pair) {
|
|
273
|
+
pair.attempted = undefined;
|
|
274
|
+
if (!this.unsettled.has(pair.scene))
|
|
275
|
+
this.pending.add(pair.scene);
|
|
276
|
+
this.indexDirty = true;
|
|
277
|
+
delete this.metrics[pair.key];
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
try {
|
|
281
|
+
if (!(await stillCurrent())) {
|
|
282
|
+
retry();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
let record;
|
|
286
|
+
if (!pair.force) {
|
|
287
|
+
try {
|
|
288
|
+
const { source: saved, ...cached } = JSON.parse(await readFile(metricsPath, 'utf8'));
|
|
289
|
+
if (JSON.stringify(saved) === JSON.stringify(source) &&
|
|
290
|
+
validMetrics(cached) &&
|
|
291
|
+
(!this.config.delta || (await signature(join(this.root, pair.delta)))))
|
|
292
|
+
record = cached;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
/* Missing or invalid metrics are recomputed. */
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (record) {
|
|
299
|
+
if (!(await stillCurrent())) {
|
|
300
|
+
retry();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
this.result.skipped++;
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
const compared = await compareImages(join(this.root, pair.reference), join(this.root, pair.renderer), {
|
|
307
|
+
delta: this.config.delta,
|
|
308
|
+
});
|
|
309
|
+
record = {
|
|
310
|
+
...compared.metrics,
|
|
311
|
+
width: compared.width,
|
|
312
|
+
height: compared.height,
|
|
313
|
+
generatedAt: new Date().toISOString(),
|
|
314
|
+
};
|
|
315
|
+
if (!(await stillCurrent())) {
|
|
316
|
+
retry();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
// Metrics are the commit marker: publish them only after all required artifacts exist.
|
|
320
|
+
if (this.config.delta)
|
|
321
|
+
await atomicWrite(join(this.root, pair.delta), compared.deltaImage);
|
|
322
|
+
await atomicWrite(metricsPath, JSON.stringify({ ...record, source }, null, 2) + '\n');
|
|
323
|
+
if (!(await stillCurrent())) {
|
|
324
|
+
retry();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
this.result.computed++;
|
|
328
|
+
this.opts.onCompute?.(metricsPath);
|
|
329
|
+
}
|
|
330
|
+
pair.attempted = key;
|
|
331
|
+
pair.force = false;
|
|
332
|
+
this.metrics[pair.key] = record;
|
|
333
|
+
this.indexDirty = true;
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
if (!this.current(pair, epoch) || signatureKey(await this.readInputs(pair).catch(() => null)) !== key) {
|
|
337
|
+
retry();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
pair.attempted = key; // Wait for changed inputs; do not spin on corrupt images.
|
|
341
|
+
delete this.metrics[pair.key];
|
|
342
|
+
this.indexDirty = true;
|
|
343
|
+
this.result.failed.push({
|
|
344
|
+
file: join(this.root, pair.renderer),
|
|
345
|
+
error: error instanceof Error ? error.message : String(error),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async drain() {
|
|
350
|
+
do {
|
|
351
|
+
while (this.pending.size || this.dirty.size || this.active.size) {
|
|
352
|
+
const pending = [...this.pending];
|
|
353
|
+
this.pending.clear();
|
|
354
|
+
let scanned = 0;
|
|
355
|
+
const reportScanning = () => this.opts.onProgress?.({
|
|
356
|
+
phase: 'Scanning changes',
|
|
357
|
+
completed: scanned,
|
|
358
|
+
total: pending.length,
|
|
359
|
+
unit: 'scenes',
|
|
360
|
+
});
|
|
361
|
+
if (pending.length)
|
|
362
|
+
reportScanning();
|
|
363
|
+
for (const rel of pending) {
|
|
27
364
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
365
|
+
if (!rel) {
|
|
366
|
+
this.suite.hasReadme = !!(await signature(join(this.root, 'README.md')));
|
|
367
|
+
this.suite.root.hasReadme = this.suite.hasReadme;
|
|
368
|
+
this.indexDirty = true;
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
await this.reconcile(rel, await scanScene(this.root, rel, this.config));
|
|
372
|
+
// Newly inserted groups and README-only changes need current presence flags too.
|
|
373
|
+
let group = this.suite.root;
|
|
374
|
+
for (const part of rel.split('/')) {
|
|
375
|
+
const path = group.path ? `${group.path}/${part}` : part;
|
|
376
|
+
const child = group.groups.find((g) => g.path === path);
|
|
377
|
+
if (!child)
|
|
378
|
+
break;
|
|
379
|
+
child.hasReadme = !!(await signature(join(this.root, path, 'README.md')));
|
|
380
|
+
group = child;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
35
383
|
}
|
|
36
|
-
catch (
|
|
37
|
-
result.failed.push({
|
|
384
|
+
catch (error) {
|
|
385
|
+
this.result.failed.push({
|
|
386
|
+
file: join(this.root, rel),
|
|
387
|
+
error: error instanceof Error ? error.message : String(error),
|
|
388
|
+
});
|
|
38
389
|
}
|
|
390
|
+
scanned++;
|
|
391
|
+
reportScanning();
|
|
39
392
|
}
|
|
393
|
+
this.pump();
|
|
394
|
+
if (this.active.size)
|
|
395
|
+
await Promise.race(this.active);
|
|
396
|
+
else if (!this.pending.size)
|
|
397
|
+
break; // Remaining dirty nodes belong to an unsettled watcher batch.
|
|
40
398
|
}
|
|
41
|
-
|
|
399
|
+
if (this.indexDirty) {
|
|
400
|
+
this.opts.onProgress?.({ phase: 'Writing index', completed: 0, total: 1, unit: 'files' });
|
|
401
|
+
const index = {
|
|
402
|
+
...this.suite,
|
|
403
|
+
metrics: Object.fromEntries(Object.entries(this.metrics).toSorted(([a], [b]) => a.localeCompare(b))),
|
|
404
|
+
};
|
|
405
|
+
await mkdir(this.root, { recursive: true });
|
|
406
|
+
await atomicWrite(join(this.root, 'index.json'), JSON.stringify(index, null, 2) + '\n');
|
|
407
|
+
this.indexDirty = false;
|
|
408
|
+
this.opts.onProgress?.({ phase: 'Writing index', completed: 1, total: 1, unit: 'files' });
|
|
409
|
+
}
|
|
410
|
+
} while (this.pending.size);
|
|
411
|
+
this.force = false;
|
|
412
|
+
this.progressCompleted = 0;
|
|
413
|
+
this.progressTotal = 0;
|
|
414
|
+
const result = this.result;
|
|
415
|
+
this.result = emptyResult();
|
|
416
|
+
result.failed.sort((a, b) => a.file.localeCompare(b.file));
|
|
417
|
+
return result;
|
|
42
418
|
}
|
|
43
|
-
await writeIndex(root, await scanSuite(root));
|
|
44
|
-
return result;
|
|
45
419
|
}
|
|
46
|
-
/**
|
|
47
|
-
async function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
420
|
+
/** Process discovered scenes through the same scheduler used by watch mode. */
|
|
421
|
+
export async function processSuite(root, opts = {}) {
|
|
422
|
+
return (await SuiteProcessor.create(root, opts)).initialize();
|
|
423
|
+
}
|
|
424
|
+
/** One-shot compatibility helper. Watch mode retains its SuiteProcessor instead. */
|
|
425
|
+
export async function processChanges(root, paths, onProgress) {
|
|
426
|
+
const processor = await SuiteProcessor.create(root, { onProgress });
|
|
427
|
+
try {
|
|
428
|
+
await processor.initialize(false);
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return processSuite(root, { onProgress });
|
|
432
|
+
}
|
|
433
|
+
processor.notify(paths);
|
|
434
|
+
return processor.flush();
|
|
435
|
+
}
|
|
436
|
+
function replaceScene(root, path, scene) {
|
|
437
|
+
const parts = path.split('/');
|
|
438
|
+
const parents = [root];
|
|
439
|
+
let group = root;
|
|
440
|
+
for (const part of parts.slice(0, -1)) {
|
|
441
|
+
const groupPath = group.path ? `${group.path}/${part}` : part;
|
|
442
|
+
let child = group.groups.find((g) => g.path === groupPath);
|
|
443
|
+
if (!child && scene) {
|
|
444
|
+
child = { path: groupPath, hasReadme: false, groups: [], scenes: [] };
|
|
445
|
+
group.groups.push(child);
|
|
446
|
+
group.groups.sort((a, b) => a.path.localeCompare(b.path));
|
|
59
447
|
}
|
|
448
|
+
if (!child)
|
|
449
|
+
return;
|
|
450
|
+
group = child;
|
|
451
|
+
parents.push(group);
|
|
452
|
+
}
|
|
453
|
+
group.scenes = group.scenes.filter((s) => s.path !== path);
|
|
454
|
+
if (scene)
|
|
455
|
+
group.scenes.push(scene);
|
|
456
|
+
group.scenes.sort((a, b) => a.path.localeCompare(b.path));
|
|
457
|
+
for (let i = parents.length - 1; i > 0; i--) {
|
|
458
|
+
const current = parents[i];
|
|
459
|
+
if (current.groups.length || current.scenes.length)
|
|
460
|
+
break;
|
|
461
|
+
parents[i - 1].groups = parents[i - 1].groups.filter((g) => g !== current);
|
|
60
462
|
}
|
|
61
|
-
const file = join(root, 'index.json');
|
|
62
|
-
await mkdir(dirname(file), { recursive: true });
|
|
63
|
-
await writeFile(file, JSON.stringify({ ...suite, metrics }, null, 2) + '\n');
|
|
64
463
|
}
|
|
65
464
|
//# sourceMappingURL=process.js.map
|