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.
Files changed (104) hide show
  1. package/README.md +1 -0
  2. package/THIRD-PARTY-NOTICES.md +1 -1
  3. package/bin/start.mjs +33 -8
  4. package/build-info.json +2 -2
  5. package/driver/CHANGELOG.md +32 -0
  6. package/driver/ask-ledger.mjs +90 -16
  7. package/driver/band-shape.mjs +65 -11
  8. package/driver/band-size.mjs +10 -0
  9. package/driver/close-verify.mjs +29 -2
  10. package/driver/common-law-coverage-status.mjs +1 -1
  11. package/driver/connotation-search.mjs +7 -9
  12. package/driver/contract-e3-backlog.mjs +34 -34
  13. package/driver/contract-e3-baseline.json +5 -5
  14. package/driver/contract-vocabulary.mjs +12 -12
  15. package/driver/corrections-delivered.mjs +113 -0
  16. package/driver/corrections-feedforward.mjs +16 -6
  17. package/driver/corrective-arm.mjs +11 -8
  18. package/driver/coverage-form-io.mjs +2 -1
  19. package/driver/coverage-form.mjs +17 -2
  20. package/driver/coverage-union.mjs +11 -2
  21. package/driver/digest-queue.mjs +1 -1
  22. package/driver/disposition-call.mjs +1 -1
  23. package/driver/document-coverage.mjs +3 -2
  24. package/driver/effective-scope.mjs +12 -7
  25. package/driver/engine/mcp/band-server.mjs +77 -13
  26. package/driver/engine/mcp/dispositions-server.mjs +2 -2
  27. package/driver/engine/mcp/held-answers.mjs +128 -0
  28. package/driver/engine/mcp/recording-server.mjs +10 -6
  29. package/driver/engine/mcp/supplemental.mjs +74 -12
  30. package/driver/findings-model.mjs +16 -4
  31. package/driver/floor-duty.mjs +9 -0
  32. package/driver/gate-metrics.mjs +7 -1
  33. package/driver/gateway.mjs +28 -14
  34. package/driver/goods-evidence.mjs +295 -0
  35. package/driver/knockout-assess-record.mjs +1 -1
  36. package/driver/narrative-refutation-record.mjs +40 -2
  37. package/driver/package.json +1 -1
  38. package/driver/pipeline.mjs +1994 -323
  39. package/driver/placement-decisions.mjs +243 -0
  40. package/driver/placement-form.mjs +42 -8
  41. package/driver/placement-union.mjs +14 -6
  42. package/driver/provider-usage.mjs +10 -7
  43. package/driver/publish/audit-from-spine.mjs +5 -2
  44. package/driver/publish/index.mjs +19 -4
  45. package/driver/publish/xlsx.mjs +6 -6
  46. package/driver/recall-reconciliation.mjs +15 -5
  47. package/driver/record-carry.mjs +70 -6
  48. package/driver/record-discard.mjs +7 -1
  49. package/driver/record-fold.mjs +14 -0
  50. package/driver/record-goods.mjs +101 -5
  51. package/driver/register-plan.mjs +221 -6
  52. package/driver/registry-fidelity.mjs +34 -3
  53. package/driver/remedy-accounting.mjs +52 -14
  54. package/driver/repair-composers.mjs +265 -105
  55. package/driver/repair-scope.mjs +327 -0
  56. package/driver/repair-targets.mjs +92 -0
  57. package/driver/result-noun-fields.mjs +3 -3
  58. package/driver/scope-facts.mjs +6 -2
  59. package/driver/screen-gate.mjs +96 -0
  60. package/driver/skills/clearance-common-law/SKILL.md +8 -0
  61. package/driver/skills/clearance-register/SKILL.md +29 -21
  62. package/driver/skills/clearance-register/digest.md +22 -17
  63. package/driver/skills/clearance-register/providers/clarivate.md +4 -4
  64. package/driver/skills/clearance-register/providers/corsearch.md +1 -1
  65. package/driver/skills/clearance-register/providers/euipo.md +4 -4
  66. package/driver/skills/clearance-register/providers/free-tier.md +4 -4
  67. package/driver/skills/clearance-register/providers/uspto-local.md +3 -3
  68. package/driver/skills/clearance-register/register-recipes.md +6 -6
  69. package/driver/skills/clearance-register/unit.md +29 -22
  70. package/driver/skills/clearance-search/delivery-contract.md +2 -58
  71. package/driver/skills/clearance-search/report-prose.md +12 -3
  72. package/driver/skills/clearance-search/risk-framework-triage.md +1 -1
  73. package/driver/skills/clearance-search/synthesis-rules.md +10 -13
  74. package/driver/skills/clearance-variants/SKILL.md +4 -4
  75. package/driver/skills/knockout-assess/SKILL.md +9 -14
  76. package/driver/skills/knockout-frame/SKILL.md +5 -5
  77. package/driver/skills/narrative-refutation/SKILL.md +1 -1
  78. package/driver/skills/placement-inquiry/SKILL.md +1 -1
  79. package/driver/skills/whatif-memo/SKILL.md +1 -1
  80. package/driver/stage-context.mjs +2 -1
  81. package/driver/stages-knockout.mjs +1 -1
  82. package/driver/stages.mjs +96 -61
  83. package/driver/suite-census.json +415 -25
  84. package/driver/synthesis-record.mjs +310 -8
  85. package/driver/terminal-clamp.mjs +53 -2
  86. package/driver/verify-knockout.mjs +22 -4
  87. package/driver/verify.mjs +23 -3
  88. package/mcp-server/CHANGELOG.md +8 -0
  89. package/mcp-server/lib/driver.mjs +5 -0
  90. package/mcp-server/lib/events.mjs +25 -4
  91. package/mcp-server/lib/findings.mjs +4 -2
  92. package/mcp-server/package.json +1 -1
  93. package/package.json +1 -1
  94. package/portal-ui/package.json +1 -1
  95. package/providers/_shared/enumerate.mjs +12 -6
  96. package/providers/_shared/execute-plan.mjs +25 -1
  97. package/providers/_shared/follow-up-searches.mjs +139 -0
  98. package/providers/jx/src/core.js +3 -4
  99. package/providers/jx/src/judge.js +2 -2
  100. package/providers/jx/src/nativeread.js +1 -2
  101. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  102. package/providers/oauth-mcp-bridge/package.json +1 -1
  103. package/scripts/e2e-first-time.mjs +30 -13
  104. 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.set(String(row.target).toLowerCase(), body);
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
- map.set(String(row.target).toLowerCase(), { fetched_at: String(row.ts), context: stripGatewayNs(row.sessionKey ?? "") || null });
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.set(uri, body);
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 five term classes. EXACTLY ONE per remedy term, decided in the fixed order in classifyRemedyTerm.
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 counted crowd
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, EVERY slice a counted zero ⇒ `searched-empty`;
144
- * 5. otherwise ⇒ `unaccounted`, with the shape that defeated the join stated.
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 qids) {
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 = qids.map((q) => blockFor(q));
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) => !ACCOUNTED_CLASSES.includes(r.class)),
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) && !ACCOUNTED_CLASSES.includes(r.class))
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,