clearotron 0.4.1 → 0.5.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/bin/start.mjs +33 -8
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +32 -0
- package/driver/ask-ledger.mjs +90 -16
- package/driver/band-shape.mjs +65 -11
- package/driver/band-size.mjs +10 -0
- package/driver/close-verify.mjs +29 -2
- package/driver/common-law-coverage-status.mjs +1 -1
- package/driver/connotation-search.mjs +7 -9
- package/driver/contract-e3-backlog.mjs +34 -34
- package/driver/contract-e3-baseline.json +5 -5
- package/driver/contract-vocabulary.mjs +12 -12
- package/driver/corrections-delivered.mjs +113 -0
- package/driver/corrections-feedforward.mjs +16 -6
- package/driver/corrective-arm.mjs +11 -8
- package/driver/coverage-form-io.mjs +2 -1
- package/driver/coverage-form.mjs +17 -2
- package/driver/coverage-union.mjs +11 -2
- package/driver/digest-queue.mjs +1 -1
- package/driver/disposition-call.mjs +1 -1
- package/driver/document-coverage.mjs +3 -2
- package/driver/effective-scope.mjs +12 -7
- package/driver/engine/mcp/band-server.mjs +77 -13
- package/driver/engine/mcp/dispositions-server.mjs +2 -2
- package/driver/engine/mcp/held-answers.mjs +128 -0
- package/driver/engine/mcp/recording-server.mjs +10 -6
- package/driver/engine/mcp/supplemental.mjs +74 -12
- package/driver/findings-model.mjs +16 -4
- package/driver/floor-duty.mjs +9 -0
- package/driver/gate-metrics.mjs +7 -1
- package/driver/gateway.mjs +28 -14
- package/driver/goods-evidence.mjs +295 -0
- package/driver/knockout-assess-record.mjs +1 -1
- package/driver/narrative-refutation-record.mjs +40 -2
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +1994 -323
- package/driver/placement-decisions.mjs +243 -0
- package/driver/placement-form.mjs +42 -8
- package/driver/placement-union.mjs +14 -6
- package/driver/provider-usage.mjs +10 -7
- package/driver/publish/audit-from-spine.mjs +5 -2
- package/driver/publish/index.mjs +19 -4
- package/driver/publish/xlsx.mjs +6 -6
- package/driver/recall-reconciliation.mjs +15 -5
- package/driver/record-carry.mjs +70 -6
- package/driver/record-discard.mjs +7 -1
- package/driver/record-fold.mjs +14 -0
- package/driver/record-goods.mjs +101 -5
- package/driver/register-plan.mjs +221 -6
- package/driver/registry-fidelity.mjs +34 -3
- package/driver/remedy-accounting.mjs +52 -14
- package/driver/repair-composers.mjs +265 -105
- package/driver/repair-scope.mjs +327 -0
- package/driver/repair-targets.mjs +92 -0
- package/driver/result-noun-fields.mjs +3 -3
- package/driver/scope-facts.mjs +6 -2
- package/driver/screen-gate.mjs +96 -0
- package/driver/skills/clearance-common-law/SKILL.md +8 -0
- package/driver/skills/clearance-register/SKILL.md +29 -21
- package/driver/skills/clearance-register/digest.md +22 -17
- package/driver/skills/clearance-register/providers/clarivate.md +4 -4
- package/driver/skills/clearance-register/providers/corsearch.md +1 -1
- package/driver/skills/clearance-register/providers/euipo.md +4 -4
- package/driver/skills/clearance-register/providers/free-tier.md +4 -4
- package/driver/skills/clearance-register/providers/uspto-local.md +3 -3
- package/driver/skills/clearance-register/register-recipes.md +6 -6
- package/driver/skills/clearance-register/unit.md +29 -22
- package/driver/skills/clearance-search/delivery-contract.md +2 -58
- package/driver/skills/clearance-search/report-prose.md +12 -3
- package/driver/skills/clearance-search/risk-framework-triage.md +1 -1
- package/driver/skills/clearance-search/synthesis-rules.md +10 -13
- package/driver/skills/clearance-variants/SKILL.md +4 -4
- package/driver/skills/knockout-assess/SKILL.md +9 -14
- package/driver/skills/knockout-frame/SKILL.md +5 -5
- package/driver/skills/narrative-refutation/SKILL.md +1 -1
- package/driver/skills/placement-inquiry/SKILL.md +1 -1
- package/driver/skills/whatif-memo/SKILL.md +1 -1
- package/driver/stage-context.mjs +2 -1
- package/driver/stages-knockout.mjs +1 -1
- package/driver/stages.mjs +96 -61
- package/driver/suite-census.json +415 -25
- package/driver/synthesis-record.mjs +310 -8
- package/driver/terminal-clamp.mjs +53 -2
- package/driver/verify-knockout.mjs +22 -4
- package/driver/verify.mjs +23 -3
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/lib/driver.mjs +5 -0
- package/mcp-server/lib/events.mjs +25 -4
- package/mcp-server/lib/findings.mjs +4 -2
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/_shared/enumerate.mjs +12 -6
- package/providers/_shared/execute-plan.mjs +25 -1
- package/providers/_shared/follow-up-searches.mjs +139 -0
- package/providers/jx/src/core.js +3 -4
- package/providers/jx/src/judge.js +2 -2
- package/providers/jx/src/nativeread.js +1 -2
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/e2e-first-time.mjs +30 -13
- package/scripts/release-await-cut.mjs +4 -2
|
@@ -105,6 +105,32 @@ let lastLedgerError = null;
|
|
|
105
105
|
export const ledgerReadError = () => lastLedgerError;
|
|
106
106
|
const noteLedger = (r) => { if (r.error) lastLedgerError = r.error; return r; };
|
|
107
107
|
|
|
108
|
+
// A RECORD BODY THAT CARRIES NO RECORD IS NOT A RECORD. A body counts as the record only where some field beside
|
|
109
|
+
// its identity and its plumbing holds a value: a mark, a status, an owner, a class list, goods, a date. An object
|
|
110
|
+
// holding nothing else is a placeholder: it satisfies no evidence duty, and it never replaces a body that carries
|
|
111
|
+
// the record. The normalising registers write their identity on every record, an empty one included: the provider,
|
|
112
|
+
// the office, the numbers the request asked for, a status class of "unknown", the link minted from them, and the
|
|
113
|
+
// underscored provenance and raw copy. None of those is the record. A value counts only where something in it holds
|
|
114
|
+
// one, so an object of nulls, or a list of empty objects, is no content either.
|
|
115
|
+
const RECORD_IDENTITY_KEYS = new Set(["uri", "record_id", "id", "guid", "serial", "serialNumber", "object", "provider", "office", "statusClass", "status_class",
|
|
116
|
+
"applicationNumber", "registrationNumber", "resolved_link", "officeUrl"]);
|
|
117
|
+
const holdsValue = (v, depth = 0) => {
|
|
118
|
+
if (v == null) return false;
|
|
119
|
+
if (typeof v === "string") return Boolean(v.trim());
|
|
120
|
+
if (depth > 8) return true;
|
|
121
|
+
if (Array.isArray(v)) return v.some((x) => holdsValue(x, depth + 1));
|
|
122
|
+
if (typeof v === "object") return Object.values(v).some((x) => holdsValue(x, depth + 1));
|
|
123
|
+
return true;
|
|
124
|
+
};
|
|
125
|
+
/** Does this body carry the record's own content, rather than only an identity? PURE. */
|
|
126
|
+
export function recordBodyHasContent(body) {
|
|
127
|
+
const b = Array.isArray(body) ? body[0] : body;
|
|
128
|
+
if (!b || typeof b !== "object" || Array.isArray(b)) return false;
|
|
129
|
+
return Object.entries(b).some(([k, v]) => !k.startsWith("_") && !RECORD_IDENTITY_KEYS.has(k) && holdsValue(v));
|
|
130
|
+
}
|
|
131
|
+
// A later copy wins, unless it is a placeholder and the record already holds one with content.
|
|
132
|
+
const keepRecord = (map, uri, body) => { if (recordBodyHasContent(body) || !recordBodyHasContent(map.get(uri))) map.set(uri, body); };
|
|
133
|
+
|
|
108
134
|
// Latest fetched body per record URI for the run. Never throws (a missing ledger → empty map).
|
|
109
135
|
//
|
|
110
136
|
// — NO DEFAULT PATH. It used to fall back to `ledgerPath("record")`, the box-global file. Now that
|
|
@@ -118,7 +144,7 @@ export function collectRecordBodies(recordLogPath, runPrefix) {
|
|
|
118
144
|
try { row = JSON.parse(line); } catch { return; }
|
|
119
145
|
if (!runPrefix || !rowMatchesRun(row, runPrefix)) return;
|
|
120
146
|
const body = Array.isArray(row.body) ? row.body[0] : row.body;
|
|
121
|
-
if (row.target && body && typeof body === "object") map
|
|
147
|
+
if (row.target && body && typeof body === "object") keepRecord(map, String(row.target).toLowerCase(), body);
|
|
122
148
|
}));
|
|
123
149
|
return map;
|
|
124
150
|
}
|
|
@@ -128,12 +154,17 @@ export function collectRecordBodies(recordLogPath, runPrefix) {
|
|
|
128
154
|
// provider calls); replaces the "was it on disk?" inference the report's `verified` label rested on.
|
|
129
155
|
export function collectRecordReceipts(recordLogPath, runPrefix) { // — no default; see above
|
|
130
156
|
const map = new Map();
|
|
157
|
+
const real = new Set(); // the records whose receipt is from a body carrying the record, which a placeholder's never replaces
|
|
131
158
|
noteLedger(forEachLedgerLine(recordLogPath, (line) => {
|
|
132
159
|
let row;
|
|
133
160
|
try { row = JSON.parse(line); } catch { return; }
|
|
134
161
|
if (!runPrefix || !rowMatchesRun(row, runPrefix)) return;
|
|
135
162
|
if (!row.target || !row.ts) return;
|
|
136
|
-
|
|
163
|
+
const key = String(row.target).toLowerCase();
|
|
164
|
+
const full = recordBodyHasContent(row.body);
|
|
165
|
+
if (!full && real.has(key)) return;
|
|
166
|
+
if (full) real.add(key);
|
|
167
|
+
map.set(key, { fetched_at: String(row.ts), context: stripGatewayNs(row.sessionKey ?? "") || null });
|
|
137
168
|
}));
|
|
138
169
|
return map;
|
|
139
170
|
}
|
|
@@ -229,7 +260,7 @@ export function assembleRunRecords(runDir, runPrefix,
|
|
|
229
260
|
const fromRunDir = readRecordArtifacts(runDir);
|
|
230
261
|
const fromLedger = collectRecordBodies(recordLogPath, runPrefix);
|
|
231
262
|
const records = new Map(fromRunDir);
|
|
232
|
-
for (const [uri, body] of fromLedger) records
|
|
263
|
+
for (const [uri, body] of fromLedger) keepRecord(records, uri, body);
|
|
233
264
|
// WP-receipts: receipts assemble the same way the records do — artifact-embedded ones (inherited run
|
|
234
265
|
// dir) ∪ this-session ledger rows (fresh fetch wins) — so a re-assembled run dir keeps its proof.
|
|
235
266
|
const receipts = new Map();
|
|
@@ -42,19 +42,23 @@
|
|
|
42
42
|
// re-stated: verifyRegisterDirectiveClose asks the identical question of the same blocks, and two
|
|
43
43
|
// copies of a matcher is how two matchers drift apart.
|
|
44
44
|
|
|
45
|
-
import { blockCollapsed, answeredAs } from "./close-verify.mjs";
|
|
45
|
+
import { blockCollapsed, answeredAs, withheldAs } from "./close-verify.mjs";
|
|
46
46
|
import { reopenKey } from "./frame-diff-model.mjs";
|
|
47
47
|
|
|
48
48
|
export const REMEDY_ACCOUNTING_SCHEMA_VERSION = 1;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* The
|
|
51
|
+
* The six term classes. EXACTLY ONE per remedy term, decided in the fixed order in classifyRemedyTerm.
|
|
52
52
|
*
|
|
53
|
-
* found — every slice landed and the register returned material (records, or a
|
|
54
|
-
* too large to enumerate). The term is answered.
|
|
53
|
+
* found — every slice that ran landed and the register returned material (records, or a
|
|
54
|
+
* counted crowd too large to enumerate). The term is answered.
|
|
55
55
|
* searched-empty — every slice landed and every one is a COUNTED zero: state `enumerated`, a finite
|
|
56
56
|
* total_hits of 0, an empty records array. The executed query and its empty result
|
|
57
57
|
* are both recorded on the row. This is the state that used to leave no trace.
|
|
58
|
+
* withheld-by-judgment — a slice repeated a waiting family the reading turn withheld, so it was never
|
|
59
|
+
* asked, and every other slice landed with nothing material. Nothing was found and
|
|
60
|
+
* nothing is claimed clean: the term rests on the reading turn's recorded judgment,
|
|
61
|
+
* named on the slice it settles (`withheld_as`).
|
|
58
62
|
* not-dispatched — no slice was ever minted for this term, with the reason the mint or the remedy
|
|
59
63
|
* derivation gave. The term was NOT searched, and now says so.
|
|
60
64
|
* dispatch-failed— a slice was minted but did not land as a usable executed block: unlanded, error,
|
|
@@ -64,7 +68,7 @@ export const REMEDY_ACCOUNTING_SCHEMA_VERSION = 1;
|
|
|
64
68
|
* counted zero (an uncountable total, an unrecognised state), or the receipt offers
|
|
65
69
|
* no mapping at all. NEVER guessed in either direction.
|
|
66
70
|
*/
|
|
67
|
-
export const REMEDY_TERM_CLASSES = ["found", "searched-empty", "not-dispatched", "dispatch-failed", "unaccounted"];
|
|
71
|
+
export const REMEDY_TERM_CLASSES = ["found", "searched-empty", "withheld-by-judgment", "not-dispatched", "dispatch-failed", "unaccounted"];
|
|
68
72
|
|
|
69
73
|
/**
|
|
70
74
|
* The two classes that mean "this term's search is ON THE RECORD". Deliberately NOT every class with a
|
|
@@ -73,6 +77,15 @@ export const REMEDY_TERM_CLASSES = ["found", "searched-empty", "not-dispatched",
|
|
|
73
77
|
*/
|
|
74
78
|
export const ACCOUNTED_CLASSES = ["found", "searched-empty"];
|
|
75
79
|
|
|
80
|
+
/**
|
|
81
|
+
* The classes that leave nothing open: a search on the record, or the reading turn's recorded judgment not
|
|
82
|
+
* to ask the question at all. A withheld family is a wider question the ruled method lets that turn leave
|
|
83
|
+
* unasked, on the record with its reason, so a term resting on one is neither a hole nor a doubt. It is
|
|
84
|
+
* still not a search, which is why ACCOUNTED_CLASSES stays as it is. The not-accounted list, the doubts and
|
|
85
|
+
* the closure read below all read this one.
|
|
86
|
+
*/
|
|
87
|
+
export const SETTLED_CLASSES = [...ACCOUNTED_CLASSES, "withheld-by-judgment"];
|
|
88
|
+
|
|
76
89
|
const clip = (s, n = 240) => {
|
|
77
90
|
const t = String(s ?? "").replace(/\s+/g, " ").trim();
|
|
78
91
|
return t.length > n ? `${t.slice(0, n - 1)}…` : t;
|
|
@@ -138,10 +151,13 @@ function material(block) {
|
|
|
138
151
|
* 2. ANY slice that did not land clean ⇒ `dispatch-failed`, naming the first such qid. Every slice
|
|
139
152
|
* must land, exactly as verifyRegisterDirectiveClose requires of a directive — one convention. A
|
|
140
153
|
* slice the FOLD refused as already-asked is read through the twin it duplicates first, because that
|
|
141
|
-
* slice cannot land and the twin is where its question was answered
|
|
154
|
+
* slice cannot land and the twin is where its question was answered. A slice refused as a family
|
|
155
|
+
* the reading turn withheld is settled by that judgment (`withheldAs`) and is left out of rules 2-5:
|
|
156
|
+
* it never ran, so it has no result to read either way;
|
|
142
157
|
* 3. all landed, ANY slice material ⇒ `found`;
|
|
143
|
-
* 4. all landed,
|
|
144
|
-
* 5.
|
|
158
|
+
* 4. all landed with nothing material, and a slice withheld ⇒ `withheld-by-judgment`;
|
|
159
|
+
* 5. all landed, EVERY slice a counted zero ⇒ `searched-empty`;
|
|
160
|
+
* 6. otherwise ⇒ `unaccounted`, with the shape that defeated the join stated.
|
|
145
161
|
* PURE.
|
|
146
162
|
*/
|
|
147
163
|
export function classifyRemedyTerm(row, { blocksByQid = new Map(), executedQids = new Set(), foldRefusals = new Map() } = {}) {
|
|
@@ -159,20 +175,31 @@ export function classifyRemedyTerm(row, { blocksByQid = new Map(), executedQids
|
|
|
159
175
|
// ONLY THE DUPLICATE KIND RESOLVES. An identity collision or a malformed term is a genuine fault and
|
|
160
176
|
// stays `dispatch-failed`: nothing else asked those questions, so nothing else answers them.
|
|
161
177
|
const landsAs = new Map(qids.map((q) => [q, answeredAs(q, foldRefusals)])); // one rule with the directive check
|
|
178
|
+
// — A SLICE REFUSED AS A FAMILY THE READING TURN WITHHELD IS SETTLED, NOT READ.
|
|
179
|
+
//
|
|
180
|
+
// Its twin never ran: the reading turn decided not to ask that family and recorded why, and the re-sweep
|
|
181
|
+
// refused to ask it on the turn's behalf. So there is no result to read, and reading the slice as one that
|
|
182
|
+
// failed to land would record a dispatch failure, and mint a doubt, over a decision the run had made on
|
|
183
|
+
// purpose. It is settled by the same rule the directive check uses (`withheldAs`), it keeps its own row
|
|
184
|
+
// saying which family it repeats, and it carries no count, no state and no executed flag of its own.
|
|
185
|
+
const settledBy = new Map(qids.map((q) => [q, withheldAs(q, foldRefusals)]).filter(([, w]) => w));
|
|
162
186
|
const blockFor = (q) => blocksByQid.get(landsAs.get(q) ?? q);
|
|
163
187
|
const ranAs = (q) => executedQids.has(landsAs.get(q) ?? q);
|
|
164
188
|
const slices = qids.map((q) => {
|
|
189
|
+
const settled = settledBy.get(q);
|
|
190
|
+
if (settled) return { ...sliceRow(q, null, false), refused_at_fold: "withheld-by-judgment", withheld_as: settled.family };
|
|
165
191
|
const via = landsAs.get(q);
|
|
166
192
|
const slice = sliceRow(q, blockFor(q), ranAs(q));
|
|
167
193
|
return via && via !== q ? { ...slice, answered_by: via, refused_at_fold: "duplicate-question" } : slice;
|
|
168
194
|
});
|
|
195
|
+
const asked = qids.filter((q) => !settledBy.has(q));
|
|
169
196
|
if (!qids.length) {
|
|
170
197
|
const reason = String(row?.dispatch_reason ?? "").trim();
|
|
171
198
|
return reason
|
|
172
199
|
? { class: "not-dispatched", basis: "no-slice-minted", reason: clip(reason, 300), slices }
|
|
173
200
|
: { class: "unaccounted", basis: "no-mapping", reason: "the receipt records no slice and no reason for this term — the join cannot say whether it was searched", slices };
|
|
174
201
|
}
|
|
175
|
-
for (const q of
|
|
202
|
+
for (const q of asked) {
|
|
176
203
|
const b = blockFor(q);
|
|
177
204
|
if (sliceLanded(b, ranAs(q))) continue;
|
|
178
205
|
const why = !ranAs(q) ? "slice-not-landed"
|
|
@@ -182,8 +209,11 @@ export function classifyRemedyTerm(row, { blocksByQid = new Map(), executedQids
|
|
|
182
209
|
: "collapsed-slice";
|
|
183
210
|
return { class: "dispatch-failed", basis: why, reason: `${why}:${q}${b?.reason ? ` — ${clip(b.reason, 200)}` : ""}`, slices };
|
|
184
211
|
}
|
|
185
|
-
const blocks =
|
|
212
|
+
const blocks = asked.map((q) => blockFor(q));
|
|
186
213
|
if (blocks.some(material)) return { class: "found", basis: "band-block", reason: null, slices };
|
|
214
|
+
// Nothing material came back, and part of the term was never asked by the turn's decision: a counted zero
|
|
215
|
+
// beside a question nobody asked is not a clean result for the term, so it is not `searched-empty`.
|
|
216
|
+
if (settledBy.size && blocks.every(countedZero)) return { class: "withheld-by-judgment", basis: "reading-turn-record", reason: null, slices };
|
|
187
217
|
if (blocks.every(countedZero)) return { class: "searched-empty", basis: "counted-zero", reason: null, slices };
|
|
188
218
|
const odd = blocks.find((b) => !countedZero(b));
|
|
189
219
|
return {
|
|
@@ -240,9 +270,10 @@ export function accountRemedyTerms({ terms = [], blocksByQid = new Map(), execut
|
|
|
240
270
|
totals,
|
|
241
271
|
by_directive: byDirective,
|
|
242
272
|
// the two lists a reader is owed, kept APART on purpose (see the zero-semantics note in the header):
|
|
243
|
-
// a term that ran and found nothing is EVIDENCE; a term that never ran is a hole.
|
|
273
|
+
// a term that ran and found nothing is EVIDENCE; a term that never ran is a hole. A term the reading
|
|
274
|
+
// turn's judgment settled is neither, and is in neither list: its row says which family it rests on.
|
|
244
275
|
searched_empty: rows.filter((r) => r.class === "searched-empty"),
|
|
245
|
-
not_accounted: rows.filter((r) => !
|
|
276
|
+
not_accounted: rows.filter((r) => !SETTLED_CLASSES.includes(r.class)),
|
|
246
277
|
out_of_scope: (Array.isArray(outOfScope) ? outOfScope : []).map((o) => ({
|
|
247
278
|
directive: String(o?.directive ?? ""), layer: String(o?.layer ?? ""),
|
|
248
279
|
reason: clip(o?.reason ?? "source-channel directive — swept by a prose byte-diff on the common-law findings, so it has no term unit and no per-query record to account", 300),
|
|
@@ -269,6 +300,11 @@ export function accountRemedyTerms({ terms = [], blocksByQid = new Map(), execut
|
|
|
269
300
|
* `accounting` may be null (the ledger did not build) — then this returns the caller's own prior answer
|
|
270
301
|
* untouched, because a join that could not run is not evidence of a gap. PURE.
|
|
271
302
|
*
|
|
303
|
+
* A term settled by the reading turn's recorded judgment does not hold the gap open (SETTLED_CLASSES): the
|
|
304
|
+
* re-sweep did not ask a question that turn had decided against, and holding a CLEAR on that would turn a
|
|
305
|
+
* recorded decision into an unfinished search. It closes nothing by itself either: the directive must still
|
|
306
|
+
* be verified-closed, and every slice that did run must have landed.
|
|
307
|
+
*
|
|
272
308
|
* @returns {{domClosed: boolean, blockedBy: {term, directive, class, reason}[]}}
|
|
273
309
|
*/
|
|
274
310
|
export function domClosedOverAccounted({ directives = [], swept = [], accounting = null, reopenKeyOf = reopenKey } = {}) {
|
|
@@ -283,7 +319,7 @@ export function domClosedOverAccounted({ directives = [], swept = [], accounting
|
|
|
283
319
|
if (!closedOnADominant) return { domClosed: false, blockedBy: [] };
|
|
284
320
|
if (!accounting || accounting.computable !== true) return { domClosed: true, blockedBy: [] };
|
|
285
321
|
const blockedBy = (accounting.rows ?? [])
|
|
286
|
-
.filter((r) => domKeys.has(r.directive) && !
|
|
322
|
+
.filter((r) => domKeys.has(r.directive) && !SETTLED_CLASSES.includes(r.class))
|
|
287
323
|
.map((r) => ({ term: r.term, directive: r.directive, class: r.class, reason: r.reason }));
|
|
288
324
|
return { domClosed: blockedBy.length === 0, blockedBy };
|
|
289
325
|
}
|
|
@@ -292,7 +328,7 @@ export function domClosedOverAccounted({ directives = [], swept = [], accounting
|
|
|
292
328
|
// AD-4 house rule: every field is written on EVERY row, so "no term went unaccounted"
|
|
293
329
|
// (unaccounted:0) and "the ledger could not run" (unaccounted:null) differ by VALUE, never by field
|
|
294
330
|
// presence.
|
|
295
|
-
export const REMEDY_ACCOUNTING_EVENT_FIELDS = ["terms", "found", "searched_empty", "not_dispatched",
|
|
331
|
+
export const REMEDY_ACCOUNTING_EVENT_FIELDS = ["terms", "found", "searched_empty", "withheld_by_judgment", "not_dispatched",
|
|
296
332
|
"dispatch_failed", "unaccounted"];
|
|
297
333
|
|
|
298
334
|
export function remedyAccountingEvent({ trigger = null, artifact = null, reason = null } = {}) {
|
|
@@ -313,6 +349,8 @@ export function remedyAccountingEvent({ trigger = null, artifact = null, reason
|
|
|
313
349
|
* doubt:remedy-accounting:unaccounted:N — the join cannot say either way
|
|
314
350
|
* A `searched-empty` term mints NOTHING: it is answered, and its query and empty result ride the
|
|
315
351
|
* artifact and the receipt. Minting a doubt for it would turn a completed search into an open question.
|
|
352
|
+
* A `withheld-by-judgment` term mints nothing either: it rests on the reading turn's recorded decision not
|
|
353
|
+
* to ask, and a doubt would re-open that decision as a question nobody had answered.
|
|
316
354
|
*
|
|
317
355
|
* The frozen doubt-record shape, status "open"; stitchDoubts and the doubt-closure stage decide endings
|
|
318
356
|
* exactly as for every other doubt family, and an OPEN one shipping is the system working. Never gates,
|