harness-everything 0.36.3 → 0.36.4

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.
Files changed (47) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +10 -0
  4. package/ci/fixtures/doc-obligations/read-boundary/boundary-controls.json +211 -0
  5. package/ci/fixtures/doc-obligations/read-boundary/boundary-policy.json +325 -0
  6. package/ci/fixtures/doc-obligations/read-boundary/cases.json +14058 -0
  7. package/ci/fixtures/doc-obligations/read-boundary/expected.json +1 -0
  8. package/ci/fixtures/doc-obligations/read-boundary/identity-cases.json +119 -0
  9. package/ci/fixtures/doc-obligations/read-boundary/identity-expected.json +1 -0
  10. package/ci/fixtures/doc-obligations/read-boundary/scope-cases.json +208 -0
  11. package/ci/fixtures/doc-obligations/read-boundary/scope-expected.json +1 -0
  12. package/ci/fixtures/doc-obligations/reconciliation-prep/cases.json +2751 -0
  13. package/ci/fixtures/doc-obligations/reconciliation-prep/flood-entrypoint.js +5 -0
  14. package/ci/fixtures/doc-obligations/reconciliation-prep/hang-entrypoint.js +5 -0
  15. package/ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json +648 -0
  16. package/ci/fixtures/doc-obligations/reconciliation-prep/provider-contract.json +100 -0
  17. package/ci/fixtures/doc-obligations/reconciliation-prep/trust-contract.json +188 -0
  18. package/ci/lib/doc-evidence-boundary-check.js +1240 -0
  19. package/ci/lib/doc-evidence-fixtures.js +459 -0
  20. package/ci/lib/doc-evidence-trace.js +171 -0
  21. package/ci/lib/doc-reconciliation-prep-fixtures.js +502 -0
  22. package/ci/lib/snapshot-stdin.js +27 -0
  23. package/ci/mechanism-325-3b-preparation.test.js +382 -0
  24. package/ci/mechanism-341-boundary-enforcement.test.js +660 -0
  25. package/ci/mechanism-341-capture-module.test.js +357 -0
  26. package/ci/mechanism-341-mutation-runner.test.js +413 -0
  27. package/ci/mechanism-341-read-boundary.test.js +388 -0
  28. package/ci/mechanism-341-snapshot-stdin.test.js +40 -0
  29. package/ci/mutation-check.js +805 -45
  30. package/ci/mutation-loader.js +544 -57
  31. package/ci/prep/doc-reconciliation-red.js +407 -0
  32. package/ci/stress-341-boundary-control.js +85 -0
  33. package/docs/doc-evidence-baseline-binding.md +105 -19
  34. package/docs/doc-evidence-boundary.md +135 -0
  35. package/docs/doc-workflow-reconciliation.md +224 -0
  36. package/docs/document-lifecycle.md +8 -1
  37. package/harness-everything/SKILL.md +1 -1
  38. package/harness-everything/scripts/doc-evidence-capture.js +331 -0
  39. package/harness-everything/scripts/doc-evidence.js +72 -223
  40. package/opencode-plugin/plugin.json +1 -1
  41. package/package.json +5 -3
  42. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  43. package/plugins/harness-everything/plugin.json +1 -1
  44. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  45. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence-capture.js +331 -0
  46. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +72 -223
  47. package/sbom.cdx.json +43 -7
