@gamaze/hicortex 0.23.1 → 0.24.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/assets/dashboard.html +64 -13
- package/dist/calibration.d.ts +31 -0
- package/dist/calibration.js +38 -1
- package/dist/capture.d.ts +7 -0
- package/dist/capture.js +10 -1
- package/dist/dashboard.d.ts +32 -7
- package/dist/dashboard.js +50 -10
- package/dist/db.js +45 -0
- package/dist/dedup.js +2 -2
- package/dist/distill-queue.d.ts +203 -0
- package/dist/distill-queue.js +440 -0
- package/dist/health.d.ts +13 -1
- package/dist/health.js +6 -1
- package/dist/hosted-boot.d.ts +1 -1
- package/dist/index.js +17 -2
- package/dist/learnings-identity.js +20 -1
- package/dist/localhost-bypass.js +1 -1
- package/dist/mcp-server.js +92 -7
- package/dist/nightly.js +88 -1
- package/dist/nofit.d.ts +1 -1
- package/dist/nofit.js +1 -1
- package/dist/schema-prototypes.d.ts +1 -1
- package/dist/schema-prototypes.js +1 -1
- package/dist/status.d.ts +11 -0
- package/dist/status.js +25 -0
- package/dist/types.d.ts +20 -0
- package/hermes-plugin/hicortex/README.md +13 -6
- package/hermes-plugin/hicortex/__init__.py +7 -0
- package/hermes-plugin/hicortex/client.py +64 -2
- package/hermes-plugin/hicortex/plugin.yaml +1 -1
- package/hermes-plugin/hicortex/provider.py +24 -1
- package/opencode-plugin/hicortex/index.ts +24 -1
- package/package.json +2 -1
- package/pi-extension/hicortex/index.ts +24 -1
- package/server.json +2 -2
- package/dist/eval/decay-eval.d.ts +0 -111
- package/dist/eval/decay-eval.js +0 -214
- package/dist/eval/dups.d.ts +0 -100
- package/dist/eval/dups.js +0 -174
- package/dist/eval/eval-clock.d.ts +0 -32
- package/dist/eval/eval-clock.js +0 -47
- package/dist/eval/eval-db.d.ts +0 -25
- package/dist/eval/eval-db.js +0 -67
- package/dist/eval/graph-eval.d.ts +0 -89
- package/dist/eval/graph-eval.js +0 -246
- package/dist/eval/importance-eval.d.ts +0 -85
- package/dist/eval/importance-eval.js +0 -286
- package/dist/eval/planted-eval.d.ts +0 -30
- package/dist/eval/planted-eval.js +0 -122
- package/dist/eval/planted-fixtures.d.ts +0 -107
- package/dist/eval/planted-fixtures.js +0 -283
- package/dist/eval/planted-harness.d.ts +0 -183
- package/dist/eval/planted-harness.js +0 -651
- package/dist/eval/ranking-battery.d.ts +0 -125
- package/dist/eval/ranking-battery.js +0 -289
- package/dist/eval/ranking-eval.d.ts +0 -61
- package/dist/eval/ranking-eval.js +0 -554
- package/dist/eval/ranking-fixtures.d.ts +0 -117
- package/dist/eval/ranking-fixtures.js +0 -485
- package/dist/eval/recall-sweep.d.ts +0 -87
- package/dist/eval/recall-sweep.js +0 -1030
- package/dist/eval/reflection-census.d.ts +0 -19
- package/dist/eval/reflection-census.js +0 -25
- package/dist/eval/relevance-eval.d.ts +0 -178
- package/dist/eval/relevance-eval.js +0 -2240
- package/dist/eval/run-eval.d.ts +0 -20
- package/dist/eval/run-eval.js +0 -299
|
@@ -1,651 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* The planted-pairs acceptance gate (#393 increment A) — machinery shared by
|
|
4
|
-
* the CLI runner (planted-eval.ts, REAL embedder + real-text corpus) and the
|
|
5
|
-
* vitest suite (tests/planted-pairs.test.ts, controlled synthetic vectors).
|
|
6
|
-
*
|
|
7
|
-
* What it measures, per planted pair, before/after running the REAL
|
|
8
|
-
* production resolution stage (`stageReconsolidation`, which internally runs
|
|
9
|
-
* the deterministic merge zone LAST — guard-C: judgment outranks the sweep):
|
|
10
|
-
*
|
|
11
|
-
* DETECTED — the pair entered the resolution machinery as a candidate: a
|
|
12
|
-
* verdict call was made on it (judged band, logged by the ground-truth
|
|
13
|
-
* judge) OR it co-clustered in the pre-stage `planDedup` discovery at the
|
|
14
|
-
* zone ceiling. Tagged by source ("judge" = KNN/similarity, "scout" = the
|
|
15
|
-
* #393 B reference-extraction source — attribution rule: a verdict call
|
|
16
|
-
* on a below-floor pair can only be scout-sourced, "zone" = the
|
|
17
|
-
* deterministic ceiling).
|
|
18
|
-
* BOUND — a resolution structure reflecting the ground truth now connects
|
|
19
|
-
* the pair: `corrected_by` / `superseded_by` / `conflicts` link, or a
|
|
20
|
-
* merge (`dedup_log` row). Guard-C added the `conflicts` bind (the judge
|
|
21
|
-
* flags a genuine conflict instead of leaving it inexpressible).
|
|
22
|
-
* RESOLVED — the class-specific desired END STATE holds in the store:
|
|
23
|
-
* corrects = correction effective (older demoted/absorbed/rewritten) and
|
|
24
|
-
* the correction information live;
|
|
25
|
-
* supersedes= older demoted-or-absorbed and newer live in recall;
|
|
26
|
-
* merge = exactly one live row remains, the other absorbed via dedup;
|
|
27
|
-
* conflict = both live AND not blended AND the conflicts link exists
|
|
28
|
-
* (guard-C's end state — the consumer sees both truths);
|
|
29
|
-
* none = both live, not blended, no resolution link between them.
|
|
30
|
-
*
|
|
31
|
-
* The version_chain CLASS end-state is measured on the RECALL surface, not
|
|
32
|
-
* the store (#393 D): a retrieval-only change cannot move a store probe, so
|
|
33
|
-
* post-stage the harness runs the production retrieve() (read-only: limit 3,
|
|
34
|
-
* below the cold-exposure threshold, noStrengthen) with the OLDEST chain
|
|
35
|
-
* member's content as query and requires the belief walk's guarantee — no
|
|
36
|
-
* superseded ancestor surfaces as a competing truth, and the linked chain's
|
|
37
|
-
* terminal surfaces. Store facts (terminal live, non-terminals gone) remain
|
|
38
|
-
* in the class note as corroboration.
|
|
39
|
-
*
|
|
40
|
-
* The judge is a GROUND-TRUTH-STUBBED LlmClient (the refine addendum's Q3
|
|
41
|
-
* recommendation): it answers each planted pair's declared relation and
|
|
42
|
-
* defaults unknown/collider pairs to `none`. This isolates the MACHINERY
|
|
43
|
-
* (detection sources, binding actions, merge guard, walk) from judge quality —
|
|
44
|
-
* the harness answers "would the pipeline resolve a KNOWN correction if the
|
|
45
|
-
* judge were perfect?". Live-LLM verdict quality is the real-corpus soak run.
|
|
46
|
-
*
|
|
47
|
-
* Read/write discipline: the gate NEVER touches ~/.hicortex — the fixture DB
|
|
48
|
-
* and the stage's stateDir are caller-supplied (temp) paths. A real snapshot
|
|
49
|
-
* is only ever PLANTED INTO A COPY (planted-eval.ts --snapshot).
|
|
50
|
-
*/
|
|
51
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
52
|
-
if (k2 === undefined) k2 = k;
|
|
53
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
54
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
55
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
56
|
-
}
|
|
57
|
-
Object.defineProperty(o, k2, desc);
|
|
58
|
-
}) : (function(o, m, k, k2) {
|
|
59
|
-
if (k2 === undefined) k2 = k;
|
|
60
|
-
o[k2] = m[k];
|
|
61
|
-
}));
|
|
62
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
63
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
64
|
-
}) : function(o, v) {
|
|
65
|
-
o["default"] = v;
|
|
66
|
-
});
|
|
67
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
68
|
-
var ownKeys = function(o) {
|
|
69
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
70
|
-
var ar = [];
|
|
71
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
72
|
-
return ar;
|
|
73
|
-
};
|
|
74
|
-
return ownKeys(o);
|
|
75
|
-
};
|
|
76
|
-
return function (mod) {
|
|
77
|
-
if (mod && mod.__esModule) return mod;
|
|
78
|
-
var result = {};
|
|
79
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
80
|
-
__setModuleDefault(result, mod);
|
|
81
|
-
return result;
|
|
82
|
-
};
|
|
83
|
-
})();
|
|
84
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
85
|
-
exports.plantCorpus = plantCorpus;
|
|
86
|
-
exports.makeGroundTruthJudge = makeGroundTruthJudge;
|
|
87
|
-
exports.runPlantedGate = runPlantedGate;
|
|
88
|
-
exports.renderPlantedReport = renderPlantedReport;
|
|
89
|
-
const node_fs_1 = require("node:fs");
|
|
90
|
-
const retrieval_js_1 = require("../retrieval.js");
|
|
91
|
-
const db_js_1 = require("../db.js");
|
|
92
|
-
const storage = __importStar(require("../storage.js"));
|
|
93
|
-
const consolidate_js_1 = require("../consolidate.js");
|
|
94
|
-
const reconsolidation_js_1 = require("../reconsolidation.js");
|
|
95
|
-
const dedup_js_1 = require("../dedup.js");
|
|
96
|
-
const graph_eval_js_1 = require("./graph-eval.js");
|
|
97
|
-
const planted_fixtures_js_1 = require("./planted-fixtures.js");
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
// Fixture DB builder
|
|
100
|
-
// ---------------------------------------------------------------------------
|
|
101
|
-
/** Insert the corpus into a writable DB (fresh, or a copy of a snapshot). */
|
|
102
|
-
async function plantCorpus(db, corpus, embedFn) {
|
|
103
|
-
const idByKey = new Map();
|
|
104
|
-
for (const row of corpus.rows) {
|
|
105
|
-
const embedding = await embedFn(row.content);
|
|
106
|
-
const id = storage.insertMemory(db, row.content, embedding, {
|
|
107
|
-
sourceAgent: planted_fixtures_js_1.PLANTED_SOURCE_AGENT,
|
|
108
|
-
project: planted_fixtures_js_1.PLANTED_PROJECT,
|
|
109
|
-
memoryType: row.memoryType,
|
|
110
|
-
createdAt: row.createdAt,
|
|
111
|
-
});
|
|
112
|
-
idByKey.set(row.key, id);
|
|
113
|
-
}
|
|
114
|
-
return idByKey;
|
|
115
|
-
}
|
|
116
|
-
const VERDICT_PROMPT_RE = /OLDER MEMORY:\n([\s\S]*?)\n\nNEWER MEMORY:\n([\s\S]*?)\n\nHow does the NEWER/;
|
|
117
|
-
const REWRITE_PROMPT_RE = /OLDER MEMORY \(currently stored[^)]*\):\n([\s\S]*?)\n\nCORRECTING MEMORIES \(newer[^)]*\):\n([\s\S]*?)\n\nCompose/;
|
|
118
|
-
const SCOUT_PROMPT_RE = /You are scanning a memory that was just added[\s\S]*?MEMORY:\n([\s\S]*?)\n\nDoes this memory correct/;
|
|
119
|
-
/**
|
|
120
|
-
* Deterministic, ground-truth-keyed judge. Parses each prompt back into its
|
|
121
|
-
* (older, newer) contents — the exact texts `buildCorrectionVerdictPrompt` /
|
|
122
|
-
* `buildRewritePrompt` embed — and answers from the corpus's declared
|
|
123
|
-
* relations:
|
|
124
|
-
* corrects -> {"action":"corrects","confidence":0.95} (above the gate)
|
|
125
|
-
* supersedes-> {"action":"supersedes","confidence":0.9}
|
|
126
|
-
* merge -> {"action":"merge","confidence":0.95} (above the gate)
|
|
127
|
-
* conflict -> {"action":"conflicts","confidence":0.9} (guard-C: flag,
|
|
128
|
-
* both live, never blended)
|
|
129
|
-
* none -> {"action":"none","confidence":0.9}
|
|
130
|
-
* Unknown pairs (colliders, filler cross-pairs) default to none.
|
|
131
|
-
*
|
|
132
|
-
* #393 B: the judge also answers the scout's per-memory shape calls — a
|
|
133
|
-
* memory is correction-shaped iff it is the newer side of a declared
|
|
134
|
-
* corrects/supersedes/conflict pair (declared `references` returned verbatim,
|
|
135
|
-
* throwing if a resolution-shaped pair never declared them; guard-C added
|
|
136
|
-
* `conflict` to the shaped set — two records disagreeing on one quantity
|
|
137
|
-
* share their wording); everything else answers correction=false. Shape calls
|
|
138
|
-
* are logged in scoutCalls.
|
|
139
|
-
*/
|
|
140
|
-
function makeGroundTruthJudge(corpus, idByKey) {
|
|
141
|
-
const keyByContent = new Map();
|
|
142
|
-
for (const row of corpus.rows)
|
|
143
|
-
keyByContent.set(row.content, row.key);
|
|
144
|
-
const pairByKeys = new Map();
|
|
145
|
-
for (const p of corpus.pairs)
|
|
146
|
-
pairByKeys.set(`${p.olderKey}|${p.newerKey}`, p);
|
|
147
|
-
const keyById = new Map([...idByKey.entries()].map(([key, id]) => [id, key]));
|
|
148
|
-
const verdictCalls = [];
|
|
149
|
-
const rewriteCalls = [];
|
|
150
|
-
const scoutCalls = [];
|
|
151
|
-
const verdictReply = (p) => {
|
|
152
|
-
switch (p.relation) {
|
|
153
|
-
case "corrects":
|
|
154
|
-
return '{"action": "corrects", "confidence": 0.95}';
|
|
155
|
-
case "supersedes":
|
|
156
|
-
return '{"action": "supersedes", "confidence": 0.9}';
|
|
157
|
-
case "merge":
|
|
158
|
-
return '{"action": "merge", "confidence": 0.95}';
|
|
159
|
-
case "conflict":
|
|
160
|
-
return '{"action": "conflicts", "confidence": 0.9}';
|
|
161
|
-
default:
|
|
162
|
-
return '{"action": "none", "confidence": 0.9}';
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
/**
|
|
166
|
-
* Ground truth for the scout's shape question: a memory is correction-shaped
|
|
167
|
-
* iff it is the NEWER side of a declared `corrects`/`supersedes`/`conflict`
|
|
168
|
-
* pair (guard-C added `conflict` — two records disagreeing on one quantity
|
|
169
|
-
* share their wording, so the scout's reference extraction finds the old
|
|
170
|
-
* record exactly like a correction does). `merge`/`none` are
|
|
171
|
-
* duplicates/additions, not resolutions.
|
|
172
|
-
*/
|
|
173
|
-
const shapeFor = (key) => {
|
|
174
|
-
const shaped = corpus.pairs.find((p) => p.newerKey === key &&
|
|
175
|
-
(p.relation === "corrects" || p.relation === "supersedes" || p.relation === "conflict"));
|
|
176
|
-
if (!shaped)
|
|
177
|
-
return { correction: false, references: "" };
|
|
178
|
-
if (!shaped.references) {
|
|
179
|
-
throw new Error(`planted-harness: shape call on ${key} (relation ${shaped.relation}) has no declared ` +
|
|
180
|
-
`references — every corrects/supersedes/conflict pair must declare the old claim's terms`);
|
|
181
|
-
}
|
|
182
|
-
return { correction: true, references: shaped.references };
|
|
183
|
-
};
|
|
184
|
-
const obj = {
|
|
185
|
-
verdictCalls,
|
|
186
|
-
rewriteCalls,
|
|
187
|
-
scoutCalls,
|
|
188
|
-
complete: async (prompt) => {
|
|
189
|
-
const scoutMatch = SCOUT_PROMPT_RE.exec(prompt);
|
|
190
|
-
if (scoutMatch) {
|
|
191
|
-
const key = keyByContent.get(scoutMatch[1]) ?? "?";
|
|
192
|
-
const shape = key === "?" ? { correction: false, references: "" } : shapeFor(key);
|
|
193
|
-
scoutCalls.push({ key, correction: shape.correction });
|
|
194
|
-
return {
|
|
195
|
-
text: JSON.stringify({
|
|
196
|
-
correction: shape.correction,
|
|
197
|
-
references: shape.references,
|
|
198
|
-
confidence: 0.9,
|
|
199
|
-
}),
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
const verdictMatch = VERDICT_PROMPT_RE.exec(prompt);
|
|
203
|
-
if (verdictMatch) {
|
|
204
|
-
const olderKey = keyByContent.get(verdictMatch[1]);
|
|
205
|
-
const newerKey = keyByContent.get(verdictMatch[2]);
|
|
206
|
-
const pair = olderKey !== undefined && newerKey !== undefined
|
|
207
|
-
? pairByKeys.get(`${olderKey}|${newerKey}`)
|
|
208
|
-
: undefined;
|
|
209
|
-
verdictCalls.push({
|
|
210
|
-
olderKey: olderKey ?? "?",
|
|
211
|
-
newerKey: newerKey ?? "?",
|
|
212
|
-
grounded: pair !== undefined,
|
|
213
|
-
});
|
|
214
|
-
return { text: pair ? verdictReply(pair) : '{"action": "none", "confidence": 0.9}' };
|
|
215
|
-
}
|
|
216
|
-
const rewriteMatch = REWRITE_PROMPT_RE.exec(prompt);
|
|
217
|
-
if (rewriteMatch) {
|
|
218
|
-
const targetKey = keyByContent.get(rewriteMatch[1]);
|
|
219
|
-
const triggerIds = [...rewriteMatch[2].matchAll(/\[([0-9a-f-]{36})\]/g)].map((m) => m[1]);
|
|
220
|
-
rewriteCalls.push({
|
|
221
|
-
targetKey: targetKey ?? "?",
|
|
222
|
-
triggerKeys: triggerIds.map((id) => keyById.get(id) ?? "?"),
|
|
223
|
-
});
|
|
224
|
-
// The scripted rewrite: the declared `rewritten` of the corrects pair
|
|
225
|
-
// targeting this row, with every trigger absorbed (the correction
|
|
226
|
-
// rider is transition-only — the standard disposition).
|
|
227
|
-
const pair = corpus.pairs.find((p) => p.relation === "corrects" && p.olderKey === targetKey && p.rewritten !== undefined);
|
|
228
|
-
if (!pair || !pair.rewritten) {
|
|
229
|
-
throw new Error(`planted-harness: rewrite prompt for ${targetKey ?? "?"} has no scripted rewritten text — ` +
|
|
230
|
-
`every corrects pair must declare one`);
|
|
231
|
-
}
|
|
232
|
-
return {
|
|
233
|
-
text: JSON.stringify({
|
|
234
|
-
rewritten: pair.rewritten,
|
|
235
|
-
triggers: triggerIds.map((id) => ({ id, disposition: "absorb" })),
|
|
236
|
-
}),
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
throw new Error(`planted-harness: unrecognized LLM prompt (neither verdict nor rewrite):\n${prompt.slice(0, 200)}`);
|
|
240
|
-
},
|
|
241
|
-
};
|
|
242
|
-
return { llm: obj, verdictCalls, rewriteCalls, scoutCalls };
|
|
243
|
-
}
|
|
244
|
-
function probe(db, id) {
|
|
245
|
-
const row = db
|
|
246
|
-
.prepare("SELECT status, content FROM memories WHERE id = ?")
|
|
247
|
-
.get(id);
|
|
248
|
-
if (!row)
|
|
249
|
-
return null;
|
|
250
|
-
const hasVector = !!db.prepare("SELECT 1 FROM memory_vectors WHERE id = ?").get(id);
|
|
251
|
-
return { status: row.status, content: row.content, hasVector };
|
|
252
|
-
}
|
|
253
|
-
/** Absorbed rows are structurally invisible (no vector row). */
|
|
254
|
-
function absorbed(p) {
|
|
255
|
-
return p === null || p.status === "absorbed" || !p.hasVector;
|
|
256
|
-
}
|
|
257
|
-
/** Live in recall: not absorbed (superseded/retracted rows remain visible, demoted). */
|
|
258
|
-
function liveInRecall(p) {
|
|
259
|
-
return p !== null && p.status !== "absorbed" && p.hasVector;
|
|
260
|
-
}
|
|
261
|
-
/** Demoted (ranking-penalty status) or fully out of recall. */
|
|
262
|
-
function demotedOrGone(p) {
|
|
263
|
-
return p === null || p.status === "superseded" || p.status === "retracted" || absorbed(p);
|
|
264
|
-
}
|
|
265
|
-
function linkBetween(db, a, b, relationships) {
|
|
266
|
-
if (relationships.length === 0)
|
|
267
|
-
return null;
|
|
268
|
-
const ph = relationships.map(() => "?").join(",");
|
|
269
|
-
const row = db
|
|
270
|
-
.prepare(`SELECT source_id, target_id, relationship FROM memory_links
|
|
271
|
-
WHERE relationship IN (${ph})
|
|
272
|
-
AND ((source_id = ? AND target_id = ?) OR (source_id = ? AND target_id = ?))`)
|
|
273
|
-
.get(...relationships, a, b, b, a);
|
|
274
|
-
if (!row)
|
|
275
|
-
return null;
|
|
276
|
-
return {
|
|
277
|
-
relationship: row.relationship,
|
|
278
|
-
direction: row.source_id === a ? "a_to_b" : "b_to_a",
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
function dedupLogBetween(db, a, b) {
|
|
282
|
-
const row = db
|
|
283
|
-
.prepare(`SELECT loser_id, canonical_id FROM dedup_log
|
|
284
|
-
WHERE (loser_id = ? AND canonical_id = ?) OR (loser_id = ? AND canonical_id = ?)`)
|
|
285
|
-
.get(a, b, b, a);
|
|
286
|
-
if (!row)
|
|
287
|
-
return null;
|
|
288
|
-
return { loserKeyIsA: row.loser_id === a };
|
|
289
|
-
}
|
|
290
|
-
const alwaysAcquire = async () => () => { };
|
|
291
|
-
/** Canonical map key for an unordered row pair. */
|
|
292
|
-
function pairKey(a, b) {
|
|
293
|
-
return [a, b].sort().join("|");
|
|
294
|
-
}
|
|
295
|
-
/** All rows' measured pairwise cosines (planted corpus only). */
|
|
296
|
-
function measuredCosines(db, corpus, idByKey) {
|
|
297
|
-
const out = new Map();
|
|
298
|
-
for (let i = 0; i < corpus.rows.length; i++) {
|
|
299
|
-
for (let j = i + 1; j < corpus.rows.length; j++) {
|
|
300
|
-
const a = corpus.rows[i];
|
|
301
|
-
const b = corpus.rows[j];
|
|
302
|
-
const va = storage.getStoredEmbedding(db, idByKey.get(a.key) ?? "");
|
|
303
|
-
const vb = storage.getStoredEmbedding(db, idByKey.get(b.key) ?? "");
|
|
304
|
-
out.set(pairKey(a.key, b.key), va && vb ? (0, graph_eval_js_1.cosineBetween)(va, vb) : Number.NaN);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return out;
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Build (or plant into) the DB, run the production stage, measure the gate.
|
|
311
|
-
* This is the ONE orchestration shared by the CLI runner and the vitest suite.
|
|
312
|
-
*/
|
|
313
|
-
async function runPlantedGate(corpus, opts) {
|
|
314
|
-
(0, node_fs_1.mkdirSync)(opts.stateDir, { recursive: true });
|
|
315
|
-
const db = (0, db_js_1.initDb)(opts.dbPath);
|
|
316
|
-
try {
|
|
317
|
-
const idByKey = await plantCorpus(db, corpus, opts.embedFn);
|
|
318
|
-
const keyById = new Map([...idByKey.entries()].map(([k, v]) => [v, k]));
|
|
319
|
-
// --- pre-stage zone discovery snapshot (detection + blend prediction) ---
|
|
320
|
-
const threshold = opts.threshold ?? dedup_js_1.DEFAULT_DEDUP_MERGE_THRESHOLD;
|
|
321
|
-
const zonePlanBefore = (0, dedup_js_1.planDedup)(db, threshold);
|
|
322
|
-
const zoneClusterKeys = zonePlanBefore.mergePlans.map((p) => {
|
|
323
|
-
const keys = [...p.membersOldestFirst.map((m) => m.id), ...p.losers.map((l) => l.id)]
|
|
324
|
-
.map((id) => keyById.get(id))
|
|
325
|
-
.filter((k) => k !== undefined);
|
|
326
|
-
return new Set(keys);
|
|
327
|
-
});
|
|
328
|
-
const mismatchClusterKeys = zonePlanBefore.mismatchSkipped.map((p) => {
|
|
329
|
-
const keys = p.memberIds.map((id) => keyById.get(id)).filter((k) => k !== undefined);
|
|
330
|
-
return new Set(keys);
|
|
331
|
-
});
|
|
332
|
-
const blendPredictions = [];
|
|
333
|
-
for (const p of zonePlanBefore.mergePlans) {
|
|
334
|
-
const canonicalKey = keyById.get(p.canonical.id);
|
|
335
|
-
const loserKeys = p.losers.map((l) => keyById.get(l.id)).filter((k) => !!k);
|
|
336
|
-
if (!canonicalKey)
|
|
337
|
-
continue;
|
|
338
|
-
for (const loserKey of loserKeys) {
|
|
339
|
-
blendPredictions.push({ pair: `${loserKey}+${canonicalKey}`, canonicalKey, loserKey });
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
const cosines = measuredCosines(db, corpus, idByKey);
|
|
343
|
-
const declaredPairs = new Set(corpus.pairs.map((p) => pairKey(p.olderKey, p.newerKey)));
|
|
344
|
-
// --- run the production stage ---
|
|
345
|
-
const judge = makeGroundTruthJudge(corpus, idByKey);
|
|
346
|
-
const budget = new consolidate_js_1.BudgetTracker(1_000_000);
|
|
347
|
-
const stageReport = await (0, reconsolidation_js_1.stageReconsolidation)(db, judge.llm, budget, opts.embedFn, false, opts.stateDir, { acquireLock: opts.acquireLock ?? alwaysAcquire, autoMergeThreshold: threshold });
|
|
348
|
-
// --- measure ---
|
|
349
|
-
const inCluster = (sets, a, b) => sets.some((s) => s.has(a) && s.has(b));
|
|
350
|
-
const pairResults = corpus.pairs.map((p) => {
|
|
351
|
-
const olderId = idByKey.get(p.olderKey) ?? "";
|
|
352
|
-
const newerId = idByKey.get(p.newerKey) ?? "";
|
|
353
|
-
const cosine = cosines.get(pairKey(p.olderKey, p.newerKey)) ?? Number.NaN;
|
|
354
|
-
const inBand = Number.isFinite(cosine) && cosine >= p.band[0] && cosine <= p.band[1];
|
|
355
|
-
const judged = judge.verdictCalls.some((c) => (c.olderKey === p.olderKey && c.newerKey === p.newerKey) ||
|
|
356
|
-
(c.olderKey === p.newerKey && c.newerKey === p.olderKey));
|
|
357
|
-
const zoned = inCluster(zoneClusterKeys, p.olderKey, p.newerKey);
|
|
358
|
-
const mismatched = inCluster(mismatchClusterKeys, p.olderKey, p.newerKey);
|
|
359
|
-
// #393 B scout attribution: a verdict call on a BELOW-floor pair can
|
|
360
|
-
// only come from the scout source (the similarity floor structurally
|
|
361
|
-
// excludes sub-floor pairs before any LLM sees them).
|
|
362
|
-
const scouted = judged && Number.isFinite(cosine) && cosine < reconsolidation_js_1.DEFAULT_CORRECTION_MIN_SIMILARITY;
|
|
363
|
-
const detectionSource = scouted && zoned ? "scout+zone"
|
|
364
|
-
: scouted ? "scout"
|
|
365
|
-
: judged && zoned ? "judge+zone"
|
|
366
|
-
: judged ? "judge"
|
|
367
|
-
: zoned || mismatched ? "zone"
|
|
368
|
-
: "";
|
|
369
|
-
const olderProbe = probe(db, olderId);
|
|
370
|
-
const newerProbe = probe(db, newerId);
|
|
371
|
-
const merged = dedupLogBetween(db, olderId, newerId);
|
|
372
|
-
let bound;
|
|
373
|
-
let boundHow;
|
|
374
|
-
switch (p.relation) {
|
|
375
|
-
case "corrects": {
|
|
376
|
-
const link = linkBetween(db, olderId, newerId, ["corrected_by"]);
|
|
377
|
-
bound = link !== null;
|
|
378
|
-
boundHow = link ? `corrected_by (${link.direction === "a_to_b" ? "older→newer" : "newer→older"})` : "no corrected_by link";
|
|
379
|
-
break;
|
|
380
|
-
}
|
|
381
|
-
case "supersedes": {
|
|
382
|
-
const link = linkBetween(db, olderId, newerId, ["superseded_by"]);
|
|
383
|
-
bound = link !== null || merged !== null;
|
|
384
|
-
boundHow = link
|
|
385
|
-
? `superseded_by (${link.direction === "a_to_b" ? "older→newer" : "newer→older"})`
|
|
386
|
-
: merged
|
|
387
|
-
? "merged via dedup_log (blend, not a chain link)"
|
|
388
|
-
: "no superseded_by link";
|
|
389
|
-
break;
|
|
390
|
-
}
|
|
391
|
-
case "merge": {
|
|
392
|
-
bound = merged !== null;
|
|
393
|
-
boundHow = merged ? "merged via dedup_log" : "not merged";
|
|
394
|
-
break;
|
|
395
|
-
}
|
|
396
|
-
case "conflict": {
|
|
397
|
-
// Guard-C's bind: the `conflicts` link, either direction.
|
|
398
|
-
const link = linkBetween(db, olderId, newerId, ["conflicts"]);
|
|
399
|
-
bound = link !== null;
|
|
400
|
-
boundHow = link ? "conflicts bind" : "no conflicts link";
|
|
401
|
-
break;
|
|
402
|
-
}
|
|
403
|
-
default: {
|
|
404
|
-
bound = null;
|
|
405
|
-
boundHow = "n/a (keep-both relation)";
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
let resolved;
|
|
409
|
-
const notes = [];
|
|
410
|
-
switch (p.relation) {
|
|
411
|
-
case "corrects": {
|
|
412
|
-
const rewrittenInPlace = olderProbe !== null &&
|
|
413
|
-
olderProbe.status === "corrected" &&
|
|
414
|
-
olderProbe.content !== (corpus.rows.find((r) => r.key === p.olderKey)?.content ?? "");
|
|
415
|
-
const correctionLive = liveInRecall(newerProbe) || rewrittenInPlace;
|
|
416
|
-
resolved = (demotedOrGone(olderProbe) || rewrittenInPlace) && correctionLive;
|
|
417
|
-
if (!judged && !zoned)
|
|
418
|
-
notes.push(`cosine ${cosine.toFixed(3)} below the 0.75 floor — never a candidate`);
|
|
419
|
-
if (rewrittenInPlace)
|
|
420
|
-
notes.push("older rewritten in place (corrected)");
|
|
421
|
-
else if (demotedOrGone(olderProbe))
|
|
422
|
-
notes.push("older demoted/absorbed");
|
|
423
|
-
else
|
|
424
|
-
notes.push("older still active — wrong fact remains recallable");
|
|
425
|
-
break;
|
|
426
|
-
}
|
|
427
|
-
case "supersedes": {
|
|
428
|
-
const olderGone = demotedOrGone(olderProbe);
|
|
429
|
-
const newerLive = liveInRecall(newerProbe);
|
|
430
|
-
resolved = olderGone && newerLive;
|
|
431
|
-
if (merged) {
|
|
432
|
-
notes.push(`BLENDED by the deterministic zone (canonical=${merged.loserKeyIsA ? p.newerKey : p.olderKey}) — no supersession chain`);
|
|
433
|
-
}
|
|
434
|
-
if (!olderGone)
|
|
435
|
-
notes.push("older still recallable");
|
|
436
|
-
if (!newerLive) {
|
|
437
|
-
// Guard-C note: with the zone running last, an absorbed newer side
|
|
438
|
-
// was scanned (even judged) before the zone blended it out of
|
|
439
|
-
// recall — the parenthetical states the store fact, not the scan.
|
|
440
|
-
notes.push(`newer not live in recall${absorbed(newerProbe) ? " (absorbed)" : ""}`);
|
|
441
|
-
}
|
|
442
|
-
if (resolved && !merged)
|
|
443
|
-
notes.push("older demoted, newer live");
|
|
444
|
-
break;
|
|
445
|
-
}
|
|
446
|
-
case "merge": {
|
|
447
|
-
resolved =
|
|
448
|
-
merged !== null &&
|
|
449
|
-
(liveInRecall(olderProbe) !== liveInRecall(newerProbe)) &&
|
|
450
|
-
(absorbed(olderProbe) || absorbed(newerProbe));
|
|
451
|
-
notes.push(resolved ? "one live row remains, loser absorbed with audit row" : "pair not merged");
|
|
452
|
-
break;
|
|
453
|
-
}
|
|
454
|
-
case "conflict": {
|
|
455
|
-
const bothLive = liveInRecall(olderProbe) && liveInRecall(newerProbe);
|
|
456
|
-
const conflictMarked = bound === true;
|
|
457
|
-
resolved = bothLive && !merged && conflictMarked;
|
|
458
|
-
if (merged) {
|
|
459
|
-
notes.push(`HARM: blended by the deterministic zone (canonical=${merged.loserKeyIsA ? p.newerKey : p.olderKey}) — two live true records destroyed into one`);
|
|
460
|
-
}
|
|
461
|
-
else if (conflictMarked && bothLive) {
|
|
462
|
-
notes.push("both live, conflict-flagged, no blend — the consumer sees both truths");
|
|
463
|
-
}
|
|
464
|
-
else if (bothLive) {
|
|
465
|
-
notes.push("both live but unflagged — silent coexistence, consumer cannot see the conflict");
|
|
466
|
-
}
|
|
467
|
-
break;
|
|
468
|
-
}
|
|
469
|
-
default: {
|
|
470
|
-
// none: keep both, untouched
|
|
471
|
-
const bothLive = liveInRecall(olderProbe) && liveInRecall(newerProbe);
|
|
472
|
-
const linked = linkBetween(db, olderId, newerId, ["corrected_by", "superseded_by", "conflicts"]);
|
|
473
|
-
resolved = bothLive && !merged && !linked;
|
|
474
|
-
notes.push(resolved ? "both kept live, no link, not merged" : "unexpectedly bound or missing");
|
|
475
|
-
break;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
if (mismatched && !zoned)
|
|
479
|
-
notes.push("zone cluster refused on metadata mismatch (detected, not merged)");
|
|
480
|
-
if (!inBand)
|
|
481
|
-
notes.push(`OUT-OF-BAND cosine ${cosine.toFixed(3)} vs declared [${p.band[0]}, ${p.band[1]}]`);
|
|
482
|
-
return {
|
|
483
|
-
class: p.class,
|
|
484
|
-
olderKey: p.olderKey,
|
|
485
|
-
newerKey: p.newerKey,
|
|
486
|
-
relation: p.relation,
|
|
487
|
-
cosine,
|
|
488
|
-
inBand,
|
|
489
|
-
detected: detectionSource !== "",
|
|
490
|
-
detectionSource,
|
|
491
|
-
bound,
|
|
492
|
-
boundHow,
|
|
493
|
-
resolved,
|
|
494
|
-
notes: notes.join("; "),
|
|
495
|
-
};
|
|
496
|
-
});
|
|
497
|
-
// --- class aggregates ---
|
|
498
|
-
const byClass = new Map();
|
|
499
|
-
for (const r of pairResults) {
|
|
500
|
-
const arr = byClass.get(r.class) ?? [];
|
|
501
|
-
arr.push(r);
|
|
502
|
-
byClass.set(r.class, arr);
|
|
503
|
-
}
|
|
504
|
-
const classResults = [];
|
|
505
|
-
for (const [cls, rows] of byClass) {
|
|
506
|
-
let classResolved;
|
|
507
|
-
let notes;
|
|
508
|
-
if (cls === "version_chain") {
|
|
509
|
-
const members = (0, planted_fixtures_js_1.chainMembers)(corpus);
|
|
510
|
-
const terminal = members[members.length - 1];
|
|
511
|
-
const terminalLive = liveInRecall(probe(db, idByKey.get(terminal) ?? ""));
|
|
512
|
-
const nonTerminalsGone = members
|
|
513
|
-
.slice(0, -1)
|
|
514
|
-
.every((k) => demotedOrGone(probe(db, idByKey.get(k) ?? "")));
|
|
515
|
-
// #393 D: the class end-state is measured on the RECALL surface —
|
|
516
|
-
// the production retrieve(), read-only. limit 3 is deliberately
|
|
517
|
-
// below the cold-exposure threshold (limit >= 4) and noStrengthen
|
|
518
|
-
// keeps the probe side-effect-free, so it cannot perturb anything
|
|
519
|
-
// downstream in this report.
|
|
520
|
-
const queryContent = corpus.rows.find((r) => r.key === members[0])?.content ?? "";
|
|
521
|
-
const recall = await (0, retrieval_js_1.retrieve)(db, opts.embedFn, queryContent, {
|
|
522
|
-
limit: 3,
|
|
523
|
-
noStrengthen: true,
|
|
524
|
-
...(opts.now ? { now: opts.now } : {}), // #458: pinned clock (undefined = live)
|
|
525
|
-
});
|
|
526
|
-
// Every surfaced row must be its own walk terminal — no superseded
|
|
527
|
-
// ancestor competes as a truth — and the linked chain's terminal
|
|
528
|
-
// (computed with the production helper) must be surfaced.
|
|
529
|
-
const competing = recall.filter((r) => (0, retrieval_js_1.beliefWalkTerminal)(db, r.id) !== r.id);
|
|
530
|
-
const walkTerminalId = (0, retrieval_js_1.beliefWalkTerminal)(db, idByKey.get(members[0]) ?? "");
|
|
531
|
-
const terminalSurfaced = recall.some((r) => r.id === walkTerminalId);
|
|
532
|
-
classResolved = terminalSurfaced && competing.length === 0;
|
|
533
|
-
notes =
|
|
534
|
-
`terminal ${terminal} live=${terminalLive}; non-terminals demoted/absorbed=${nonTerminalsGone}; ` +
|
|
535
|
-
`walk terminal ${keyById.get(walkTerminalId) ?? "?"} surfaced=${terminalSurfaced}; superseded ancestors competing=${competing.length}` +
|
|
536
|
-
(terminalLive
|
|
537
|
-
? ""
|
|
538
|
-
: `; ground-truth terminal ${terminal} absorbed by the zone blend — the walk terminal is the linked chain's end`);
|
|
539
|
-
}
|
|
540
|
-
else {
|
|
541
|
-
classResolved = rows.every((r) => r.resolved);
|
|
542
|
-
notes = rows.length === 1 ? (rows[0].resolved ? "pair resolved" : "pair not resolved") : `${rows.filter((r) => r.resolved).length}/${rows.length} pairs resolved`;
|
|
543
|
-
}
|
|
544
|
-
classResults.push({
|
|
545
|
-
class: cls,
|
|
546
|
-
pairs: rows.length,
|
|
547
|
-
detected: rows.filter((r) => r.detected).length,
|
|
548
|
-
bound: rows.filter((r) => r.bound === true).length,
|
|
549
|
-
resolved: rows.filter((r) => r.resolved).length,
|
|
550
|
-
classResolved,
|
|
551
|
-
notes,
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
// --- colliders: non-ground-truth pairs at/above 0.70 ---
|
|
555
|
-
const colliders = [];
|
|
556
|
-
for (let i = 0; i < corpus.rows.length; i++) {
|
|
557
|
-
for (let j = i + 1; j < corpus.rows.length; j++) {
|
|
558
|
-
const a = corpus.rows[i];
|
|
559
|
-
const b = corpus.rows[j];
|
|
560
|
-
const sorted = pairKey(a.key, b.key);
|
|
561
|
-
if (declaredPairs.has(sorted))
|
|
562
|
-
continue;
|
|
563
|
-
const cosine = cosines.get(sorted) ?? Number.NaN;
|
|
564
|
-
if (!Number.isFinite(cosine) || cosine < 0.7)
|
|
565
|
-
continue;
|
|
566
|
-
const idA = idByKey.get(a.key) ?? "";
|
|
567
|
-
const idB = idByKey.get(b.key) ?? "";
|
|
568
|
-
const [olderKey, newerKey] = a.createdAt <= b.createdAt ? [a.key, b.key] : [b.key, a.key];
|
|
569
|
-
colliders.push({
|
|
570
|
-
aKey: a.key,
|
|
571
|
-
bKey: b.key,
|
|
572
|
-
cosine,
|
|
573
|
-
verdictCalled: judge.verdictCalls.some((c) => (c.olderKey === olderKey && c.newerKey === newerKey) ||
|
|
574
|
-
(c.olderKey === newerKey && c.newerKey === olderKey)),
|
|
575
|
-
merged: dedupLogBetween(db, idA, idB) !== null,
|
|
576
|
-
bothLive: liveInRecall(probe(db, idA)) && liveInRecall(probe(db, idB)),
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
return { pairResults, classResults, colliders, stageReport, zonePlanBefore, blendPredictions };
|
|
581
|
-
}
|
|
582
|
-
finally {
|
|
583
|
-
db.close();
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
// ---------------------------------------------------------------------------
|
|
587
|
-
// Report rendering (markdown — issue-comment shaped)
|
|
588
|
-
// ---------------------------------------------------------------------------
|
|
589
|
-
function yn(v) {
|
|
590
|
-
return v === null ? "n/a" : v ? "YES" : "no";
|
|
591
|
-
}
|
|
592
|
-
function renderPlantedReport(args) {
|
|
593
|
-
const { result } = args;
|
|
594
|
-
const L = [];
|
|
595
|
-
L.push("# Planted-Pairs Resolution Gate (#393 increment A)\n");
|
|
596
|
-
L.push(`Corpus: ${args.corpusLabel} \nGenerated: ${args.generatedAt} \nEmbedder: ${args.embedderLabel} \n` +
|
|
597
|
-
`Clock: ${args.clock} (#458) \n` +
|
|
598
|
-
`Judge: ground-truth stub (perfect-judge semantics — machinery isolation, per the refine Q3 ruling). \n` +
|
|
599
|
-
`Floors: correctionMinSimilarity 0.75 (judged-band floor) · dedupAutoMergeThreshold 0.92 (zone ceiling).\n`);
|
|
600
|
-
L.push("## Gate — per planted pair\n");
|
|
601
|
-
L.push("| class | pair | truth | cosine | band | detected | source | bound | resolved | notes |");
|
|
602
|
-
L.push("|---|---|---|---|---|---|---|---|---|---|");
|
|
603
|
-
for (const r of result.pairResults) {
|
|
604
|
-
L.push(`| ${r.class} | ${r.olderKey}→${r.newerKey} | ${r.relation} | ${r.cosine.toFixed(4)} | ` +
|
|
605
|
-
`${r.inBand ? "in" : "**OUT**"} | ${yn(r.detected)} | ${r.detectionSource || "-"} | ${yn(r.bound)} | ` +
|
|
606
|
-
`${yn(r.resolved)} | ${r.notes} |`);
|
|
607
|
-
}
|
|
608
|
-
L.push("");
|
|
609
|
-
L.push("## Gate — per class\n");
|
|
610
|
-
L.push("| class | pairs | detected | bound | resolved | class end-state | notes |");
|
|
611
|
-
L.push("|---|---|---|---|---|---|---|");
|
|
612
|
-
for (const c of result.classResults) {
|
|
613
|
-
L.push(`| ${c.class} | ${c.pairs} | ${c.detected} | ${c.bound} | ${c.resolved} | ${yn(c.classResolved)} | ${c.notes} |`);
|
|
614
|
-
}
|
|
615
|
-
L.push("");
|
|
616
|
-
if (result.colliders.length > 0) {
|
|
617
|
-
L.push("## Colliders (non-ground-truth pairs >= 0.70 cosine)\n");
|
|
618
|
-
L.push("| pair | cosine | verdict called | merged | both live |");
|
|
619
|
-
L.push("|---|---|---|---|---|");
|
|
620
|
-
for (const c of result.colliders) {
|
|
621
|
-
L.push(`| ${c.aKey} + ${c.bKey} | ${c.cosine.toFixed(4)} | ${yn(c.verdictCalled)} | ${yn(c.merged)} | ${yn(c.bothLive)} |`);
|
|
622
|
-
}
|
|
623
|
-
L.push("");
|
|
624
|
-
}
|
|
625
|
-
else {
|
|
626
|
-
L.push("## Colliders\n\n_(no non-ground-truth pair >= 0.70 cosine)_\n");
|
|
627
|
-
}
|
|
628
|
-
if (result.blendPredictions.length > 0) {
|
|
629
|
-
L.push("## Pre-stage zone-blend predictions (>= 0.92 discovery)\n");
|
|
630
|
-
for (const b of result.blendPredictions) {
|
|
631
|
-
L.push(`- ${b.pair} — canonical survives: **${b.canonicalKey}**, loser absorbed: ${b.loserKey}`);
|
|
632
|
-
}
|
|
633
|
-
L.push("");
|
|
634
|
-
}
|
|
635
|
-
const s = result.stageReport;
|
|
636
|
-
L.push("## Stage report (production `stageReconsolidation` output)\n");
|
|
637
|
-
L.push(`- scanned ${s.scanned}, pairs_discovered ${s.pairs_discovered} (unlinked ${s.pairs_discovered_unlinked}), ` +
|
|
638
|
-
`pairs_evaluated ${s.pairs_evaluated}\n` +
|
|
639
|
-
`- scout (#393 B): scanned ${s.scout_scanned}, correction_shaped ${s.scout_correction_shaped}, ` +
|
|
640
|
-
`candidates_found ${s.scout_candidates_found}\n` +
|
|
641
|
-
`- verdicts applied: rewritten ${s.rewritten} (absorbed ${s.absorbed}, kept ${s.kept_linked}), ` +
|
|
642
|
-
`marked_superseded ${s.marked_superseded}, marked_retracted ${s.marked_retracted}, ` +
|
|
643
|
-
`merge_pairs_applied ${s.merge_pairs_applied}\n` +
|
|
644
|
-
`- conflicts (guard-C): flagged ${s.conflict_flagged}, skipped ${s.conflict_skipped} ` +
|
|
645
|
-
`(zone clusters + judged merges refused on a conflicts link)\n` +
|
|
646
|
-
`- zone: clusters_found ${s.merges.clusters_found}, merged ${s.merges.merged_clusters}, ` +
|
|
647
|
-
`losers_merged ${s.merges.losers_merged}, project_skipped ${s.merges.skipped_project_mismatch}, ` +
|
|
648
|
-
`conflict_skipped ${s.merges.skipped_conflict}\n` +
|
|
649
|
-
`- skipped: infra ${s.skipped_infra}, idempotent ${s.skipped_idempotent}, above_ceiling ${s.skipped_above_ceiling}\n`);
|
|
650
|
-
return L.join("\n");
|
|
651
|
-
}
|