parseman 0.36.0 → 0.37.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 +34 -19
- package/dist/analysis/duplication.d.ts +421 -0
- package/dist/analysis/duplication.d.ts.map +1 -0
- package/dist/analysis/gating.d.ts +25 -0
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/analysis/grammar.d.ts +12 -0
- package/dist/analysis/grammar.d.ts.map +1 -0
- package/dist/combinators/parser.d.ts +26 -0
- package/dist/combinators/parser.d.ts.map +1 -1
- package/dist/compiler/build-arity.d.ts +12 -0
- package/dist/compiler/build-arity.d.ts.map +1 -1
- package/dist/compiler/codegen.d.ts +43 -1
- package/dist/compiler/codegen.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts +64 -7
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/cst/host-mode.d.ts +43 -0
- package/dist/cst/host-mode.d.ts.map +1 -0
- package/dist/functional/doc.d.ts.map +1 -1
- package/dist/functional/run.d.ts.map +1 -1
- package/dist/index.cjs +1620 -54
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1620 -54
- package/dist/index.js.map +4 -4
- package/dist/language-service/index.cjs +37 -1
- package/dist/language-service/index.cjs.map +3 -3
- package/dist/language-service/index.js +37 -1
- package/dist/language-service/index.js.map +3 -3
- package/dist/oracle/corpus.d.ts +32 -0
- package/dist/oracle/corpus.d.ts.map +1 -0
- package/dist/oracle/digest.d.ts +21 -0
- package/dist/oracle/digest.d.ts.map +1 -0
- package/dist/oracle/identity.d.ts +117 -0
- package/dist/oracle/identity.d.ts.map +1 -0
- package/dist/oracle/index.cjs +423 -0
- package/dist/oracle/index.cjs.map +7 -0
- package/dist/oracle/index.d.ts +37 -0
- package/dist/oracle/index.d.ts.map +1 -0
- package/dist/oracle/index.js +400 -0
- package/dist/oracle/index.js.map +7 -0
- package/dist/plugin/index.cjs +1627 -61
- package/dist/plugin/index.cjs.map +4 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1627 -61
- package/dist/plugin/index.js.map +4 -4
- package/dist/run/index.cjs +23 -0
- package/dist/run/index.cjs.map +3 -3
- package/dist/run/index.js +23 -0
- package/dist/run/index.js.map +3 -3
- package/dist/spec/index.cjs +2977 -56
- package/dist/spec/index.cjs.map +4 -4
- package/dist/spec/index.js +2977 -56
- package/dist/spec/index.js.map +4 -4
- package/dist/spec/model.d.ts.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +10 -3
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CorpusEntry } from './identity.ts';
|
|
2
|
+
export type LoadCorpusOptions = {
|
|
3
|
+
/** Ids are relative to this directory. Required — see the note above. */
|
|
4
|
+
base: string;
|
|
5
|
+
/** Directories to walk, relative to `base` (or absolute). */
|
|
6
|
+
roots: readonly string[];
|
|
7
|
+
/** File extensions to include, with the dot: `['.less', '.css']`. */
|
|
8
|
+
extensions: readonly string[];
|
|
9
|
+
/**
|
|
10
|
+
* Skip files larger than this. A handful of enormous generated fixtures can
|
|
11
|
+
* dominate the runtime of a gate meant to be run on every edit.
|
|
12
|
+
*/
|
|
13
|
+
maxBytes?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Directories never descended into. An entry with no `/` matches a directory
|
|
16
|
+
* NAME anywhere; an entry containing `/` matches a path suffix, in POSIX form,
|
|
17
|
+
* relative to the root being walked — so `node_modules/.cache` skips the cache
|
|
18
|
+
* without skipping the package tree around it.
|
|
19
|
+
*/
|
|
20
|
+
ignoreDirs?: readonly string[];
|
|
21
|
+
/** Tolerate a root that does not exist, reporting it in `missingRoots`. */
|
|
22
|
+
allowMissingRoots?: boolean;
|
|
23
|
+
};
|
|
24
|
+
export type LoadedCorpus = {
|
|
25
|
+
entries: CorpusEntry[];
|
|
26
|
+
/** Roots that did not resolve. Non-empty only with `allowMissingRoots`. */
|
|
27
|
+
missingRoots: string[];
|
|
28
|
+
/** Files skipped for exceeding `maxBytes`, by id. */
|
|
29
|
+
skippedLarge: string[];
|
|
30
|
+
};
|
|
31
|
+
export declare function loadCorpus(options: LoadCorpusOptions): LoadedCorpus;
|
|
32
|
+
//# sourceMappingURL=corpus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"corpus.d.ts","sourceRoot":"","sources":["../../src/oracle/corpus.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAEhD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAA;IACZ,6DAA6D;IAC7D,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACxB,qEAAqE;IACrE,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;IAC7B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,2EAA2E;IAC3E,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,WAAW,EAAE,CAAA;IACtB,2EAA2E;IAC3E,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,qDAAqD;IACrD,YAAY,EAAE,MAAM,EAAE,CAAA;CACvB,CAAA;AAID,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,YAAY,CAgHnE"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bump when the token grammar below changes in a way that alters output for any
|
|
3
|
+
* input. It is a coarse, human-readable companion to the behavioural harness
|
|
4
|
+
* digest — the harness digest is what actually enforces this, but a reader
|
|
5
|
+
* comparing two reports should not have to diff hex to see that the format moved.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DIGEST_FORMAT = 1;
|
|
8
|
+
/**
|
|
9
|
+
* The canonical token string for a value. Exported because a moved digest is
|
|
10
|
+
* only actionable if you can see WHAT moved: diff two canonical strings and the
|
|
11
|
+
* answer is a line, not a hex mismatch.
|
|
12
|
+
*/
|
|
13
|
+
export declare function canonicalize(value: unknown): string;
|
|
14
|
+
/** Full 64-hex sha256 of a string. */
|
|
15
|
+
export declare function hash(payload: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Per-entry fingerprint width. 64 bits over any realistic corpus is far below
|
|
18
|
+
* the collision floor, and a report a human has to read stays readable.
|
|
19
|
+
*/
|
|
20
|
+
export declare const FINGERPRINT_HEX = 16;
|
|
21
|
+
//# sourceMappingURL=digest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"digest.d.ts","sourceRoot":"","sources":["../../src/oracle/digest.ts"],"names":[],"mappings":"AAsDA;;;;;GAKG;AACH,eAAO,MAAM,aAAa,IAAI,CAAA;AAyG9B;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAInD;AAED,sCAAsC;AACtC,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,KAAK,CAAA"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One parse entry point under test.
|
|
3
|
+
*
|
|
4
|
+
* Declare EVERY shipped surface, not just the one you are editing. A refactor
|
|
5
|
+
* that touches one grammar should move neither, and the untouched surface is a
|
|
6
|
+
* free control: if it moves too, the harness or the corpus moved, not the rule
|
|
7
|
+
* you edited.
|
|
8
|
+
*/
|
|
9
|
+
export type Surface = {
|
|
10
|
+
/** Stable identifier. Renaming a surface changes its aggregate, deliberately. */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Called once per corpus entry. May return anything, and may throw. */
|
|
13
|
+
parse: (source: string, id: string) => unknown;
|
|
14
|
+
};
|
|
15
|
+
export type CorpusEntry = {
|
|
16
|
+
/**
|
|
17
|
+
* Stable, RELATIVE identifier — a path relative to a base, not an absolute
|
|
18
|
+
* one. An absolute path makes the digest depend on the checkout directory,
|
|
19
|
+
* which means two machines can never agree.
|
|
20
|
+
*/
|
|
21
|
+
id: string;
|
|
22
|
+
source: string;
|
|
23
|
+
};
|
|
24
|
+
export type SurfaceReport = {
|
|
25
|
+
name: string;
|
|
26
|
+
/** sha256 over every entry's fingerprint, in id order. The gate. */
|
|
27
|
+
aggregate: string;
|
|
28
|
+
/** How many entries threw. Expected to be non-zero; expected not to MOVE. */
|
|
29
|
+
threw: number;
|
|
30
|
+
};
|
|
31
|
+
export type IdentityReport = {
|
|
32
|
+
format: number;
|
|
33
|
+
/** Behavioural fingerprint of the harness that produced this report. */
|
|
34
|
+
harness: string;
|
|
35
|
+
entries: number;
|
|
36
|
+
surfaces: SurfaceReport[];
|
|
37
|
+
/** entry id → surface name → 16-hex fingerprint. Diff this to localise a move. */
|
|
38
|
+
perEntry: Record<string, Record<string, string>>;
|
|
39
|
+
};
|
|
40
|
+
export type DigestOptions = {
|
|
41
|
+
/**
|
|
42
|
+
* Project a thrown value into something hashable. The default keeps an Error's
|
|
43
|
+
* `name` and `message`.
|
|
44
|
+
*
|
|
45
|
+
* Override when your messages embed anything machine-specific — an absolute
|
|
46
|
+
* path, a wall-clock time, a memory address. Those make a digest that cannot
|
|
47
|
+
* be compared across machines, and the symptom is a gate that fails for
|
|
48
|
+
* everyone except its author.
|
|
49
|
+
*/
|
|
50
|
+
projectError?: (thrown: unknown, id: string, surface: string) => unknown;
|
|
51
|
+
/**
|
|
52
|
+
* How many entries to re-parse to prove the grammar is deterministic. Sampled
|
|
53
|
+
* evenly across the corpus, not from the front. `0` disables the check; do that
|
|
54
|
+
* only if you have another reason to trust it.
|
|
55
|
+
*/
|
|
56
|
+
determinismSample?: number;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Run every surface over every entry and produce the report.
|
|
60
|
+
*
|
|
61
|
+
* The corpus is sorted by id, so the aggregate does not depend on the order the
|
|
62
|
+
* caller happened to list files in. The aggregate covers the ids as well as the
|
|
63
|
+
* fingerprints, so a corpus that quietly SHRANK — a root that stopped resolving,
|
|
64
|
+
* a glob that stopped matching — moves the aggregate instead of producing a
|
|
65
|
+
* smaller, greener gate. {@link compareReports} then tells you it was the corpus.
|
|
66
|
+
*/
|
|
67
|
+
export declare function digestCorpus(surfaces: readonly Surface[], corpus: readonly CorpusEntry[], options?: DigestOptions): IdentityReport;
|
|
68
|
+
export type SurfaceComparison = {
|
|
69
|
+
name: string;
|
|
70
|
+
before: string | null;
|
|
71
|
+
after: string | null;
|
|
72
|
+
equal: boolean;
|
|
73
|
+
/** Entry ids whose fingerprint moved on this surface. Empty when equal. */
|
|
74
|
+
moved: string[];
|
|
75
|
+
beforeThrew: number | null;
|
|
76
|
+
afterThrew: number | null;
|
|
77
|
+
};
|
|
78
|
+
export type IdentityComparison = {
|
|
79
|
+
/**
|
|
80
|
+
* `identical` — output-neutral, the refactor is accepted.
|
|
81
|
+
* `moved` — the output changed; this is not a refactor.
|
|
82
|
+
* `incomparable` — the two reports cannot be compared at all. Never a pass.
|
|
83
|
+
*/
|
|
84
|
+
verdict: 'identical' | 'moved' | 'incomparable';
|
|
85
|
+
reason: string | null;
|
|
86
|
+
addedEntries: string[];
|
|
87
|
+
removedEntries: string[];
|
|
88
|
+
surfaces: SurfaceComparison[];
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Compare two reports.
|
|
92
|
+
*
|
|
93
|
+
* Refuses — `incomparable`, with a reason — when the two were not produced by the
|
|
94
|
+
* same harness over comparable inputs. That is the whole point: the failure mode
|
|
95
|
+
* this guards against is a harness change that quietly re-baselines every recorded
|
|
96
|
+
* digest, and the only defence is that mismatched provenance can never come out
|
|
97
|
+
* as a verdict about the grammar.
|
|
98
|
+
*
|
|
99
|
+
* A corpus that gained or lost entries is reported but is NOT by itself
|
|
100
|
+
* incomparable — the surviving entries still carry a real signal, and telling you
|
|
101
|
+
* "1 entry disappeared, the other 4,300 are unchanged" is more useful than
|
|
102
|
+
* refusing. The verdict still reflects the difference.
|
|
103
|
+
*/
|
|
104
|
+
export declare function compareReports(before: IdentityReport, after: IdentityReport): IdentityComparison;
|
|
105
|
+
/** Human-readable rendering of a comparison, for a CLI or a CI log. */
|
|
106
|
+
export declare function formatComparison(c: IdentityComparison, options?: {
|
|
107
|
+
maxMoved?: number;
|
|
108
|
+
}): string;
|
|
109
|
+
/**
|
|
110
|
+
* Behavioural fingerprint of this harness, embedded in every report it produces.
|
|
111
|
+
*
|
|
112
|
+
* Parseman's suite pins this to a literal. If you change the projection, that
|
|
113
|
+
* test fails and the constant has to be updated in the same diff — which is the
|
|
114
|
+
* point. There is no quiet edit here.
|
|
115
|
+
*/
|
|
116
|
+
export declare const HARNESS_DIGEST: string;
|
|
117
|
+
//# sourceMappingURL=identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../../src/oracle/identity.ts"],"names":[],"mappings":"AA0DA;;;;;;;GAOG;AACH,MAAM,MAAM,OAAO,GAAG;IACpB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAA;IACZ,wEAAwE;IACxE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAA;CAC/C,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB;;;;OAIG;IACH,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,SAAS,EAAE,MAAM,CAAA;IACjB,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,aAAa,EAAE,CAAA;IACzB,kFAAkF;IAClF,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACjD,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAA;IACxE;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,CAAA;AAyBD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,SAAS,OAAO,EAAE,EAC5B,MAAM,EAAE,SAAS,WAAW,EAAE,EAC9B,OAAO,GAAE,aAAkB,GAC1B,cAAc,CAmDhB;AAyBD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,KAAK,EAAE,OAAO,CAAA;IACd,2EAA2E;IAC3E,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;OAIG;IACH,OAAO,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAA;IAC/C,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,EAAE,iBAAiB,EAAE,CAAA;CAC9B,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,cAAc,GAAG,kBAAkB,CA0ChG;AAMD,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,kBAAkB,EAAE,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,MAAM,CA0BnG;AA+ED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,EAAE,MAKzB,CAAA"}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/oracle/index.ts
|
|
21
|
+
var oracle_exports = {};
|
|
22
|
+
__export(oracle_exports, {
|
|
23
|
+
DIGEST_FORMAT: () => DIGEST_FORMAT,
|
|
24
|
+
HARNESS_DIGEST: () => HARNESS_DIGEST,
|
|
25
|
+
canonicalize: () => canonicalize,
|
|
26
|
+
compareReports: () => compareReports,
|
|
27
|
+
digestCorpus: () => digestCorpus,
|
|
28
|
+
formatComparison: () => formatComparison,
|
|
29
|
+
loadCorpus: () => loadCorpus
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(oracle_exports);
|
|
32
|
+
|
|
33
|
+
// src/oracle/digest.ts
|
|
34
|
+
var import_node_crypto = require("node:crypto");
|
|
35
|
+
var DIGEST_FORMAT = 1;
|
|
36
|
+
var str = (s) => JSON.stringify(s);
|
|
37
|
+
var tagOf = (v) => {
|
|
38
|
+
const ctor = v.constructor;
|
|
39
|
+
return typeof ctor === "function" && typeof ctor.name === "string" ? ctor.name : "";
|
|
40
|
+
};
|
|
41
|
+
function writeScalar(v, out) {
|
|
42
|
+
switch (typeof v) {
|
|
43
|
+
case "undefined":
|
|
44
|
+
out.push("u");
|
|
45
|
+
return;
|
|
46
|
+
case "boolean":
|
|
47
|
+
out.push(v ? "T" : "F");
|
|
48
|
+
return;
|
|
49
|
+
case "bigint":
|
|
50
|
+
out.push(`n${v}`);
|
|
51
|
+
return;
|
|
52
|
+
case "string":
|
|
53
|
+
out.push(`s${str(v)}`);
|
|
54
|
+
return;
|
|
55
|
+
// TODO(oracle-callable-identity): a callable is projected by NAME and a symbol by
|
|
56
|
+
// DESCRIPTION, so two distinct same-named functions — or two same-description
|
|
57
|
+
// symbols — digest identically, and a change between them reads as "identical".
|
|
58
|
+
// This is a real hole in a tool whose whole claim is paranoia, and it is left open
|
|
59
|
+
// deliberately: closing it means either hashing `Function.prototype.toString()`,
|
|
60
|
+
// which flips the digest on a comment edit inside a builder, or REFUSING to digest
|
|
61
|
+
// any value carrying a callable, which rejects legitimate ASTs that park a callback
|
|
62
|
+
// on a node. Both are worse than the hole for some caller, and picking between them
|
|
63
|
+
// is a product decision, not a bug fix. Documented under "What it is not" in
|
|
64
|
+
// `docs/guide/identity-oracle.md` so no reader infers a guarantee that is not here.
|
|
65
|
+
case "symbol":
|
|
66
|
+
out.push(`y${str(v.description ?? "")}`);
|
|
67
|
+
return;
|
|
68
|
+
case "function":
|
|
69
|
+
out.push(`f${str(v.name)}`);
|
|
70
|
+
return;
|
|
71
|
+
case "number":
|
|
72
|
+
if (Number.isNaN(v)) out.push("#nan");
|
|
73
|
+
else if (v === Number.POSITIVE_INFINITY) out.push("#inf");
|
|
74
|
+
else if (v === Number.NEGATIVE_INFINITY) out.push("#-inf");
|
|
75
|
+
else if (Object.is(v, -0)) out.push("#-0");
|
|
76
|
+
else out.push(`#${v}`);
|
|
77
|
+
return;
|
|
78
|
+
default:
|
|
79
|
+
out.push("z");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function write(v, path, depth, out) {
|
|
83
|
+
if (typeof v !== "object" || v === null) {
|
|
84
|
+
writeScalar(v, out);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const seenAt = path.get(v);
|
|
88
|
+
if (seenAt !== void 0) {
|
|
89
|
+
out.push(`^${depth - seenAt}`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
path.set(v, depth);
|
|
93
|
+
if (Array.isArray(v)) {
|
|
94
|
+
out.push("[");
|
|
95
|
+
for (const item of v) write(item, path, depth + 1, out);
|
|
96
|
+
out.push("]");
|
|
97
|
+
} else if (v instanceof Map) {
|
|
98
|
+
out.push("M");
|
|
99
|
+
for (const [k, val] of v) {
|
|
100
|
+
write(k, path, depth + 1, out);
|
|
101
|
+
write(val, path, depth + 1, out);
|
|
102
|
+
}
|
|
103
|
+
out.push("}");
|
|
104
|
+
} else if (v instanceof Set) {
|
|
105
|
+
out.push("S");
|
|
106
|
+
for (const item of v) write(item, path, depth + 1, out);
|
|
107
|
+
out.push("}");
|
|
108
|
+
} else if (v instanceof Date) {
|
|
109
|
+
out.push(`D${v.getTime()}`);
|
|
110
|
+
} else if (v instanceof RegExp) {
|
|
111
|
+
out.push(`R${str(v.source)}${str(v.flags)}`);
|
|
112
|
+
} else {
|
|
113
|
+
const tag = tagOf(v);
|
|
114
|
+
out.push(`{${str(tag === "Object" ? "" : tag)}`);
|
|
115
|
+
for (const k of Object.keys(v).sort()) {
|
|
116
|
+
out.push(`k${str(k)}`);
|
|
117
|
+
write(v[k], path, depth + 1, out);
|
|
118
|
+
}
|
|
119
|
+
out.push("}");
|
|
120
|
+
}
|
|
121
|
+
path.delete(v);
|
|
122
|
+
}
|
|
123
|
+
function canonicalize(value) {
|
|
124
|
+
const out = [];
|
|
125
|
+
write(value, /* @__PURE__ */ new Map(), 0, out);
|
|
126
|
+
return out.join("\0");
|
|
127
|
+
}
|
|
128
|
+
function hash(payload2) {
|
|
129
|
+
return (0, import_node_crypto.createHash)("sha256").update(payload2).digest("hex");
|
|
130
|
+
}
|
|
131
|
+
var FINGERPRINT_HEX = 16;
|
|
132
|
+
|
|
133
|
+
// src/oracle/identity.ts
|
|
134
|
+
var defaultProjectError = (thrown) => thrown instanceof Error ? { name: thrown.name, message: thrown.message } : { thrown };
|
|
135
|
+
function payload(surface, entry, options) {
|
|
136
|
+
try {
|
|
137
|
+
return { text: `OK:${canonicalize(surface.parse(entry.source, entry.id))}`, threw: false };
|
|
138
|
+
} catch (thrown) {
|
|
139
|
+
const project = options.projectError ?? defaultProjectError;
|
|
140
|
+
return { text: `ERR:${canonicalize(project(thrown, entry.id, surface.name))}`, threw: true };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
var fingerprint = (text) => hash(text).slice(0, FINGERPRINT_HEX);
|
|
144
|
+
function digestCorpus(surfaces, corpus, options = {}) {
|
|
145
|
+
if (surfaces.length === 0) throw new Error("digestCorpus: no surfaces \u2014 there is nothing to compare.");
|
|
146
|
+
const names = /* @__PURE__ */ new Set();
|
|
147
|
+
for (const s of surfaces) {
|
|
148
|
+
if (names.has(s.name)) throw new Error(`digestCorpus: duplicate surface name ${JSON.stringify(s.name)}.`);
|
|
149
|
+
names.add(s.name);
|
|
150
|
+
}
|
|
151
|
+
const entries = [...corpus].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
152
|
+
for (let n = 1; n < entries.length; n++) {
|
|
153
|
+
if (entries[n].id === entries[n - 1].id) {
|
|
154
|
+
throw new Error(`digestCorpus: duplicate corpus id ${JSON.stringify(entries[n].id)}.`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const perEntry = {};
|
|
158
|
+
const threw = {};
|
|
159
|
+
const texts = [];
|
|
160
|
+
for (const s of surfaces) threw[s.name] = 0;
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
const row = {};
|
|
163
|
+
const rowTexts = /* @__PURE__ */ new Map();
|
|
164
|
+
for (const s of surfaces) {
|
|
165
|
+
const result = payload(s, entry, options);
|
|
166
|
+
if (result.threw) threw[s.name]++;
|
|
167
|
+
row[s.name] = fingerprint(result.text);
|
|
168
|
+
rowTexts.set(s.name, result.text);
|
|
169
|
+
}
|
|
170
|
+
perEntry[entry.id] = row;
|
|
171
|
+
texts.push(rowTexts);
|
|
172
|
+
}
|
|
173
|
+
verifyDeterminism(surfaces, entries, texts, options);
|
|
174
|
+
return {
|
|
175
|
+
format: DIGEST_FORMAT,
|
|
176
|
+
harness: HARNESS_DIGEST,
|
|
177
|
+
entries: entries.length,
|
|
178
|
+
surfaces: surfaces.map((s) => ({
|
|
179
|
+
name: s.name,
|
|
180
|
+
// The surface NAME is inside its own aggregate: two surfaces that happen
|
|
181
|
+
// to agree on every entry must still have distinct aggregates, or a
|
|
182
|
+
// report that swapped them would compare equal.
|
|
183
|
+
aggregate: hash(
|
|
184
|
+
[`surface:${s.name}`, ...entries.map((e) => `${e.id}:${perEntry[e.id][s.name]}`)].join("\n")
|
|
185
|
+
),
|
|
186
|
+
threw: threw[s.name]
|
|
187
|
+
})),
|
|
188
|
+
perEntry
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function verifyDeterminism(surfaces, entries, texts, options) {
|
|
192
|
+
const want = options.determinismSample ?? 32;
|
|
193
|
+
if (want <= 0 || entries.length === 0) return;
|
|
194
|
+
const stride = Math.max(1, Math.floor(entries.length / Math.min(want, entries.length)));
|
|
195
|
+
for (let n = 0; n < entries.length; n += stride) {
|
|
196
|
+
const entry = entries[n];
|
|
197
|
+
for (const s of surfaces) {
|
|
198
|
+
if (payload(s, entry, options).text === texts[n].get(s.name)) continue;
|
|
199
|
+
throw new Error(
|
|
200
|
+
`digestCorpus: surface ${JSON.stringify(s.name)} is NOT DETERMINISTIC on ${JSON.stringify(entry.id)} \u2014 two parses of the same source produced different output. Every digest from this grammar would move on its own, so a comparison against it cannot mean anything. Usual causes: a timestamp or counter in the output, iteration over a keyed-by-object Map, or a node carrying a reference to mutable shared state.`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function compareReports(before, after) {
|
|
206
|
+
if (before.harness !== after.harness) {
|
|
207
|
+
return incomparable(
|
|
208
|
+
`harness drift: reports were produced by DIFFERENT versions of the identity harness (${before.harness.slice(0, 12)}\u2026 vs ${after.harness.slice(0, 12)}\u2026). Their digests are not comparable, and treating them as such would either invent a regression or hide one. Re-run BOTH sides on one harness.`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (before.format !== after.format) {
|
|
212
|
+
return incomparable(`digest format ${before.format} vs ${after.format} \u2014 re-run both sides on one version.`);
|
|
213
|
+
}
|
|
214
|
+
const beforeIds = new Set(Object.keys(before.perEntry));
|
|
215
|
+
const afterIds = new Set(Object.keys(after.perEntry));
|
|
216
|
+
const addedEntries = [...afterIds].filter((id) => !beforeIds.has(id)).sort();
|
|
217
|
+
const removedEntries = [...beforeIds].filter((id) => !afterIds.has(id)).sort();
|
|
218
|
+
const shared = [...beforeIds].filter((id) => afterIds.has(id)).sort();
|
|
219
|
+
const byName = /* @__PURE__ */ new Map();
|
|
220
|
+
for (const s of before.surfaces) byName.set(s.name, { ...byName.get(s.name), before: s });
|
|
221
|
+
for (const s of after.surfaces) byName.set(s.name, { ...byName.get(s.name), after: s });
|
|
222
|
+
const surfaces = [...byName.entries()].sort(([a], [b]) => a < b ? -1 : 1).map(([name, pair]) => {
|
|
223
|
+
const moved = pair.before && pair.after ? shared.filter((id) => before.perEntry[id][name] !== after.perEntry[id][name]) : [];
|
|
224
|
+
return {
|
|
225
|
+
name,
|
|
226
|
+
before: pair.before?.aggregate ?? null,
|
|
227
|
+
after: pair.after?.aggregate ?? null,
|
|
228
|
+
equal: pair.before !== void 0 && pair.after !== void 0 && pair.before.aggregate === pair.after.aggregate,
|
|
229
|
+
moved,
|
|
230
|
+
beforeThrew: pair.before?.threw ?? null,
|
|
231
|
+
afterThrew: pair.after?.threw ?? null
|
|
232
|
+
};
|
|
233
|
+
});
|
|
234
|
+
const identical = addedEntries.length === 0 && removedEntries.length === 0 && surfaces.every((s) => s.equal);
|
|
235
|
+
return { verdict: identical ? "identical" : "moved", reason: null, addedEntries, removedEntries, surfaces };
|
|
236
|
+
}
|
|
237
|
+
function incomparable(reason) {
|
|
238
|
+
return { verdict: "incomparable", reason, addedEntries: [], removedEntries: [], surfaces: [] };
|
|
239
|
+
}
|
|
240
|
+
function formatComparison(c, options = {}) {
|
|
241
|
+
if (c.verdict === "incomparable") return `INCOMPARABLE \u2014 ${c.reason ?? "unknown reason"}`;
|
|
242
|
+
const max = options.maxMoved ?? 10;
|
|
243
|
+
const lines = [];
|
|
244
|
+
for (const s of c.surfaces) {
|
|
245
|
+
if (s.before === null) lines.push(` + surface ${s.name} (added)`);
|
|
246
|
+
else if (s.after === null) lines.push(` - surface ${s.name} (removed)`);
|
|
247
|
+
else if (s.equal) lines.push(` = ${s.name} ${s.before.slice(0, 16)}\u2026 threw=${s.afterThrew}`);
|
|
248
|
+
else {
|
|
249
|
+
lines.push(
|
|
250
|
+
` ! ${s.name} ${s.before.slice(0, 16)}\u2026 -> ${s.after.slice(0, 16)}\u2026 threw ${s.beforeThrew} -> ${s.afterThrew} (${s.moved.length} entries moved)`
|
|
251
|
+
);
|
|
252
|
+
for (const id of s.moved.slice(0, max)) lines.push(` ${id}`);
|
|
253
|
+
if (s.moved.length > max) lines.push(` \u2026 and ${s.moved.length - max} more`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (c.addedEntries.length > 0) lines.push(` corpus GAINED ${c.addedEntries.length} entries`);
|
|
257
|
+
if (c.removedEntries.length > 0) {
|
|
258
|
+
lines.push(
|
|
259
|
+
` corpus LOST ${c.removedEntries.length} entries \u2014 a differential over a SMALLER corpus is a weaker gate, not a passing one`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
return [c.verdict === "identical" ? "IDENTICAL \u2014 output-neutral" : "MOVED \u2014 this is not a refactor", ...lines].join("\n");
|
|
263
|
+
}
|
|
264
|
+
function canaryReport() {
|
|
265
|
+
class Tagged {
|
|
266
|
+
x;
|
|
267
|
+
constructor(x) {
|
|
268
|
+
this.x = x;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const shared = { shared: true };
|
|
272
|
+
const cyclic = { name: "root" };
|
|
273
|
+
cyclic.self = cyclic;
|
|
274
|
+
cyclic.child = { up: cyclic };
|
|
275
|
+
const values = {
|
|
276
|
+
"a/scalars": [0, -0, 1.5, Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, 10n, true, false, null],
|
|
277
|
+
"b/absent": [{ a: void 0 }, {}, { a: null }, [void 0], []],
|
|
278
|
+
"c/key-order": [{ a: 1, b: 2 }, { b: 2, a: 1 }],
|
|
279
|
+
"d/collections": [/* @__PURE__ */ new Map([["k", 1], ["j", 2]]), /* @__PURE__ */ new Set([1, 2]), /* @__PURE__ */ new Date(0), /ab+c/giu],
|
|
280
|
+
"e/tagged": [new Tagged(1), { x: 1 }],
|
|
281
|
+
"f/sharing": { left: shared, right: shared },
|
|
282
|
+
"g/cycle": cyclic,
|
|
283
|
+
"h/text": ["", "a\0b", "a b", '"quoted"', "\\"],
|
|
284
|
+
"i/callable": [function named() {
|
|
285
|
+
}, /* @__PURE__ */ Symbol("sym")]
|
|
286
|
+
};
|
|
287
|
+
const surfaces = [
|
|
288
|
+
{ name: "value", parse: (_source, id) => values[id] },
|
|
289
|
+
{
|
|
290
|
+
name: "thrower",
|
|
291
|
+
parse: (_source, id) => {
|
|
292
|
+
if (id === "a/scalars") throw new TypeError("canary");
|
|
293
|
+
if (id === "b/absent") throw "a bare string";
|
|
294
|
+
return { id };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
];
|
|
298
|
+
const corpus = Object.keys(values).map((id) => ({ id, source: id }));
|
|
299
|
+
const perEntry = {};
|
|
300
|
+
const threw = { value: 0, thrower: 0 };
|
|
301
|
+
for (const entry of [...corpus].sort((a, b) => a.id < b.id ? -1 : 1)) {
|
|
302
|
+
const row = {};
|
|
303
|
+
for (const s of surfaces) {
|
|
304
|
+
const result = payload(s, entry, {});
|
|
305
|
+
if (result.threw) threw[s.name]++;
|
|
306
|
+
row[s.name] = fingerprint(result.text);
|
|
307
|
+
}
|
|
308
|
+
perEntry[entry.id] = row;
|
|
309
|
+
}
|
|
310
|
+
const ids = Object.keys(perEntry).sort();
|
|
311
|
+
return {
|
|
312
|
+
format: DIGEST_FORMAT,
|
|
313
|
+
harness: "",
|
|
314
|
+
entries: ids.length,
|
|
315
|
+
surfaces: surfaces.map((s) => ({
|
|
316
|
+
name: s.name,
|
|
317
|
+
aggregate: hash([`surface:${s.name}`, ...ids.map((id) => `${id}:${perEntry[id][s.name]}`)].join("\n")),
|
|
318
|
+
threw: threw[s.name]
|
|
319
|
+
})),
|
|
320
|
+
perEntry
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
var HARNESS_DIGEST = (() => {
|
|
324
|
+
const canary = canaryReport();
|
|
325
|
+
return hash(
|
|
326
|
+
[`format:${DIGEST_FORMAT}`, ...canary.surfaces.map((s) => `${s.name}:${s.aggregate}:${s.threw}`)].join("\n")
|
|
327
|
+
);
|
|
328
|
+
})();
|
|
329
|
+
|
|
330
|
+
// src/oracle/corpus.ts
|
|
331
|
+
var import_node_fs = require("node:fs");
|
|
332
|
+
var import_node_path = require("node:path");
|
|
333
|
+
var DEFAULT_IGNORE = [".git", "node_modules/.cache"];
|
|
334
|
+
function loadCorpus(options) {
|
|
335
|
+
const base = (0, import_node_path.resolve)(options.base);
|
|
336
|
+
const ignoreDirs = options.ignoreDirs ?? DEFAULT_IGNORE;
|
|
337
|
+
const ignoreNames = new Set(ignoreDirs.filter((d) => !d.includes("/")));
|
|
338
|
+
const ignorePaths = ignoreDirs.filter((d) => d.includes("/")).map((d) => d.replace(/^\.?\/+/, "").replace(/\/+$/, ""));
|
|
339
|
+
const maxBytes = options.maxBytes ?? Number.POSITIVE_INFINITY;
|
|
340
|
+
const exts = new Set(options.extensions.map((e) => e.toLowerCase()));
|
|
341
|
+
const missingRoots = [];
|
|
342
|
+
const skippedLarge = [];
|
|
343
|
+
const files = /* @__PURE__ */ new Map();
|
|
344
|
+
const isIgnored = (name, relFromRoot) => {
|
|
345
|
+
if (ignoreNames.has(name)) return true;
|
|
346
|
+
return ignorePaths.some((p) => relFromRoot === p || relFromRoot.endsWith(`/${p}`));
|
|
347
|
+
};
|
|
348
|
+
const walk = (dir, root, visited) => {
|
|
349
|
+
let real;
|
|
350
|
+
try {
|
|
351
|
+
real = (0, import_node_fs.realpathSync)(dir);
|
|
352
|
+
} catch {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (visited.has(real)) return;
|
|
356
|
+
visited.add(real);
|
|
357
|
+
for (const item of (0, import_node_fs.readdirSync)(dir, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
|
|
358
|
+
const full = (0, import_node_path.join)(dir, item.name);
|
|
359
|
+
if (isIgnored(item.name, (0, import_node_path.relative)(root, full).split(import_node_path.sep).join("/"))) continue;
|
|
360
|
+
let isDir = item.isDirectory();
|
|
361
|
+
let isFile = item.isFile();
|
|
362
|
+
let size = 0;
|
|
363
|
+
if (item.isSymbolicLink() || !isDir && !isFile) {
|
|
364
|
+
try {
|
|
365
|
+
const st = (0, import_node_fs.statSync)(full);
|
|
366
|
+
isDir = st.isDirectory();
|
|
367
|
+
isFile = st.isFile();
|
|
368
|
+
size = st.size;
|
|
369
|
+
} catch {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (isDir) {
|
|
374
|
+
walk(full, root, visited);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (!isFile) continue;
|
|
378
|
+
const dot = item.name.lastIndexOf(".");
|
|
379
|
+
if (dot < 0 || !exts.has(item.name.slice(dot).toLowerCase())) continue;
|
|
380
|
+
if (size === 0) {
|
|
381
|
+
try {
|
|
382
|
+
size = (0, import_node_fs.statSync)(full).size;
|
|
383
|
+
} catch {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (size > maxBytes) {
|
|
388
|
+
skippedLarge.push(idOf(base, full));
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
let realFile;
|
|
392
|
+
try {
|
|
393
|
+
realFile = (0, import_node_fs.realpathSync)(full);
|
|
394
|
+
} catch {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const id = idOf(base, full);
|
|
398
|
+
const existing = files.get(realFile);
|
|
399
|
+
if (existing === void 0 || id < existing) files.set(realFile, id);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
for (const root of options.roots) {
|
|
403
|
+
const full = (0, import_node_path.resolve)(base, root);
|
|
404
|
+
try {
|
|
405
|
+
if (!(0, import_node_fs.statSync)(full).isDirectory()) throw new Error("not a directory");
|
|
406
|
+
} catch {
|
|
407
|
+
if (!options.allowMissingRoots) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`loadCorpus: root ${JSON.stringify(root)} does not resolve to a directory (${full}). A corpus that quietly shrank produces a different aggregate that looks like a grammar change. Fix the root, or pass allowMissingRoots and record \`missingRoots\` alongside the digest.`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
missingRoots.push(root);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
walk(full, full, /* @__PURE__ */ new Set());
|
|
416
|
+
}
|
|
417
|
+
const entries = [...files.values()].sort().map((id) => ({ id, source: (0, import_node_fs.readFileSync)((0, import_node_path.resolve)(base, id), "utf8") }));
|
|
418
|
+
return { entries, missingRoots, skippedLarge: skippedLarge.sort() };
|
|
419
|
+
}
|
|
420
|
+
function idOf(base, full) {
|
|
421
|
+
return (0, import_node_path.relative)(base, full).split(import_node_path.sep).join("/");
|
|
422
|
+
}
|
|
423
|
+
//# sourceMappingURL=index.cjs.map
|