parseman 0.43.0 → 0.45.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/dist/analysis/diagnose.d.ts +118 -0
- package/dist/analysis/diagnose.d.ts.map +1 -0
- package/dist/analysis/gating.d.ts +9 -9
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/analysis/grammar.d.ts.map +1 -1
- package/dist/combinators/attempt.d.ts.map +1 -1
- package/dist/combinators/choice.d.ts.map +1 -1
- package/dist/combinators/dispatch.d.ts +19 -1
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/grammar.d.ts +7 -0
- package/dist/combinators/grammar.d.ts.map +1 -1
- package/dist/combinators/map.d.ts +11 -0
- package/dist/combinators/map.d.ts.map +1 -1
- package/dist/combinators/node.d.ts +19 -0
- package/dist/combinators/node.d.ts.map +1 -1
- package/dist/combinators/token.d.ts +3 -2
- package/dist/combinators/token.d.ts.map +1 -1
- package/dist/combinators/trivia-skip.d.ts +1 -0
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/compiler/build-arity.d.ts +28 -0
- package/dist/compiler/build-arity.d.ts.map +1 -1
- package/dist/compiler/codegen.d.ts +80 -60
- package/dist/compiler/codegen.d.ts.map +1 -1
- package/dist/compiler/degradation.d.ts +123 -0
- package/dist/compiler/degradation.d.ts.map +1 -0
- package/dist/compiler/fields.d.ts.map +1 -1
- package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
- package/dist/compiler/inline-build.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts +1 -1
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/compiler/scannable-run.d.ts +1 -1
- package/dist/compiler/scannable-run.d.ts.map +1 -1
- package/dist/compiler/trivia-fast-path.d.ts +4 -3
- package/dist/compiler/trivia-fast-path.d.ts.map +1 -1
- package/dist/coverage.d.ts +13 -0
- package/dist/coverage.d.ts.map +1 -1
- package/dist/cst/reflection.d.ts.map +1 -1
- package/dist/cst/trivia-entries.d.ts +15 -9
- package/dist/cst/trivia-entries.d.ts.map +1 -1
- package/dist/cst/trivia-kinds.d.ts +15 -11
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/functional/run.d.ts +22 -16
- package/dist/functional/run.d.ts.map +1 -1
- package/dist/index.cjs +2131 -1594
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +6 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2131 -1594
- package/dist/index.js.map +4 -4
- package/dist/language-service/index.cjs +201 -132
- package/dist/language-service/index.cjs.map +4 -4
- package/dist/language-service/index.js +201 -132
- package/dist/language-service/index.js.map +4 -4
- package/dist/oracle/digest.d.ts +132 -6
- package/dist/oracle/digest.d.ts.map +1 -1
- package/dist/oracle/index.cjs +136 -305
- package/dist/oracle/index.cjs.map +4 -4
- package/dist/oracle/index.d.ts +43 -31
- package/dist/oracle/index.d.ts.map +1 -1
- package/dist/oracle/index.js +136 -305
- package/dist/oracle/index.js.map +4 -4
- package/dist/plugin/evaluator.d.ts +3 -0
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.cjs +2333 -1721
- package/dist/plugin/index.cjs.map +4 -4
- package/dist/plugin/index.d.ts +18 -0
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +2333 -1721
- package/dist/plugin/index.js.map +4 -4
- package/dist/plugin/reducer-resolver.d.ts +41 -0
- package/dist/plugin/reducer-resolver.d.ts.map +1 -0
- package/dist/run/index.cjs +185 -132
- package/dist/run/index.cjs.map +3 -3
- package/dist/run/index.d.ts +1 -1
- package/dist/run/index.d.ts.map +1 -1
- package/dist/run/index.js +185 -132
- package/dist/run/index.js.map +3 -3
- package/dist/spec/index.cjs +1637 -1322
- package/dist/spec/index.cjs.map +4 -4
- package/dist/spec/index.js +1637 -1322
- package/dist/spec/index.js.map +4 -4
- package/dist/spec/model.d.ts.map +1 -1
- package/dist/types.d.ts +31 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +7 -3
- package/dist/combinators/guard.d.ts +0 -8
- package/dist/combinators/guard.d.ts.map +0 -1
- package/dist/cst/offset-model.d.ts +0 -119
- package/dist/cst/offset-model.d.ts.map +0 -1
- package/dist/oracle/corpus.d.ts +0 -32
- package/dist/oracle/corpus.d.ts.map +0 -1
- package/dist/oracle/identity.d.ts +0 -117
- package/dist/oracle/identity.d.ts.map +0 -1
package/dist/oracle/index.js
CHANGED
|
@@ -1,11 +1,113 @@
|
|
|
1
1
|
// src/oracle/digest.ts
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
var DIGEST_FORMAT = 1;
|
|
4
|
+
var SEPARATOR = String.fromCharCode(0);
|
|
4
5
|
var str = (s) => JSON.stringify(s);
|
|
5
6
|
var tagOf = (v) => {
|
|
6
7
|
const ctor = v.constructor;
|
|
7
8
|
return typeof ctor === "function" && typeof ctor.name === "string" ? ctor.name : "";
|
|
8
9
|
};
|
|
10
|
+
var ArraySink = class {
|
|
11
|
+
tokens = [];
|
|
12
|
+
push(token) {
|
|
13
|
+
this.tokens.push(token);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var StreamSink = class {
|
|
17
|
+
target;
|
|
18
|
+
buffer = [];
|
|
19
|
+
buffered = 0;
|
|
20
|
+
started = false;
|
|
21
|
+
constructor(target) {
|
|
22
|
+
this.target = target;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Write text that is NOT a canonical token and takes no separator — the
|
|
26
|
+
* `OK:` / `ERR:` discriminator, which the string form concatenates onto the
|
|
27
|
+
* front rather than joining.
|
|
28
|
+
*/
|
|
29
|
+
prefix(text) {
|
|
30
|
+
if (text === "") return;
|
|
31
|
+
this.buffer.push(text);
|
|
32
|
+
this.buffered += text.length;
|
|
33
|
+
}
|
|
34
|
+
push(token) {
|
|
35
|
+
if (this.started) {
|
|
36
|
+
this.buffer.push(SEPARATOR);
|
|
37
|
+
this.buffered += 1;
|
|
38
|
+
} else {
|
|
39
|
+
this.started = true;
|
|
40
|
+
}
|
|
41
|
+
this.buffer.push(token);
|
|
42
|
+
this.buffered += token.length;
|
|
43
|
+
if (this.buffered >= FLUSH_CHARS) this.flush();
|
|
44
|
+
}
|
|
45
|
+
flush() {
|
|
46
|
+
if (this.buffer.length === 0) return;
|
|
47
|
+
this.target.update(this.buffer.join(""));
|
|
48
|
+
this.buffer = [];
|
|
49
|
+
this.buffered = 0;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var FLUSH_CHARS = 1 << 16;
|
|
53
|
+
var CanonicalBudgetError = class extends Error {
|
|
54
|
+
name = "CanonicalBudgetError";
|
|
55
|
+
constructor(budget, projected) {
|
|
56
|
+
super(
|
|
57
|
+
`canonicalize: exceeded ${budget} visits. The value is not merely large \u2014 it is almost certainly a DAG being unrolled. This projection writes a shared subtree once per PATH that reaches it (see "Sharing is not a cycle"), so a node referenced from two places at each of d levels costs 2^d. Deduplicate the shared structure, or raise \`maxVisits\` if the value really is that big.` + (projected === void 0 ? "" : ` Refused EARLY: the walk was projected to visit ${projected} objects, so it could not have finished under this budget however long it ran. See {@link projectVisits}.`)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var DEFAULT_MAX_VISITS = 1e8;
|
|
62
|
+
var EXPANSION_PROBE_VISITS = 1e6;
|
|
63
|
+
var EXPANSION_PROBE_NODES = 2e5;
|
|
64
|
+
var EXPANSION_PROBE_MARGIN = 4;
|
|
65
|
+
function projectVisits(root) {
|
|
66
|
+
const memo = /* @__PURE__ */ new Map();
|
|
67
|
+
const path = /* @__PURE__ */ new Set();
|
|
68
|
+
let nodes = 0;
|
|
69
|
+
let usable = true;
|
|
70
|
+
const count = (v) => {
|
|
71
|
+
if (typeof v !== "object" || v === null) return 0;
|
|
72
|
+
if (path.has(v)) {
|
|
73
|
+
usable = false;
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
const done = memo.get(v);
|
|
77
|
+
if (done !== void 0) return done;
|
|
78
|
+
if (++nodes > EXPANSION_PROBE_NODES) {
|
|
79
|
+
usable = false;
|
|
80
|
+
return 1;
|
|
81
|
+
}
|
|
82
|
+
path.add(v);
|
|
83
|
+
let total = 1;
|
|
84
|
+
if (Array.isArray(v)) {
|
|
85
|
+
for (const item of v) total += count(item);
|
|
86
|
+
} else if (v instanceof Map) {
|
|
87
|
+
for (const [k, val] of v) total += count(k) + count(val);
|
|
88
|
+
} else if (v instanceof Set) {
|
|
89
|
+
for (const item of v) total += count(item);
|
|
90
|
+
} else if (!(v instanceof Date) && !(v instanceof RegExp)) {
|
|
91
|
+
for (const k of Object.keys(v)) total += count(v[k]);
|
|
92
|
+
}
|
|
93
|
+
path.delete(v);
|
|
94
|
+
memo.set(v, total);
|
|
95
|
+
return total;
|
|
96
|
+
};
|
|
97
|
+
try {
|
|
98
|
+
const total = count(root);
|
|
99
|
+
return usable ? total : void 0;
|
|
100
|
+
} catch {
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function probeExpansion(state) {
|
|
105
|
+
state.probeAt = -1;
|
|
106
|
+
const projected = projectVisits(state.root);
|
|
107
|
+
if (projected !== void 0 && projected / EXPANSION_PROBE_MARGIN > state.budget) {
|
|
108
|
+
throw new CanonicalBudgetError(state.budget, projected);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
9
111
|
function writeScalar(v, out) {
|
|
10
112
|
switch (typeof v) {
|
|
11
113
|
case "undefined":
|
|
@@ -47,11 +149,15 @@ function writeScalar(v, out) {
|
|
|
47
149
|
out.push("z");
|
|
48
150
|
}
|
|
49
151
|
}
|
|
50
|
-
function write(v,
|
|
152
|
+
function write(v, state, depth) {
|
|
153
|
+
const out = state.sink;
|
|
51
154
|
if (typeof v !== "object" || v === null) {
|
|
52
155
|
writeScalar(v, out);
|
|
53
156
|
return;
|
|
54
157
|
}
|
|
158
|
+
if (--state.visits < 0) throw new CanonicalBudgetError(state.budget);
|
|
159
|
+
if (state.visits === state.probeAt) probeExpansion(state);
|
|
160
|
+
const path = state.path;
|
|
55
161
|
const seenAt = path.get(v);
|
|
56
162
|
if (seenAt !== void 0) {
|
|
57
163
|
out.push(`^${depth - seenAt}`);
|
|
@@ -60,18 +166,18 @@ function write(v, path, depth, out) {
|
|
|
60
166
|
path.set(v, depth);
|
|
61
167
|
if (Array.isArray(v)) {
|
|
62
168
|
out.push("[");
|
|
63
|
-
for (const item of v) write(item,
|
|
169
|
+
for (const item of v) write(item, state, depth + 1);
|
|
64
170
|
out.push("]");
|
|
65
171
|
} else if (v instanceof Map) {
|
|
66
172
|
out.push("M");
|
|
67
173
|
for (const [k, val] of v) {
|
|
68
|
-
write(k,
|
|
69
|
-
write(val,
|
|
174
|
+
write(k, state, depth + 1);
|
|
175
|
+
write(val, state, depth + 1);
|
|
70
176
|
}
|
|
71
177
|
out.push("}");
|
|
72
178
|
} else if (v instanceof Set) {
|
|
73
179
|
out.push("S");
|
|
74
|
-
for (const item of v) write(item,
|
|
180
|
+
for (const item of v) write(item, state, depth + 1);
|
|
75
181
|
out.push("}");
|
|
76
182
|
} else if (v instanceof Date) {
|
|
77
183
|
out.push(`D${v.getTime()}`);
|
|
@@ -82,319 +188,44 @@ function write(v, path, depth, out) {
|
|
|
82
188
|
out.push(`{${str(tag === "Object" ? "" : tag)}`);
|
|
83
189
|
for (const k of Object.keys(v).sort()) {
|
|
84
190
|
out.push(`k${str(k)}`);
|
|
85
|
-
write(v[k],
|
|
191
|
+
write(v[k], state, depth + 1);
|
|
86
192
|
}
|
|
87
193
|
out.push("}");
|
|
88
194
|
}
|
|
89
195
|
path.delete(v);
|
|
90
196
|
}
|
|
91
|
-
function
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
function hash(payload2) {
|
|
97
|
-
return createHash("sha256").update(payload2).digest("hex");
|
|
98
|
-
}
|
|
99
|
-
var FINGERPRINT_HEX = 16;
|
|
100
|
-
|
|
101
|
-
// src/oracle/identity.ts
|
|
102
|
-
var defaultProjectError = (thrown) => thrown instanceof Error ? { name: thrown.name, message: thrown.message } : { thrown };
|
|
103
|
-
function payload(surface, entry, options) {
|
|
104
|
-
try {
|
|
105
|
-
return { text: `OK:${canonicalize(surface.parse(entry.source, entry.id))}`, threw: false };
|
|
106
|
-
} catch (thrown) {
|
|
107
|
-
const project = options.projectError ?? defaultProjectError;
|
|
108
|
-
return { text: `ERR:${canonicalize(project(thrown, entry.id, surface.name))}`, threw: true };
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
var fingerprint = (text) => hash(text).slice(0, FINGERPRINT_HEX);
|
|
112
|
-
function digestCorpus(surfaces, corpus, options = {}) {
|
|
113
|
-
if (surfaces.length === 0) throw new Error("digestCorpus: no surfaces \u2014 there is nothing to compare.");
|
|
114
|
-
const names = /* @__PURE__ */ new Set();
|
|
115
|
-
for (const s of surfaces) {
|
|
116
|
-
if (names.has(s.name)) throw new Error(`digestCorpus: duplicate surface name ${JSON.stringify(s.name)}.`);
|
|
117
|
-
names.add(s.name);
|
|
118
|
-
}
|
|
119
|
-
const entries = [...corpus].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
120
|
-
for (let n = 1; n < entries.length; n++) {
|
|
121
|
-
if (entries[n].id === entries[n - 1].id) {
|
|
122
|
-
throw new Error(`digestCorpus: duplicate corpus id ${JSON.stringify(entries[n].id)}.`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
const perEntry = {};
|
|
126
|
-
const threw = {};
|
|
127
|
-
const texts = [];
|
|
128
|
-
for (const s of surfaces) threw[s.name] = 0;
|
|
129
|
-
for (const entry of entries) {
|
|
130
|
-
const row = {};
|
|
131
|
-
const rowTexts = /* @__PURE__ */ new Map();
|
|
132
|
-
for (const s of surfaces) {
|
|
133
|
-
const result = payload(s, entry, options);
|
|
134
|
-
if (result.threw) threw[s.name]++;
|
|
135
|
-
row[s.name] = fingerprint(result.text);
|
|
136
|
-
rowTexts.set(s.name, result.text);
|
|
137
|
-
}
|
|
138
|
-
perEntry[entry.id] = row;
|
|
139
|
-
texts.push(rowTexts);
|
|
140
|
-
}
|
|
141
|
-
verifyDeterminism(surfaces, entries, texts, options);
|
|
142
|
-
return {
|
|
143
|
-
format: DIGEST_FORMAT,
|
|
144
|
-
harness: HARNESS_DIGEST,
|
|
145
|
-
entries: entries.length,
|
|
146
|
-
surfaces: surfaces.map((s) => ({
|
|
147
|
-
name: s.name,
|
|
148
|
-
// The surface NAME is inside its own aggregate: two surfaces that happen
|
|
149
|
-
// to agree on every entry must still have distinct aggregates, or a
|
|
150
|
-
// report that swapped them would compare equal.
|
|
151
|
-
aggregate: hash(
|
|
152
|
-
[`surface:${s.name}`, ...entries.map((e) => `${e.id}:${perEntry[e.id][s.name]}`)].join("\n")
|
|
153
|
-
),
|
|
154
|
-
threw: threw[s.name]
|
|
155
|
-
})),
|
|
156
|
-
perEntry
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
function verifyDeterminism(surfaces, entries, texts, options) {
|
|
160
|
-
const want = options.determinismSample ?? 32;
|
|
161
|
-
if (want <= 0 || entries.length === 0) return;
|
|
162
|
-
const stride = Math.max(1, Math.floor(entries.length / Math.min(want, entries.length)));
|
|
163
|
-
for (let n = 0; n < entries.length; n += stride) {
|
|
164
|
-
const entry = entries[n];
|
|
165
|
-
for (const s of surfaces) {
|
|
166
|
-
if (payload(s, entry, options).text === texts[n].get(s.name)) continue;
|
|
167
|
-
throw new Error(
|
|
168
|
-
`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.`
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
function compareReports(before, after) {
|
|
174
|
-
if (before.harness !== after.harness) {
|
|
175
|
-
return incomparable(
|
|
176
|
-
`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.`
|
|
197
|
+
function newState(sink, options, root) {
|
|
198
|
+
const budget = options?.maxVisits ?? DEFAULT_MAX_VISITS;
|
|
199
|
+
if (!Number.isSafeInteger(budget) || budget < 0) {
|
|
200
|
+
throw new RangeError(
|
|
201
|
+
`canonicalize: maxVisits must be a non-negative safe integer, received ${String(budget)}. A non-finite or fractional budget does not raise the ceiling, it REMOVES it \u2014 the budget is spent with \`--visits < 0\`, which is false forever for NaN and never reached for Infinity \u2014 so the walk this bounds would run unbounded into the OOM the budget exists to prevent.`
|
|
177
202
|
);
|
|
178
203
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
const beforeIds = new Set(Object.keys(before.perEntry));
|
|
183
|
-
const afterIds = new Set(Object.keys(after.perEntry));
|
|
184
|
-
const addedEntries = [...afterIds].filter((id) => !beforeIds.has(id)).sort();
|
|
185
|
-
const removedEntries = [...beforeIds].filter((id) => !afterIds.has(id)).sort();
|
|
186
|
-
const shared = [...beforeIds].filter((id) => afterIds.has(id)).sort();
|
|
187
|
-
const byName = /* @__PURE__ */ new Map();
|
|
188
|
-
for (const s of before.surfaces) byName.set(s.name, { ...byName.get(s.name), before: s });
|
|
189
|
-
for (const s of after.surfaces) byName.set(s.name, { ...byName.get(s.name), after: s });
|
|
190
|
-
const surfaces = [...byName.entries()].sort(([a], [b]) => a < b ? -1 : 1).map(([name, pair]) => {
|
|
191
|
-
const moved = pair.before && pair.after ? shared.filter((id) => before.perEntry[id][name] !== after.perEntry[id][name]) : [];
|
|
192
|
-
return {
|
|
193
|
-
name,
|
|
194
|
-
before: pair.before?.aggregate ?? null,
|
|
195
|
-
after: pair.after?.aggregate ?? null,
|
|
196
|
-
equal: pair.before !== void 0 && pair.after !== void 0 && pair.before.aggregate === pair.after.aggregate,
|
|
197
|
-
moved,
|
|
198
|
-
beforeThrew: pair.before?.threw ?? null,
|
|
199
|
-
afterThrew: pair.after?.threw ?? null
|
|
200
|
-
};
|
|
201
|
-
});
|
|
202
|
-
const identical = addedEntries.length === 0 && removedEntries.length === 0 && surfaces.every((s) => s.equal);
|
|
203
|
-
return { verdict: identical ? "identical" : "moved", reason: null, addedEntries, removedEntries, surfaces };
|
|
204
|
+
const probeAt = budget >= EXPANSION_PROBE_VISITS ? budget - EXPANSION_PROBE_VISITS : -1;
|
|
205
|
+
return { path: /* @__PURE__ */ new Map(), sink, budget, root, visits: budget, probeAt };
|
|
204
206
|
}
|
|
205
|
-
function
|
|
206
|
-
|
|
207
|
+
function canonicalize(value, options) {
|
|
208
|
+
const out = new ArraySink();
|
|
209
|
+
write(value, newState(out, options, value), 0);
|
|
210
|
+
return out.tokens.join(SEPARATOR);
|
|
207
211
|
}
|
|
208
|
-
function
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (s.before === null) lines.push(` + surface ${s.name} (added)`);
|
|
214
|
-
else if (s.after === null) lines.push(` - surface ${s.name} (removed)`);
|
|
215
|
-
else if (s.equal) lines.push(` = ${s.name} ${s.before.slice(0, 16)}\u2026 threw=${s.afterThrew}`);
|
|
216
|
-
else {
|
|
217
|
-
lines.push(
|
|
218
|
-
` ! ${s.name} ${s.before.slice(0, 16)}\u2026 -> ${s.after.slice(0, 16)}\u2026 threw ${s.beforeThrew} -> ${s.afterThrew} (${s.moved.length} entries moved)`
|
|
219
|
-
);
|
|
220
|
-
for (const id of s.moved.slice(0, max)) lines.push(` ${id}`);
|
|
221
|
-
if (s.moved.length > max) lines.push(` \u2026 and ${s.moved.length - max} more`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
if (c.addedEntries.length > 0) lines.push(` corpus GAINED ${c.addedEntries.length} entries`);
|
|
225
|
-
if (c.removedEntries.length > 0) {
|
|
226
|
-
lines.push(
|
|
227
|
-
` corpus LOST ${c.removedEntries.length} entries \u2014 a differential over a SMALLER corpus is a weaker gate, not a passing one`
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
return [c.verdict === "identical" ? "IDENTICAL \u2014 output-neutral" : "MOVED \u2014 this is not a refactor", ...lines].join("\n");
|
|
231
|
-
}
|
|
232
|
-
function canaryReport() {
|
|
233
|
-
class Tagged {
|
|
234
|
-
x;
|
|
235
|
-
constructor(x) {
|
|
236
|
-
this.x = x;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
const shared = { shared: true };
|
|
240
|
-
const cyclic = { name: "root" };
|
|
241
|
-
cyclic.self = cyclic;
|
|
242
|
-
cyclic.child = { up: cyclic };
|
|
243
|
-
const values = {
|
|
244
|
-
"a/scalars": [0, -0, 1.5, Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, 10n, true, false, null],
|
|
245
|
-
"b/absent": [{ a: void 0 }, {}, { a: null }, [void 0], []],
|
|
246
|
-
"c/key-order": [{ a: 1, b: 2 }, { b: 2, a: 1 }],
|
|
247
|
-
"d/collections": [/* @__PURE__ */ new Map([["k", 1], ["j", 2]]), /* @__PURE__ */ new Set([1, 2]), /* @__PURE__ */ new Date(0), /ab+c/giu],
|
|
248
|
-
"e/tagged": [new Tagged(1), { x: 1 }],
|
|
249
|
-
"f/sharing": { left: shared, right: shared },
|
|
250
|
-
"g/cycle": cyclic,
|
|
251
|
-
"h/text": ["", "a\0b", "a b", '"quoted"', "\\"],
|
|
252
|
-
"i/callable": [function named() {
|
|
253
|
-
}, /* @__PURE__ */ Symbol("sym")]
|
|
254
|
-
};
|
|
255
|
-
const surfaces = [
|
|
256
|
-
{ name: "value", parse: (_source, id) => values[id] },
|
|
257
|
-
{
|
|
258
|
-
name: "thrower",
|
|
259
|
-
parse: (_source, id) => {
|
|
260
|
-
if (id === "a/scalars") throw new TypeError("canary");
|
|
261
|
-
if (id === "b/absent") throw "a bare string";
|
|
262
|
-
return { id };
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
];
|
|
266
|
-
const corpus = Object.keys(values).map((id) => ({ id, source: id }));
|
|
267
|
-
const perEntry = {};
|
|
268
|
-
const threw = { value: 0, thrower: 0 };
|
|
269
|
-
for (const entry of [...corpus].sort((a, b) => a.id < b.id ? -1 : 1)) {
|
|
270
|
-
const row = {};
|
|
271
|
-
for (const s of surfaces) {
|
|
272
|
-
const result = payload(s, entry, {});
|
|
273
|
-
if (result.threw) threw[s.name]++;
|
|
274
|
-
row[s.name] = fingerprint(result.text);
|
|
275
|
-
}
|
|
276
|
-
perEntry[entry.id] = row;
|
|
277
|
-
}
|
|
278
|
-
const ids = Object.keys(perEntry).sort();
|
|
279
|
-
return {
|
|
280
|
-
format: DIGEST_FORMAT,
|
|
281
|
-
harness: "",
|
|
282
|
-
entries: ids.length,
|
|
283
|
-
surfaces: surfaces.map((s) => ({
|
|
284
|
-
name: s.name,
|
|
285
|
-
aggregate: hash([`surface:${s.name}`, ...ids.map((id) => `${id}:${perEntry[id][s.name]}`)].join("\n")),
|
|
286
|
-
threw: threw[s.name]
|
|
287
|
-
})),
|
|
288
|
-
perEntry
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
var HARNESS_DIGEST = (() => {
|
|
292
|
-
const canary = canaryReport();
|
|
293
|
-
return hash(
|
|
294
|
-
[`format:${DIGEST_FORMAT}`, ...canary.surfaces.map((s) => `${s.name}:${s.aggregate}:${s.threw}`)].join("\n")
|
|
295
|
-
);
|
|
296
|
-
})();
|
|
297
|
-
|
|
298
|
-
// src/oracle/corpus.ts
|
|
299
|
-
import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
300
|
-
import { join, relative, resolve, sep } from "node:path";
|
|
301
|
-
var DEFAULT_IGNORE = [".git", "node_modules/.cache"];
|
|
302
|
-
function loadCorpus(options) {
|
|
303
|
-
const base = resolve(options.base);
|
|
304
|
-
const ignoreDirs = options.ignoreDirs ?? DEFAULT_IGNORE;
|
|
305
|
-
const ignoreNames = new Set(ignoreDirs.filter((d) => !d.includes("/")));
|
|
306
|
-
const ignorePaths = ignoreDirs.filter((d) => d.includes("/")).map((d) => d.replace(/^\.?\/+/, "").replace(/\/+$/, ""));
|
|
307
|
-
const maxBytes = options.maxBytes ?? Number.POSITIVE_INFINITY;
|
|
308
|
-
const exts = new Set(options.extensions.map((e) => e.toLowerCase()));
|
|
309
|
-
const missingRoots = [];
|
|
310
|
-
const skippedLarge = [];
|
|
311
|
-
const files = /* @__PURE__ */ new Map();
|
|
312
|
-
const isIgnored = (name, relFromRoot) => {
|
|
313
|
-
if (ignoreNames.has(name)) return true;
|
|
314
|
-
return ignorePaths.some((p) => relFromRoot === p || relFromRoot.endsWith(`/${p}`));
|
|
315
|
-
};
|
|
316
|
-
const walk = (dir, root, visited) => {
|
|
317
|
-
let real;
|
|
318
|
-
try {
|
|
319
|
-
real = realpathSync(dir);
|
|
320
|
-
} catch {
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
if (visited.has(real)) return;
|
|
324
|
-
visited.add(real);
|
|
325
|
-
for (const item of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
|
|
326
|
-
const full = join(dir, item.name);
|
|
327
|
-
if (isIgnored(item.name, relative(root, full).split(sep).join("/"))) continue;
|
|
328
|
-
let isDir = item.isDirectory();
|
|
329
|
-
let isFile = item.isFile();
|
|
330
|
-
let size = 0;
|
|
331
|
-
if (item.isSymbolicLink() || !isDir && !isFile) {
|
|
332
|
-
try {
|
|
333
|
-
const st = statSync(full);
|
|
334
|
-
isDir = st.isDirectory();
|
|
335
|
-
isFile = st.isFile();
|
|
336
|
-
size = st.size;
|
|
337
|
-
} catch {
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
if (isDir) {
|
|
342
|
-
walk(full, root, visited);
|
|
343
|
-
continue;
|
|
344
|
-
}
|
|
345
|
-
if (!isFile) continue;
|
|
346
|
-
const dot = item.name.lastIndexOf(".");
|
|
347
|
-
if (dot < 0 || !exts.has(item.name.slice(dot).toLowerCase())) continue;
|
|
348
|
-
if (size === 0) {
|
|
349
|
-
try {
|
|
350
|
-
size = statSync(full).size;
|
|
351
|
-
} catch {
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
if (size > maxBytes) {
|
|
356
|
-
skippedLarge.push(idOf(base, full));
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
let realFile;
|
|
360
|
-
try {
|
|
361
|
-
realFile = realpathSync(full);
|
|
362
|
-
} catch {
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
const id = idOf(base, full);
|
|
366
|
-
const existing = files.get(realFile);
|
|
367
|
-
if (existing === void 0 || id < existing) files.set(realFile, id);
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
|
-
for (const root of options.roots) {
|
|
371
|
-
const full = resolve(base, root);
|
|
372
|
-
try {
|
|
373
|
-
if (!statSync(full).isDirectory()) throw new Error("not a directory");
|
|
374
|
-
} catch {
|
|
375
|
-
if (!options.allowMissingRoots) {
|
|
376
|
-
throw new Error(
|
|
377
|
-
`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.`
|
|
378
|
-
);
|
|
379
|
-
}
|
|
380
|
-
missingRoots.push(root);
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
walk(full, full, /* @__PURE__ */ new Set());
|
|
384
|
-
}
|
|
385
|
-
const entries = [...files.values()].sort().map((id) => ({ id, source: readFileSync(resolve(base, id), "utf8") }));
|
|
386
|
-
return { entries, missingRoots, skippedLarge: skippedLarge.sort() };
|
|
212
|
+
function digestInto(target, value, prefix = "", options) {
|
|
213
|
+
const sink = new StreamSink(target);
|
|
214
|
+
sink.prefix(prefix);
|
|
215
|
+
write(value, newState(sink, options, value), 0);
|
|
216
|
+
sink.flush();
|
|
387
217
|
}
|
|
388
|
-
function
|
|
389
|
-
|
|
218
|
+
function digestValue(value, prefix = "", options) {
|
|
219
|
+
const sha = createHash("sha256");
|
|
220
|
+
digestInto(sha, value, prefix, options);
|
|
221
|
+
return sha.digest("hex");
|
|
390
222
|
}
|
|
391
223
|
export {
|
|
224
|
+
CanonicalBudgetError,
|
|
225
|
+
DEFAULT_MAX_VISITS,
|
|
392
226
|
DIGEST_FORMAT,
|
|
393
|
-
HARNESS_DIGEST,
|
|
394
227
|
canonicalize,
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
formatComparison,
|
|
398
|
-
loadCorpus
|
|
228
|
+
digestInto,
|
|
229
|
+
digestValue
|
|
399
230
|
};
|
|
400
231
|
//# sourceMappingURL=index.js.map
|