@@ -0,0 +1,459 @@
1
+ 'use strict';
2
+
3
+ // #341 Phase 0A: fresh caller-graph factory for the read-boundary corpus.
4
+ //
5
+ // A recipe is plain JSON that describes the five checker inputs as named nodes. Each call to
6
+ // buildCase() creates new graphs and new accessor sequences, so no run can observe state left by
7
+ // an earlier run. Traced builds wrap every object in a Proxy over its twin (see
8
+ // doc-evidence-trace.js); untraced builds hand the checker the plain objects. Both builds use the
9
+ // recipe's node names as identities, so their encodings can be compared directly.
10
+ //
11
+ // Recipe vocabulary (values are primitives, {"$ref": name}, {"$undefined": true}, or an inline node):
12
+ // {"kind":"object","proto":"Object"|"null"|{"$ref":name},"frozen":bool,"props":{key: prop}}
13
+ // {"kind":"array","frozen":bool,"items":[value...]}
14
+ // {"kind":"opaque","ctor":"Map"|"Date"|"RegExp"} real built-in instance, no properties
15
+ // {"kind":"sentinel","message":string} thrown identity object, never traced
16
+ // {"kind":"hostileProxy","target":"object"|"array","trap":name,"throws":sentinelName,"props":{...}}
17
+ // prop: value | {"$data": value, "enumerable": bool} | {"$accessor": {"sequence": [outcome...], "enumerable": bool}}
18
+ // outcome: value | {"$throw": sentinelName} | {"$mutate": {"node","key","to"}, "$then": value}
19
+ // (read i returns sequence[min(i, last)]; a $mutate outcome first rewrites a caller data property
20
+ // with a raw write that records no trace event, then returns $then)
21
+ // keys beginning "$sym:" are symbol keys, described by the remainder.
22
+ // recipe.input: subset of the five checker roots; an absent key stays absent.
23
+ // recipe.afterReturn: [{"node","key","to"}] raw writes applied after the first checker run.
24
+ // "to": {"$delete": true} removes the own key (absent, not own-undefined); other values write it.
25
+ // recipe.detached: true runs a second checker invocation over the mutated caller graph and
26
+ // records whether the first result survived the mutation.
27
+
28
+ const { createRecorder, describeValue, describeDescriptor, encodeKey, snapshotTwin, traceProxy } = require('./doc-evidence-trace');
29
+
30
+ const ROOTS = Object.freeze(['obligations', 'artifacts', 'baseline', 'executionRecords', 'reviewRecords']);
31
+ const NODE_KINDS = Object.freeze(['object', 'array', 'sentinel', 'hostileProxy', 'opaque']);
32
+
33
+ function isObject(value) {
34
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
35
+ }
36
+
37
+ function fail(message) {
38
+ throw new Error(`doc-evidence recipe: ${message}`);
39
+ }
40
+
41
+ function isRef(value) {
42
+ return isObject(value) && typeof value.$ref === 'string' && Object.keys(value).length === 1;
43
+ }
44
+
45
+ function isDeleteSpec(value) {
46
+ return isObject(value) && value.$delete === true && Object.keys(value).length === 1;
47
+ }
48
+
49
+ function isUndefinedSpec(value) {
50
+ return isObject(value) && value.$undefined === true && Object.keys(value).length === 1;
51
+ }
52
+
53
+ function isThrowSpec(value) {
54
+ return isObject(value) && typeof value.$throw === 'string' && Object.keys(value).length === 1;
55
+ }
56
+
57
+ function isMutateSpec(value) {
58
+ return isObject(value) && isObject(value.$mutate) && Object.prototype.hasOwnProperty.call(value, '$then');
59
+ }
60
+
61
+ // Flattens inline nodes into named nodes so each caller-visible object has exactly one identity.
62
+ // Inline nodes take the path of their position (`obligation.props.scope`, `artifacts.items[0]`).
63
+ function normalizeRecipe(recipe) {
64
+ if (!isObject(recipe) || !isObject(recipe.nodes)) fail('recipe.nodes must be an object');
65
+ if (!isObject(recipe.input)) fail('recipe.input must be an object');
66
+ const nodes = new Map();
67
+
68
+ const flatValue = (value, path) => {
69
+ if (value === null || ['string', 'number', 'boolean'].includes(typeof value)) return value;
70
+ if (isRef(value) || isUndefinedSpec(value)) return value;
71
+ if (isObject(value) && NODE_KINDS.includes(value.kind)) {
72
+ if (nodes.has(path)) fail(`duplicate inline node path ${path}`);
73
+ nodes.set(path, flatNode(value, path));
74
+ return { $ref: path };
75
+ }
76
+ fail(`unsupported value at ${path}`);
77
+ };
78
+
79
+ const flatProps = (specProps, name) => {
80
+ const props = {};
81
+ for (const [key, prop] of Object.entries(specProps || {})) {
82
+ const label = `${name}.${key}`;
83
+ if (isObject(prop) && prop.$accessor !== undefined) {
84
+ const accessor = prop.$accessor;
85
+ if (!Array.isArray(accessor.sequence) || accessor.sequence.length === 0) fail(`empty accessor sequence at ${label}`);
86
+ props[key] = {
87
+ type: 'accessor',
88
+ enumerable: accessor.enumerable !== false,
89
+ sequence: accessor.sequence.map((outcome, index) => {
90
+ if (isThrowSpec(outcome)) return outcome;
91
+ if (isMutateSpec(outcome)) {
92
+ const { node, key: mutatedKey, to } = outcome.$mutate;
93
+ if (typeof node !== 'string' || typeof mutatedKey !== 'string') fail(`$mutate needs node and key at ${label}#${index}`);
94
+ return { mutate: { node, key: mutatedKey, to: flatValue(to, `${label}#${index}.to`) }, then: flatValue(outcome.$then, `${label}#${index}.then`) };
95
+ }
96
+ return flatValue(outcome, `${label}#${index}`);
97
+ })
98
+ };
99
+ } else if (isObject(prop) && prop.$data !== undefined) {
100
+ props[key] = { type: 'data', enumerable: prop.enumerable !== false, value: flatValue(prop.$data, label) };
101
+ } else {
102
+ props[key] = { type: 'data', enumerable: true, value: flatValue(prop, label) };
103
+ }
104
+ }
105
+ return props;
106
+ };
107
+
108
+ const flatNode = (spec, name) => {
109
+ if (spec.kind === 'object') {
110
+ const proto = spec.proto === undefined ? 'Object' : spec.proto;
111
+ if (!(proto === 'Object' || proto === 'null' || isRef(proto))) fail(`invalid proto at ${name}`);
112
+ return { kind: 'object', proto, frozen: spec.frozen === true, props: flatProps(spec.props, name) };
113
+ }
114
+ if (spec.kind === 'opaque') {
115
+ if (!['Map', 'Date', 'RegExp'].includes(spec.ctor)) fail(`opaque ctor must be Map, Date or RegExp at ${name}`);
116
+ return { kind: 'opaque', ctor: spec.ctor };
117
+ }
118
+ if (spec.kind === 'array') {
119
+ if (!Array.isArray(spec.items)) fail(`array items must be a list at ${name}`);
120
+ return {
121
+ kind: 'array',
122
+ frozen: spec.frozen === true,
123
+ items: spec.items.map((item, index) => flatValue(item, `${name}[${index}]`))
124
+ };
125
+ }
126
+ if (spec.kind === 'sentinel') {
127
+ if (typeof spec.message !== 'string') fail(`sentinel message must be a string at ${name}`);
128
+ return { kind: 'sentinel', message: spec.message };
129
+ }
130
+ if (spec.kind === 'hostileProxy') {
131
+ if (!['object', 'array'].includes(spec.target)) fail(`hostile proxy target must be object or array at ${name}`);
132
+ if (typeof spec.trap !== 'string') fail(`hostile proxy trap must be named at ${name}`);
133
+ if (typeof spec.throws !== 'string') fail(`hostile proxy must name a sentinel at ${name}`);
134
+ return {
135
+ kind: 'hostileProxy',
136
+ target: spec.target,
137
+ trap: spec.trap,
138
+ throws: spec.throws,
139
+ props: flatProps(spec.props, name)
140
+ };
141
+ }
142
+ fail(`unknown node kind at ${name}`);
143
+ };
144
+
145
+ for (const [name, spec] of Object.entries(recipe.nodes)) {
146
+ if (!isObject(spec) || !NODE_KINDS.includes(spec.kind)) fail(`unknown node kind at ${name}`);
147
+ if (nodes.has(name)) fail(`duplicate node ${name}`);
148
+ nodes.set(name, flatNode(spec, name));
149
+ }
150
+
151
+ const input = {};
152
+ for (const [key, value] of Object.entries(recipe.input)) {
153
+ if (!ROOTS.includes(key)) fail(`unknown checker root ${key}`);
154
+ input[key] = flatValue(value, `input.${key}`);
155
+ }
156
+
157
+ const afterReturn = (Array.isArray(recipe.afterReturn) ? recipe.afterReturn : []).map((item, index) => {
158
+ if (!isObject(item) || typeof item.node !== 'string' || typeof item.key !== 'string') fail(`afterReturn[${index}] needs node and key`);
159
+ if (isDeleteSpec(item.to)) return { node: item.node, key: item.key, delete: true };
160
+ return { node: item.node, key: item.key, to: flatValue(item.to, `afterReturn[${index}].to`) };
161
+ });
162
+ return { nodes, input, afterReturn, detached: recipe.detached === true };
163
+ }
164
+
165
+ function keyLabel(key) {
166
+ return typeof key === 'symbol' ? `[${key.description}]` : key;
167
+ }
168
+
169
+ // Builds one fresh graph. `traced` selects the Proxy-wrapped variant; the checker receives an input
170
+ // object whose roots are exactly the recipe's declared roots.
171
+ function buildCase(recipe, { traced }) {
172
+ const flat = normalizeRecipe(recipe);
173
+ const recorder = createRecorder();
174
+ const counters = { accessors: {}, traps: {}, mutations: [] };
175
+ const symbols = new Map();
176
+ const symbolFor = name => {
177
+ if (!symbols.has(name)) symbols.set(name, Symbol(name));
178
+ return symbols.get(name);
179
+ };
180
+ const sentinels = new Map();
181
+ const raws = new Map();
182
+ const reps = new Map();
183
+ const prescribed = [];
184
+
185
+ // A prescribed mutation is a raw data write on a caller object. It is never traced, and the
186
+ // integrity comparison excludes exactly the keys the factory wrote, so any other change still fails.
187
+ const applyMutation = (nodeName, key, value) => {
188
+ const raw = raws.get(nodeName);
189
+ if (raw === undefined) fail(`mutation target ${nodeName} is not a caller object`);
190
+ Object.defineProperty(raw, key, { value, writable: true, enumerable: true, configurable: true });
191
+ prescribed.push({ node: nodeName, key });
192
+ counters.mutations.push({ node: nodeName, key });
193
+ };
194
+
195
+ // A post-return delete removes the own key, so later reads see absence rather than an own undefined value.
196
+ const applyDelete = (nodeName, key) => {
197
+ const raw = raws.get(nodeName);
198
+ if (raw === undefined) fail(`mutation target ${nodeName} is not a caller object`);
199
+ if (!Reflect.deleteProperty(raw, key)) fail(`caller property ${nodeName}.${key} is not configurable`);
200
+ if (Object.prototype.hasOwnProperty.call(raw, key)) fail(`delete left ${nodeName}.${key} as an own property`);
201
+ prescribed.push({ node: nodeName, key });
202
+ counters.mutations.push({ node: nodeName, key, op: 'delete' });
203
+ };
204
+
205
+ for (const [name, node] of flat.nodes) {
206
+ if (node.kind !== 'sentinel') continue;
207
+ const sentinel = new Error(node.message);
208
+ sentinels.set(name, sentinel);
209
+ reps.set(name, recorder.register(sentinel, name));
210
+ }
211
+
212
+ for (const [name, node] of flat.nodes) {
213
+ if (node.kind === 'object') {
214
+ raws.set(name, node.proto === 'null' ? Object.create(null) : {});
215
+ } else if (node.kind === 'array') {
216
+ raws.set(name, []);
217
+ } else if (node.kind === 'hostileProxy') {
218
+ raws.set(name, node.target === 'array' ? [] : {});
219
+ } else if (node.kind === 'opaque') {
220
+ raws.set(name, node.ctor === 'Map' ? new Map() : node.ctor === 'Date' ? new Date(0) : new RegExp('x'));
221
+ }
222
+ }
223
+
224
+ for (const [name, node] of flat.nodes) {
225
+ const raw = raws.get(name);
226
+ if (raw === undefined) continue;
227
+ recorder.register(raw, name);
228
+ if (node.kind === 'hostileProxy') {
229
+ const sentinel = sentinels.get(node.throws);
230
+ if (sentinel === undefined) fail(`hostile proxy ${name} throws unknown sentinel ${node.throws}`);
231
+ counters.traps[name] = { calls: 0 };
232
+ const override = () => {
233
+ counters.traps[name].calls += 1;
234
+ throw sentinel;
235
+ };
236
+ const overrides = { [node.trap]: override };
237
+ reps.set(name, traced
238
+ ? traceProxy(recorder, name, raw, overrides)
239
+ : recorder.register(new Proxy(raw, overrides), name));
240
+ } else {
241
+ reps.set(name, traced ? traceProxy(recorder, name, raw) : raw);
242
+ }
243
+ }
244
+
245
+ const resolve = value => {
246
+ if (value === null || typeof value !== 'object') return value;
247
+ if (isRef(value)) {
248
+ if (!reps.has(value.$ref)) fail(`unknown ref ${value.$ref}`);
249
+ return reps.get(value.$ref);
250
+ }
251
+ if (isUndefinedSpec(value)) return undefined;
252
+ fail('unresolvable value');
253
+ };
254
+
255
+ const accessorGetter = (name, key, sequence) => {
256
+ const path = `${name}.${keyLabel(key)}`;
257
+ counters.accessors[path] = { reads: 0, receivers: [] };
258
+ const getter = function () {
259
+ const entry = counters.accessors[path];
260
+ const outcome = sequence[Math.min(entry.reads, sequence.length - 1)];
261
+ entry.reads += 1;
262
+ entry.receivers.push(recorder.idOf(this) ?? '<unregistered>');
263
+ if (isThrowSpec(outcome)) throw sentinels.get(outcome.$throw);
264
+ if (outcome !== null && typeof outcome === 'object' && 'mutate' in outcome) {
265
+ applyMutation(outcome.mutate.node, outcome.mutate.key, resolve(outcome.mutate.to));
266
+ return resolve(outcome.then);
267
+ }
268
+ return resolve(outcome);
269
+ };
270
+ return recorder.register(getter, `${path}#get`);
271
+ };
272
+
273
+ // Prototypes are set while every raw object is still extensible, before any freeze.
274
+ for (const [name, node] of flat.nodes) {
275
+ if (node.kind === 'object' && isRef(node.proto)) {
276
+ if (!reps.has(node.proto.$ref)) fail(`unknown prototype ${node.proto.$ref}`);
277
+ Object.setPrototypeOf(raws.get(name), reps.get(node.proto.$ref));
278
+ }
279
+ }
280
+
281
+ for (const [name, node] of flat.nodes) {
282
+ const raw = raws.get(name);
283
+ if (node.kind === 'object' || node.kind === 'hostileProxy') {
284
+ for (const [key, prop] of Object.entries(node.props || {})) {
285
+ const propertyKey = key.startsWith('$sym:') ? symbolFor(key.slice('$sym:'.length)) : key;
286
+ if (prop.type === 'accessor') {
287
+ Object.defineProperty(raw, propertyKey, {
288
+ get: accessorGetter(name, propertyKey, prop.sequence),
289
+ enumerable: prop.enumerable,
290
+ configurable: true
291
+ });
292
+ } else {
293
+ Object.defineProperty(raw, propertyKey, {
294
+ value: resolve(prop.value),
295
+ writable: true,
296
+ enumerable: prop.enumerable,
297
+ configurable: true
298
+ });
299
+ }
300
+ }
301
+ } else if (node.kind === 'array') {
302
+ for (const item of node.items) raw.push(resolve(item));
303
+ }
304
+ if ((node.kind === 'object' || node.kind === 'array') && node.frozen) Object.freeze(raw);
305
+ }
306
+
307
+ const input = {};
308
+ for (const key of ROOTS) {
309
+ if (Object.prototype.hasOwnProperty.call(flat.input, key)) input[key] = resolve(flat.input[key]);
310
+ }
311
+
312
+ const integrity = () => Object.fromEntries([...raws].map(([name, raw]) => [name, snapshotTwin(raw, recorder)]));
313
+ // The mutation list is omitted when empty, so recipes without caller writes keep their pinned counters.
314
+ const snapshotCounters = () => {
315
+ const snapshot = JSON.parse(JSON.stringify(counters));
316
+ if (snapshot.mutations.length === 0) delete snapshot.mutations;
317
+ return snapshot;
318
+ };
319
+
320
+ return {
321
+ traced,
322
+ input,
323
+ recorder,
324
+ counters: snapshotCounters,
325
+ integrity,
326
+ detached: flat.detached,
327
+ prescribed: () => prescribed.slice(),
328
+ applyAfterReturn: () => {
329
+ for (const item of flat.afterReturn) {
330
+ if (item.delete) applyDelete(item.node, item.key);
331
+ else applyMutation(item.node, item.key, resolve(item.to));
332
+ }
333
+ }
334
+ };
335
+ }
336
+
337
+ // Encodes checker output without touching caller objects. Registered input references are emitted
338
+ // as `$inputRef` markers and counted as leaks: the checker must not return live input objects.
339
+ function encodeOutput(value, recorder, leaks) {
340
+ if (value === undefined) return { kind: 'undefined' };
341
+ if (value === null || typeof value !== 'object') return value;
342
+ if (recorder.idOf(value) !== undefined) {
343
+ leaks.count += 1;
344
+ return { $inputRef: recorder.idOf(value) };
345
+ }
346
+ if (Array.isArray(value)) return value.map(item => encodeOutput(item, recorder, leaks));
347
+ const encoded = {};
348
+ for (const key of Object.keys(value)) encoded[key] = encodeOutput(value[key], recorder, leaks);
349
+ return encoded;
350
+ }
351
+
352
+ function encodeException(error, recorder) {
353
+ const encoded = { thrown: describeValue(error, recorder) };
354
+ if (error !== null && typeof error === 'object' && recorder.idOf(error) === undefined) {
355
+ encoded.name = error.name;
356
+ encoded.message = error.message;
357
+ }
358
+ return encoded;
359
+ }
360
+
361
+ // Removes the keys the factory prescribed from one integrity snapshot, so only unexplained changes
362
+ // remain in the comparison.
363
+ function stripPrescribed(snapshot, prescribed) {
364
+ return Object.fromEntries(Object.entries(snapshot).map(([name, twin]) => {
365
+ const mutated = new Set(prescribed.filter(item => item.node === name).map(item => item.key));
366
+ return [name, { ...twin, keys: twin.keys.filter(entry => !(typeof entry.key === 'string' && mutated.has(entry.key))) }];
367
+ }));
368
+ }
369
+
370
+ function changedTwins(before, after) {
371
+ return Object.keys(before).filter(name => JSON.stringify(before[name]) !== JSON.stringify(after[name]));
372
+ }
373
+
374
+ // One checker invocation inside the recorded window. Integrity snapshots are taken on either side
375
+ // without Proxies, and prescribed keys are stripped before comparison.
376
+ function invoke(checker, built) {
377
+ const before = built.integrity();
378
+ built.recorder.begin();
379
+ let output = null;
380
+ let exception = null;
381
+ try {
382
+ output = checker(built.input);
383
+ } catch (error) {
384
+ exception = encodeException(error, built.recorder);
385
+ }
386
+ const events = built.recorder.end();
387
+ const after = built.integrity();
388
+ const prescribed = built.prescribed();
389
+ const leaks = { count: 0 };
390
+ const encodedOutput = output === null ? null : encodeOutput(output, built.recorder, leaks);
391
+ return {
392
+ output,
393
+ encodedOutput,
394
+ exception,
395
+ events,
396
+ leaks: leaks.count,
397
+ integrityChangedTwins: changedTwins(stripPrescribed(before, prescribed), stripPrescribed(after, prescribed))
398
+ };
399
+ }
400
+
401
+ // Runs one checker over a built case. For detached recipes the factory then applies its post-return
402
+ // caller writes and invokes the checker again over the mutated graph.
403
+ function runChecker(checker, built) {
404
+ const first = invoke(checker, built);
405
+ const result = {
406
+ output: first.encodedOutput,
407
+ exception: first.exception,
408
+ events: first.events,
409
+ counters: built.counters(),
410
+ integrityChangedTwins: first.integrityChangedTwins,
411
+ aliasLeaks: first.leaks,
412
+ unregisteredReferences: built.recorder.stats.unregisteredReferences,
413
+ postReturn: null
414
+ };
415
+ if (!built.detached) return result;
416
+
417
+ built.applyAfterReturn();
418
+ const reencoded = first.output === null ? null : encodeOutput(first.output, built.recorder, { count: 0 });
419
+ const beforeSecond = built.counters();
420
+ const second = invoke(checker, built);
421
+ result.postReturn = {
422
+ outputUnchangedAfterMutation: JSON.stringify(reencoded) === JSON.stringify(first.encodedOutput),
423
+ secondOutput: second.encodedOutput,
424
+ secondException: second.exception,
425
+ secondEvents: second.events,
426
+ secondIntegrityChangedTwins: second.integrityChangedTwins,
427
+ secondAliasLeaks: second.leaks,
428
+ secondCounters: counterWindow(beforeSecond, built.counters())
429
+ };
430
+ return result;
431
+ }
432
+
433
+ // Counters for one invocation window: the difference between two cumulative snapshots. Accessor reads and
434
+ // receivers, trap calls and caller writes are per window. Accessor sequence positions are not reset, so the
435
+ // second window continues the recipe's outcome sequence exactly as before.
436
+ function counterWindow(before, after) {
437
+ const accessors = {};
438
+ for (const [path, entry] of Object.entries(after.accessors)) {
439
+ const prior = before.accessors[path] || { reads: 0, receivers: [] };
440
+ accessors[path] = { reads: entry.reads - prior.reads, receivers: entry.receivers.slice(prior.receivers.length) };
441
+ }
442
+ const traps = {};
443
+ for (const [name, entry] of Object.entries(after.traps)) {
444
+ traps[name] = { calls: entry.calls - (before.traps[name] ? before.traps[name].calls : 0) };
445
+ }
446
+ const window = { accessors, traps };
447
+ const mutations = (after.mutations || []).slice((before.mutations || []).length);
448
+ if (mutations.length > 0) window.mutations = mutations;
449
+ return window;
450
+ }
451
+
452
+ module.exports = {
453
+ ROOTS,
454
+ buildCase,
455
+ runChecker,
456
+ normalizeRecipe,
457
+ encodeKey,
458
+ describeDescriptor
459
+ };
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ // #341 Phase 0A: invariant-aware tracing for the read-boundary corpus.
4
+ //
5
+ // Every traced caller object is a Proxy over a factory-owned twin. The twin holds the real
6
+ // property state, including the traced children that data slots point at, so frozen,
7
+ // non-configurable and null-prototype invariants hold while each reachable access is recorded.
8
+ // Recording is active only inside the checker window. Integrity inspection reads the twins
9
+ // directly, without a Proxy, so neither setup nor inspection contaminates the checker trace.
10
+
11
+ const TRAP_NAMES = Object.freeze([
12
+ 'get', 'set', 'has', 'deleteProperty', 'defineProperty', 'getOwnPropertyDescriptor',
13
+ 'ownKeys', 'getPrototypeOf', 'setPrototypeOf', 'isExtensible', 'preventExtensions'
14
+ ]);
15
+
16
+ const KEYED_TRAPS = new Set(['get', 'set', 'has', 'deleteProperty', 'defineProperty', 'getOwnPropertyDescriptor']);
17
+
18
+ // Realm intrinsics the checker can reach through a caller object's prototype chain (for example
19
+ // `obligations.filter`, or `constructor` during Array species lookup). They are named once, so a
20
+ // trace identifies them instead of reporting an anonymous function.
21
+ const INTRINSIC_IDS = new Map();
22
+ for (const [label, value] of [
23
+ ['Array', Array],
24
+ ['Array.prototype', Array.prototype],
25
+ ['Object', Object],
26
+ ['Object.prototype', Object.prototype],
27
+ ['Function.prototype', Function.prototype],
28
+ ['Map.prototype', Map.prototype],
29
+ ['Date.prototype', Date.prototype],
30
+ ['RegExp.prototype', RegExp.prototype]
31
+ ]) {
32
+ INTRINSIC_IDS.set(value, `intrinsic:${label}`);
33
+ for (const key of Object.getOwnPropertyNames(value)) {
34
+ const member = Object.getOwnPropertyDescriptor(value, key);
35
+ if (member && typeof member.value === 'function') INTRINSIC_IDS.set(member.value, `intrinsic:${label}.${key}`);
36
+ }
37
+ }
38
+
39
+ function createRecorder() {
40
+ const ids = new Map(INTRINSIC_IDS);
41
+ const events = [];
42
+ const stats = { unregisteredReferences: 0 };
43
+ let active = false;
44
+ return {
45
+ events,
46
+ stats,
47
+ get active() {
48
+ return active;
49
+ },
50
+ begin() {
51
+ // Each window starts empty, so a later invocation's trace never includes an earlier invocation's events.
52
+ events.length = 0;
53
+ active = true;
54
+ },
55
+ end() {
56
+ active = false;
57
+ return events.slice();
58
+ },
59
+ register(value, id) {
60
+ ids.set(value, id);
61
+ return value;
62
+ },
63
+ idOf(value) {
64
+ return ids.get(value);
65
+ }
66
+ };
67
+ }
68
+
69
+ // Symbols are identified by description. The factory guarantees descriptions are unique.
70
+ function encodeKey(key) {
71
+ return typeof key === 'symbol' ? { symbol: key.description } : key;
72
+ }
73
+
74
+ function describeValue(value, recorder) {
75
+ if (value === undefined) return { kind: 'undefined' };
76
+ if (value === null) return { kind: 'null' };
77
+ const type = typeof value;
78
+ if (type === 'string') return { kind: 'primitive', type, value };
79
+ if (type !== 'object' && type !== 'function') return { kind: 'primitive', type, value: String(value) };
80
+ const id = recorder.idOf(value);
81
+ if (id === undefined) {
82
+ recorder.stats.unregisteredReferences += 1;
83
+ return { kind: type === 'function' ? 'function' : 'object', id: '<unregistered>' };
84
+ }
85
+ return { kind: type === 'function' ? 'function' : 'object', id };
86
+ }
87
+
88
+ function describeDescriptor(descriptor, recorder) {
89
+ if (descriptor === undefined) return { kind: 'undefined' };
90
+ const encoded = {};
91
+ for (const field of ['value', 'writable', 'get', 'set', 'enumerable', 'configurable']) {
92
+ if (!Object.prototype.hasOwnProperty.call(descriptor, field)) continue;
93
+ const value = descriptor[field];
94
+ encoded[field] = field === 'value' || field === 'get' || field === 'set'
95
+ ? describeValue(value, recorder)
96
+ : value;
97
+ }
98
+ return encoded;
99
+ }
100
+
101
+ function encodeResult(trap, result, recorder) {
102
+ switch (trap) {
103
+ case 'get':
104
+ case 'getPrototypeOf':
105
+ return describeValue(result, recorder);
106
+ case 'getOwnPropertyDescriptor':
107
+ return describeDescriptor(result, recorder);
108
+ case 'ownKeys':
109
+ return result.map(encodeKey);
110
+ default:
111
+ return result;
112
+ }
113
+ }
114
+
115
+ // Records one trap invocation in pre-order (the event is reserved before the operation runs), so
116
+ // nested accesses made by getters and by the checker itself keep their call order.
117
+ function observe(recorder, id, trap, args, run) {
118
+ if (!recorder.active) return run();
119
+ const event = { seq: recorder.events.length, op: trap, id };
120
+ if (KEYED_TRAPS.has(trap)) event.key = encodeKey(args[0]);
121
+ if (trap === 'set' || trap === 'defineProperty') event.input = describeValue(args[1], recorder);
122
+ recorder.events.push(event);
123
+ try {
124
+ const result = run();
125
+ event.result = encodeResult(trap, result, recorder);
126
+ return result;
127
+ } catch (error) {
128
+ event.thrown = describeValue(error, recorder);
129
+ throw error;
130
+ }
131
+ }
132
+
133
+ // `overrides` replace individual traps (used by factory-owned hostile Proxies). Every other trap
134
+ // forwards to the twin, so the Proxy never reports a value the twin would contradict.
135
+ function traceProxy(recorder, id, twin, overrides = {}) {
136
+ const handler = {};
137
+ for (const trap of TRAP_NAMES) {
138
+ handler[trap] = (target, ...args) => observe(recorder, id, trap, args, () => (
139
+ overrides[trap]
140
+ ? overrides[trap](target, ...args)
141
+ : Reflect[trap](target, ...args)
142
+ ));
143
+ }
144
+ return recorder.register(new Proxy(twin, handler), id);
145
+ }
146
+
147
+ // Structural snapshot of a twin. Reads are Reflect-based on the raw twin, so no trace event can be
148
+ // produced and no factory getter is invoked.
149
+ function snapshotTwin(twin, recorder) {
150
+ return {
151
+ isArray: Array.isArray(twin),
152
+ extensible: Reflect.isExtensible(twin),
153
+ frozen: Object.isFrozen(twin),
154
+ prototype: describeValue(Reflect.getPrototypeOf(twin), recorder),
155
+ keys: Reflect.ownKeys(twin).map(key => ({
156
+ key: encodeKey(key),
157
+ descriptor: describeDescriptor(Reflect.getOwnPropertyDescriptor(twin, key), recorder)
158
+ }))
159
+ };
160
+ }
161
+
162
+ module.exports = {
163
+ TRAP_NAMES,
164
+ createRecorder,
165
+ describeValue,
166
+ describeDescriptor,
167
+ encodeKey,
168
+ observe,
169
+ snapshotTwin,
170
+ traceProxy
171
+ };