clearotron 0.5.0-beta.3 → 0.5.0-beta.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.
- package/build-info.json +2 -2
- package/docs/architecture/03-run-lifecycle.md +6 -0
- package/driver/CHANGELOG.md +10 -0
- package/driver/band-shape.mjs +15 -7
- package/driver/engine/mcp/band-server.mjs +12 -12
- package/driver/engine/mcp/codex-config.mjs +12 -7
- package/driver/engine/mcp/dispositions-server.mjs +7 -0
- package/driver/engine/mcp/perplexity-server.mjs +29 -1
- package/driver/engine/mcp/recording-server.mjs +16 -1
- package/driver/engine/mcp/supplemental.mjs +6 -1
- package/driver/engine/openai-agent.mjs +48 -9
- package/driver/findings-model.mjs +17 -0
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +38 -9
- package/driver/placement-decisions.mjs +20 -5
- package/driver/predelivery-lint.mjs +8 -1
- package/driver/register-digest-record.mjs +6 -3
- package/driver/research-answers.mjs +54 -7
- package/driver/saved-findings-block.mjs +36 -0
- package/driver/skills/clearance-search/phase2-execution.md +4 -2
- package/driver/stage-context.mjs +18 -0
- package/driver/stages.mjs +36 -8
- package/driver/suite-census.json +56 -2
- package/driver/synthesis-record.mjs +167 -1
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/_shared/execute-plan.mjs +9 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/release-notes-lint.mjs +1 -1
- package/shared/inside-run.mjs +54 -0
package/driver/pipeline.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import { paths, STAGES, axisTier, decideAxes, assertTierSanity, assertEffectiveT
|
|
|
34
34
|
chainEntries, stageOrdinal, stageInputs, stageOutputs, dependencyOrder, REGISTER_AXES, REGISTER_ENUMERATE_TOOL,
|
|
35
35
|
buildEscalationFollowup, buildEnvelopeCloseFollowup, buildFrameReopenFollowup,
|
|
36
36
|
buildFrameReopenRetryMessage, thinkingFor, composeFollowup, stampDispatchBlocks, recordEmptyReturn, nothingFound, nothingToRead, PROVIDER_META, proseRungDirective, inquiryRungDirective } from "./stages.mjs"; import { bandSizeForStage, derivedLimitSec, limitExceedsCeiling, ceilingRefusal, stagesRefusingBand } from "./band-size.mjs";
|
|
37
|
-
import { handResearchAnswers } from "./research-answers.mjs"; // the research a stage's earlier attempts got, handed to a later one
|
|
37
|
+
import { handResearchAnswers, RESEARCH_REPLAY_FILE, savedResearchCount } from "./research-answers.mjs"; // the research a stage's earlier attempts got, handed to a later one; a replay of it
|
|
38
38
|
import { IDENTITY_FILE as REPORT_IDENTITY_FILE, renderReportOverview } from "./report-overview-record.mjs";
|
|
39
39
|
import { writePartNotCompleted, readPartNotCompleted, clearPartNotCompleted, clearPartsNotCompleted, NOT_COMPLETED_DIR, slicesNotYetRecorded, axesNotYetDisclosed, unitsNotYetRecorded } from "./parts-not-completed.mjs";
|
|
40
40
|
import { renderUnitNote, readBandAccount } from "./register-unit-record.mjs"; // a unit that could not finish, its note rendered from its band
|
|
@@ -153,7 +153,7 @@ import { escalatedAxes } from "./skeptic-record.mjs"; // THE escalation parse
|
|
|
153
153
|
// — every placed candidate ends somewhere a reader can see; the ones that do not are counted by name
|
|
154
154
|
import { PLACEMENT_CARRY_SCHEMA_VERSION, reconcilePlacementCarry, placementCarryEvent, mintPlacementCarryDoubts, entryUris, parseCarrySurfaces, classifyPlacement } from "./placement-carry.mjs";
|
|
155
155
|
import { FLOOR_DUTY_SCHEMA_VERSION, reconcileFloorDuty, floorDutyEvent, armFloorDuty, floorDutyArmed, floorDutyBlock, floorDutyClosed, floorDutyBlocksSkip } from "./floor-duty.mjs"; // — the era stamp that turns disclosure into a delivery floor
|
|
156
|
-
import { synthesisDutyForRun, snapshotSynthesisBase, restoreSynthesisBase, rebaseSynthesis, lastAcceptedCall, acceptSynthesis, driverReadsFor } from "./synthesis-record.mjs"; // — the duty checked against the DELIVERED document; the merge base a corrective pass holds and puts back
|
|
156
|
+
import { synthesisDutyForRun, snapshotSynthesisBase, restoreSynthesisBase, rebaseSynthesis, lastAcceptedCall, acceptSynthesis, driverReadsFor, handSavedFindings } from "./synthesis-record.mjs"; // — the duty checked against the DELIVERED document; the merge base a corrective pass holds and puts back
|
|
157
157
|
import { RECORD_CARRY_SCHEMA_VERSION, traceRecordCarry, parseStageOutcomes, recordCarryEvent, mintRecordCarryDoubts, recordCarryDoubtsAgain, bandRecordUri, placementIndex, findingUris , silentlyLostFindings, statedDivergenceFindings } from "./record-carry.mjs"; import { pickingExits, notesExits, notesPageRows, exitsForLog } from "./hand-off-exits.mjs";
|
|
158
158
|
import { reconcileSurfaceDuty, surfaceDutyNote } from "./surface-duty.mjs"; // item 3 — silence at the findings surface, read off the rows above
|
|
159
159
|
import { DISCARD_LEDGER_NAME, seamRows, appendDiscardRows, foldDiscardLedger } from "./record-discard.mjs";
|
|
@@ -1679,6 +1679,8 @@ const AXIS_VOCABULARY = {
|
|
|
1679
1679
|
// a preserved run's canonical artefact came from); the rest are the re-dispatch triggers runDigest
|
|
1680
1680
|
// labels, and they carry the placement rulings tail a fresh pass does not.
|
|
1681
1681
|
export const DISPATCH_TRIGGERS = ["fresh", "escalation", "envelope", "late-bind", "stale-repair", "settlement-flush", "corrective"]; // @internal — — the corrective pass is the seam the losses happen in; see driver/corrective-arm.mjs for warm vs cold
|
|
1682
|
+
// How an --experiment arm's research is answered: sent out as the run sent it, or replayed from the run's own answers.
|
|
1683
|
+
export const RESEARCH_MODES = ["live", "replay"]; // @internal
|
|
1682
1684
|
|
|
1683
1685
|
/**
|
|
1684
1686
|
* The sha of a sandbox file AS IT WOULD READ on the canonical run — the sandbox's own directory
|
|
@@ -5007,10 +5009,15 @@ export function messageCtxFor(name, ctx) { // @internal — driven by its test
|
|
|
5007
5009
|
if (name === "narrative-refutation") return { ...ctx, placementFloorDecisions: reviewFloorDecisionLines(ctx.paths) };
|
|
5008
5010
|
// The reading step and the writer are told placement's accepted set-asides the same way, read when the message is built.
|
|
5009
5011
|
if (name === "register-digest") return { ...ctx, placementSetAsides: placementSetAsideLines(ctx.paths) };
|
|
5010
|
-
// The writer is also handed the research
|
|
5011
|
-
// the message is built: none on a
|
|
5012
|
+
// The writer is also handed the research this run's earlier stages and attempts got (research-answers.mjs), written to
|
|
5013
|
+
// the run folder when the message is built: none on a run that has asked nothing yet, whose message is unchanged.
|
|
5014
|
+
// A sandbox arm reproducing a FIRST attempt hands on nothing: its copy holds the run's own answers under this stage's
|
|
5015
|
+
// key, which a first attempt in a run never has (`researchHandOff`, set by the experiment).
|
|
5012
5016
|
if (name === "synthesis") return { ...ctx, placementSetAsides: placementSetAsideLines(ctx.paths),
|
|
5013
|
-
researchAnswered: ctx.run ? handResearchAnswers(ctx.paths.runDir, stageSessionKey(ctx, name)) : 0
|
|
5017
|
+
researchAnswered: ctx.run && ctx.researchHandOff !== false ? handResearchAnswers(ctx.paths.runDir, stageSessionKey(ctx, name), { ofRun: true }) : 0,
|
|
5018
|
+
// The findings an earlier attempt saved, handed on as a file (synthesis-record.mjs): none on a first attempt, and none
|
|
5019
|
+
// in a sandbox arm, whose copy holds no saved findings.
|
|
5020
|
+
savedSynthesis: handSavedFindings(ctx.paths.runDir) };
|
|
5014
5021
|
return ctx;
|
|
5015
5022
|
}
|
|
5016
5023
|
|
|
@@ -18438,6 +18445,12 @@ async function runExperimentInner(job, opts) {
|
|
|
18438
18445
|
throw new Error(`--experiment ${name}: --axis "${axis}" is not ${vocab.what} (${vocab.oneOf})`);
|
|
18439
18446
|
// A typo here changes WHICH driver-computed blocks the arm carries and nothing would say so — the
|
|
18440
18447
|
// same silent-degradation shape the RETIRED_FLAGS table at the bottom of this file exists for.
|
|
18448
|
+
// `--research replay` answers the arm's research from the run's own answers and sends nothing out
|
|
18449
|
+
// (research-answers.mjs). Only a stage that holds the research tool has research to replay.
|
|
18450
|
+
if (opts.research != null && !RESEARCH_MODES.includes(opts.research))
|
|
18451
|
+
throw new Error(`--research: unknown value "${opts.research}" — one of: ${RESEARCH_MODES.join(", ")}`);
|
|
18452
|
+
if (opts.research === "replay" && !experimentToolGroups(name).includes("perplexity"))
|
|
18453
|
+
throw new Error(`--experiment ${name} --research replay: ${name} holds no research tool, so there is no research to replay`);
|
|
18441
18454
|
if (opts.dispatchTrigger != null && !DISPATCH_TRIGGERS.includes(opts.dispatchTrigger))
|
|
18442
18455
|
throw new Error(`--dispatch-trigger: unknown value "${opts.dispatchTrigger}" — it decides which production pass the arm reproduces, so a typo would compose a quietly different prompt. One of: ${DISPATCH_TRIGGERS.join(", ")}`);
|
|
18443
18456
|
const ctx = reconstructCtx(job, opts);
|
|
@@ -18451,7 +18464,10 @@ async function runExperimentInner(job, opts) {
|
|
|
18451
18464
|
// Any ctx field holding a PATH is dropped rather than inherited: reconstructCtx resolved them against
|
|
18452
18465
|
// the canonical run, and a sandbox whose prompt names a canonical path is not a sandbox. The
|
|
18453
18466
|
// derivations below re-set them against ctx.paths, which here is the shadow dir.
|
|
18454
|
-
|
|
18467
|
+
// A `fresh` arm is a stage's first attempt, which is handed no earlier attempt's research: the copied answers are
|
|
18468
|
+
// what the replay answers from, never a hand-off production's first attempt does not get.
|
|
18469
|
+
const shadowCtx = { ...ctx, paths: paths(shadowDir), axis, gridSpecPath: null, clHalfTerms: null,
|
|
18470
|
+
researchHandOff: (opts.dispatchTrigger ?? "fresh") !== "fresh" };
|
|
18455
18471
|
const inputOpts = { axes: ctx.axes, axis, registerOnly: ctx.registerOnly };
|
|
18456
18472
|
const manifest = sandboxManifest(name, ctx.paths, inputOpts);
|
|
18457
18473
|
// ── 1. GATHER — the sandbox manifest, not the freshness list ──────────────────────────────────────
|
|
@@ -18502,6 +18518,15 @@ async function runExperimentInner(job, opts) {
|
|
|
18502
18518
|
throw new Error(`--experiment ${name}: the sandbox could not reproduce ${gaps.length} thing(s) this stage's context has on the canonical run — refusing to dispatch against an incomplete context:\n` +
|
|
18503
18519
|
gaps.map((g) => ` · ${g.rel} [${g.kind}${g.via ? ` via ${g.via}` : ""}]${g.why ? ` — ${g.why}` : ""}`).join("\n"));
|
|
18504
18520
|
}
|
|
18521
|
+
// A REPLAY NEEDS ANSWERS TO REPLAY. Refused by name when the run kept none, rather than dispatched to refuse
|
|
18522
|
+
// every question it asks; the marker is the research server's whole switch, and only a sandbox writes it.
|
|
18523
|
+
let replayedAnswers = null;
|
|
18524
|
+
if (opts.research === "replay") {
|
|
18525
|
+
replayedAnswers = savedResearchCount(shadowDir);
|
|
18526
|
+
if (!replayedAnswers)
|
|
18527
|
+
throw new Error(`--experiment ${name} --research replay: this run kept no research answers (_driver/research-answers.jsonl is absent or holds none), so there is nothing to replay — run the arm with --research live`);
|
|
18528
|
+
atomicWrite(driverDir(shadowDir, RESEARCH_REPLAY_FILE), JSON.stringify({ replayOf: "the run's saved research answers", answers: replayedAnswers }, null, 2) + "\n");
|
|
18529
|
+
}
|
|
18505
18530
|
// ── 4. INLINE context — what no file copy can supply ──────────────────────────────────────────────
|
|
18506
18531
|
//
|
|
18507
18532
|
// `report-card` receives its finding as ctx.finding; its declared findings.json exists for staleness,
|
|
@@ -18588,6 +18613,10 @@ async function runExperimentInner(job, opts) {
|
|
|
18588
18613
|
// omitted: absent and "there was none" are different facts and only one of them is a defect.
|
|
18589
18614
|
ratedUnder: ctx.profile?.profileKey ?? null,
|
|
18590
18615
|
dispatchTrigger: opts.dispatchTrigger ?? "fresh",
|
|
18616
|
+
// How the arm's research was answered: sent out live, or replayed from the run's own answers.
|
|
18617
|
+
research: opts.research === "replay" ? { mode: "replay", answers: replayedAnswers } : { mode: "live" },
|
|
18618
|
+
// Whether the arm's message hands on an earlier attempt's research, as a retry's does: never on a `fresh` arm.
|
|
18619
|
+
researchHandOff: shadowCtx.researchHandOff,
|
|
18591
18620
|
// — WHICH PASS THIS ARM REPRODUCED, in a word. null on a non-corrective arm;
|
|
18592
18621
|
// never "dispatched-warm", which is production's alone.
|
|
18593
18622
|
correctivePass: whichPass,
|
|
@@ -18693,7 +18722,7 @@ function parseArgv(argv) {
|
|
|
18693
18722
|
const o = {};
|
|
18694
18723
|
const flags = { "--job": "job", "--agent": "agent", "--resume": "codename", "--from": "fromStage",
|
|
18695
18724
|
"--experiment": "experiment", "--model": "model", "--instructions": "instructions",
|
|
18696
|
-
"--label": "label", "--axis": "axis", "--dispatch-trigger": "dispatchTrigger" };
|
|
18725
|
+
"--label": "label", "--axis": "axis", "--dispatch-trigger": "dispatchTrigger", "--research": "research" };
|
|
18697
18726
|
for (let i = 0; i < argv.length; i++) {
|
|
18698
18727
|
const key = flags[argv[i]];
|
|
18699
18728
|
if (!key) {
|
|
@@ -18861,7 +18890,7 @@ export function resumeAdvice({ result = null, error = null, signal = null, scrip
|
|
|
18861
18890
|
|
|
18862
18891
|
const USAGE = "usage: node pipeline.mjs --job <file.json> [--agent <id>]\n" +
|
|
18863
18892
|
" [--resume <codename> [--from <stage>]]\n" +
|
|
18864
|
-
" [--resume <codename> --experiment <stage> [--model <id>] [--instructions <text>] [--axis <axis>] [--label <text>] [--dispatch-trigger <t>]]\n" +
|
|
18893
|
+
" [--resume <codename> --experiment <stage> [--model <id>] [--instructions <text>] [--axis <axis>] [--label <text>] [--dispatch-trigger <t>] [--research live|replay]]\n" +
|
|
18865
18894
|
" --instructions appends to a stage's prompt (a light tweak); a full prompt rewrite means editing the stage's skill file.\n" +
|
|
18866
18895
|
" --axis also names the ORDINAL for report-card (its dispatch label is report-card:<ordinal>).\n" +
|
|
18867
18896
|
" --dispatch-trigger picks WHICH production pass the arm reproduces (default \"fresh\"); a corrective pass carries prompt blocks a fresh one does not.";
|
|
@@ -18976,7 +19005,7 @@ if (isEntrypoint(import.meta.url)) void (async () => {
|
|
|
18976
19005
|
// hangs `__runRef` on the caller's own opts the moment the run context is built, which is how pipeline()'s
|
|
18977
19006
|
// rate-limit catch already finds the run to postpone; the same hook lets a THROWN exit name the run.
|
|
18978
19007
|
const opts = a.experiment
|
|
18979
|
-
? { ...base, codename: a.codename, experiment: a.experiment, model: a.model, instructions: a.instructions, axis: a.axis, label: a.label, dispatchTrigger: a.dispatchTrigger }
|
|
19008
|
+
? { ...base, codename: a.codename, experiment: a.experiment, model: a.model, instructions: a.instructions, axis: a.axis, label: a.label, dispatchTrigger: a.dispatchTrigger, research: a.research }
|
|
18980
19009
|
: { ...base, codename: a.codename, fromStage: a.fromStage, ...(a.job ? {} : { jobRebuiltFromStatus: true }) };
|
|
18981
19010
|
const script = resolve(fileURLToPath(import.meta.url));
|
|
18982
19011
|
// `jobPath` is null on a rebuilt resume, so the advice composes a command that does not name a file
|
|
@@ -15,7 +15,7 @@ import { dirname } from "node:path";
|
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
16
|
import { buildSelectionIndex, setAsideGrounds, isSetAsideGroupRow, lineKey, groupName, servedLines, FLOOR_GROUND_ON } from "./placement-form.mjs";
|
|
17
17
|
import { unionPlacementForm } from "./placement-union.mjs";
|
|
18
|
-
import { readPlacementForm, readPlacementFormInput } from "./placement-form-io.mjs";
|
|
18
|
+
import { readPlacementForm, readPlacementFormInput, placementFormStamp, placementFormPaths } from "./placement-form-io.mjs";
|
|
19
19
|
import { parsePlacementsJson } from "./placement-model.mjs";
|
|
20
20
|
|
|
21
21
|
const safeReadJson = (p) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; } };
|
|
@@ -118,9 +118,20 @@ export function placementFloorDecisionLines(P) { // @internal — driven by it
|
|
|
118
118
|
* enough for a stage to open. [] when nothing stands or nothing can be read. IMPURE (reads the run).
|
|
119
119
|
*/
|
|
120
120
|
export function standingPlacementDecisions(P) { // @internal — driven by its test with a run's own files
|
|
121
|
+
// A FORM THAT IS NOT THERE IS NOT "NOTHING SET ASIDE". Every failure here used to read as an empty list, so a writer in
|
|
122
|
+
// the experiment sandbox, which held no form, was told of no set-aside at all on a run whose placement had set 178
|
|
123
|
+
// aside. Where this run's placement records its decisions on a form (the era stamp), a missing or unreadable form is
|
|
124
|
+
// refused by name, and so is a failure reading what stands. A run from before the form had none, and its empty list
|
|
125
|
+
// is true.
|
|
126
|
+
const form = readPlacementForm(P.runDir);
|
|
127
|
+
if (form.error) throw new Error(`placement_form_unreadable: ${form.error} — placement's set-asides cannot be read, and an unread form is not "none set aside"`);
|
|
128
|
+
if (!form.present) {
|
|
129
|
+
if (placementFormStamp(P.runDir).required)
|
|
130
|
+
throw new Error(`placement_form_missing: this run's placement records its decisions on a form, and ${placementFormPaths(P.runDir).sidecar} is not there — refusing to read its absence as "none set aside"`);
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
if (!form.set_aside?.length) return [];
|
|
121
134
|
try {
|
|
122
|
-
const form = readPlacementForm(P.runDir);
|
|
123
|
-
if (!form.set_aside?.length) return [];
|
|
124
135
|
const input = readPlacementFormInput(P.runDir);
|
|
125
136
|
const index = buildSelectionIndex(input);
|
|
126
137
|
return standingSetAside(form.set_aside, input, { placedUris: placedUriRows(P).map((r) => r.uri), rows: form.rows }).flatMap((e) => {
|
|
@@ -132,7 +143,7 @@ export function standingPlacementDecisions(P) { // @internal — driven by its
|
|
|
132
143
|
return [{ kind: "name", record: e.set_aside, mark: c?.mark ?? null, owner: c?.owner ?? null, ground: e.ground,
|
|
133
144
|
...(FLOOR_GROUND_ON.includes(e.on) ? { on: e.on } : {}) }];
|
|
134
145
|
});
|
|
135
|
-
} catch {
|
|
146
|
+
} catch (e) { throw new Error(`placement_decisions_unreadable: ${String(e?.message ?? e).slice(0, 200)} — what placement set aside could not be worked out from its form`); }
|
|
136
147
|
}
|
|
137
148
|
|
|
138
149
|
/**
|
|
@@ -144,7 +155,11 @@ export function rankingNow(P) { // @internal — driven by its test with a run
|
|
|
144
155
|
const records = new Map();
|
|
145
156
|
for (const r of placedUriRows(P)) records.set(String(r.uri).toLowerCase(), `placed:${r.tier ?? ""}`);
|
|
146
157
|
const groups = new Map();
|
|
147
|
-
|
|
158
|
+
// The re-rank count runs inside the delivery loop, and a run always delivers: here a refusal counts no decision, and
|
|
159
|
+
// the writer's own message, which reads the same decisions, is where it is refused.
|
|
160
|
+
let decisions = [];
|
|
161
|
+
try { decisions = standingPlacementDecisions(P); } catch { decisions = []; }
|
|
162
|
+
for (const d of decisions) {
|
|
148
163
|
if (d.kind === "group") groups.set(d.group, { key: `${d.ground}|${d.covers_lines}|${d.covered_lines_hash}`, records: d.records });
|
|
149
164
|
else records.set(String(d.record).toLowerCase(), `set aside${d.on ? ` on its ${d.on}` : ""}: ${d.ground}`);
|
|
150
165
|
}
|
|
@@ -1256,11 +1256,18 @@ export function correctionConsistencyChecks({ reportMd, clientSummaryMd, auditMd
|
|
|
1256
1256
|
const report = String(reportMd ?? "");
|
|
1257
1257
|
const cs = String(clientSummaryMd ?? "");
|
|
1258
1258
|
const audit = String(auditMd ?? "");
|
|
1259
|
+
// A CARD THAT CARRIES ITS OWN `- ord:` LINE IS THE FINDING THAT LINE NAMES. One owner can hold the same mark in
|
|
1260
|
+
// several records, each its own finding, and a live one keeps its card under the same head when another is
|
|
1261
|
+
// withdrawn: read by its head, it was the withdrawn finding back, and that one false failure closed the client
|
|
1262
|
+
// export of a crowded test run (2026-10-28). The ord line already answers for such a card, so the head match is
|
|
1263
|
+
// for a card with none, the older card shape.
|
|
1264
|
+
const unnumberedHeads = [...report.matchAll(/^##\s+([^\n]+)\n?((?:- [^\n]*\n?)*)/gm)]
|
|
1265
|
+
.filter(([, , meta]) => !/^-\s*ord:/m.test(meta)).map(([, h]) => h);
|
|
1259
1266
|
for (const f of withdrawn) {
|
|
1260
1267
|
const ord = f.ordinal;
|
|
1261
1268
|
const markN = norm(f.mark), ownerN = norm(f.owner?.name);
|
|
1262
1269
|
const ordHit = ord != null && new RegExp(`^-\\s*ord:\\s*${ord}\\s*$`, "m").test(report);
|
|
1263
|
-
const headHit =
|
|
1270
|
+
const headHit = unnumberedHeads.some((h) => {
|
|
1264
1271
|
const hN = norm(h);
|
|
1265
1272
|
return markN && ownerN && hN.includes(markN) && hN.includes(ownerN);
|
|
1266
1273
|
});
|
|
@@ -146,13 +146,16 @@ export function accountedUris(model, owed = []) {
|
|
|
146
146
|
* THE SHEET-2 ROWS THE DRIVER WRITES. A sheet-2 entry is adopted as placement placed it: one row, on the entry's first
|
|
147
147
|
* live record (else its first), carrying placement's reason word for word, with verify "yes" because nothing has checked
|
|
148
148
|
* it against the live register. An entry the write-up overrode gets none: any row of its own naming any of the entry's
|
|
149
|
-
* records, or
|
|
150
|
-
*
|
|
149
|
+
* records, or an OVERRODE resolution naming one, is its ending instead. An ADOPTED resolution keeps placement's
|
|
150
|
+
* placement, so the entry keeps its row: counting it as an ending rendered an adoption exactly as an override, and
|
|
151
|
+
* kept entries vanished from the table the writer is owed from. An entry whose record the band does not hold gets
|
|
152
|
+
* none. PURE.
|
|
151
153
|
*/
|
|
152
154
|
export function adoptedRows(model, facts = emptyFacts()) {
|
|
153
155
|
const adopt = Array.isArray(facts?.adopt) ? facts.adopt : [];
|
|
154
156
|
if (!adopt.length) return [];
|
|
155
|
-
const
|
|
157
|
+
const overrides = { ...model, disagreement_resolutions: (model?.disagreement_resolutions ?? []).filter((d) => d?.decision !== "ADOPTED") };
|
|
158
|
+
const named = accountedUris(overrides, adopt.flatMap((e) => e.uris));
|
|
156
159
|
const out = [];
|
|
157
160
|
for (const e of adopt) {
|
|
158
161
|
if (e.uris.some((u) => named.has(u))) continue;
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// A first attempt has none, and its message is unchanged.
|
|
26
26
|
//
|
|
27
27
|
// A pure leaf: node:fs, node:path and shared/driver-dir.mjs only, so the research server can import it.
|
|
28
|
-
import { appendFileSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
28
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
29
29
|
import { dirname, join } from "node:path";
|
|
30
30
|
import { driverDir } from "../shared/driver-dir.mjs";
|
|
31
31
|
|
|
@@ -51,9 +51,13 @@ export function keepResearchAnswer(runDir, { session = null, agent = null, task,
|
|
|
51
51
|
/**
|
|
52
52
|
* The research answers the session `sessionKey` and its retries got, oldest first, one per question: the latest answer
|
|
53
53
|
* to each exact question. [] when there are none, or no record. A torn line is skipped. IMPURE (reads the record).
|
|
54
|
+
*
|
|
55
|
+
* `ofRun` widens it to every stage of this run: the writer is told to use an answer this run already holds, from an
|
|
56
|
+
* earlier stage as from an earlier attempt, and it cannot open `_driver/` itself. The record is the run's own file, so
|
|
57
|
+
* nothing another run asked can be in it.
|
|
54
58
|
* @returns {{ task: string, text: string, ts: string }[]}
|
|
55
59
|
*/
|
|
56
|
-
export function researchAnswersFor(runDir, sessionKey) {
|
|
60
|
+
export function researchAnswersFor(runDir, sessionKey, { ofRun = false } = {}) {
|
|
57
61
|
if (!runDir || !sessionKey) return [];
|
|
58
62
|
let raw = "";
|
|
59
63
|
try { raw = readFileSync(driverDir(runDir, RESEARCH_ANSWERS_FILE), "utf8"); } catch { return []; }
|
|
@@ -63,7 +67,7 @@ export function researchAnswersFor(runDir, sessionKey) {
|
|
|
63
67
|
if (!line.trim()) continue;
|
|
64
68
|
let row;
|
|
65
69
|
try { row = JSON.parse(line); } catch { continue; }
|
|
66
|
-
if (!own(row?.session) || typeof row.task !== "string" || !row.task.trim() || !isResearchAnswer(row.text)) continue;
|
|
70
|
+
if ((!ofRun && !own(row?.session)) || typeof row.task !== "string" || !row.task.trim() || !isResearchAnswer(row.text)) continue;
|
|
67
71
|
byTask.delete(row.task); // the latest answer to a question takes the place of an earlier one
|
|
68
72
|
byTask.set(row.task, { task: row.task, text: row.text, ts: String(row.ts ?? "") });
|
|
69
73
|
}
|
|
@@ -83,8 +87,8 @@ export const researchAnswerLine = (n, { task, text }) => `Question ${n}: ${oneLi
|
|
|
83
87
|
* folder, one question and its answer per line, and return how many. Nothing is written when there are none, and 0 is
|
|
84
88
|
* returned when the file could not be written: a message must never name a file that is not there. IMPURE.
|
|
85
89
|
*/
|
|
86
|
-
export function handResearchAnswers(runDir, sessionKey) {
|
|
87
|
-
const answers = researchAnswersFor(runDir, sessionKey);
|
|
90
|
+
export function handResearchAnswers(runDir, sessionKey, opts = {}) {
|
|
91
|
+
const answers = researchAnswersFor(runDir, sessionKey, opts);
|
|
88
92
|
if (!answers.length) return 0;
|
|
89
93
|
try {
|
|
90
94
|
const p = join(runDir, RESEARCH_ANSWERS_MD);
|
|
@@ -101,8 +105,51 @@ export function handResearchAnswers(runDir, sessionKey) {
|
|
|
101
105
|
export function researchAnsweredBlock(n) {
|
|
102
106
|
if (!(n > 0)) return "";
|
|
103
107
|
const asked = n === 1 ? "asked 1 question and got its answer" : `asked ${n} questions and got their answers`;
|
|
104
|
-
return `RESEARCH THIS RUN HAS ALREADY ANSWERED:
|
|
108
|
+
return `RESEARCH THIS RUN HAS ALREADY ANSWERED: earlier stages or attempts of this run ${asked}. They are in ${RESEARCH_ANSWERS_MD}, `
|
|
105
109
|
+ "one question and its answer per line. Before you ask about a finding's owner and mark, search that file for the owner's name "
|
|
106
|
-
+ "with search_run_artifacts. Where one of them
|
|
110
|
+
+ "with search_run_artifacts. Where one of them answers the same owner, mark, goods, territory and material time question, use its answer as "
|
|
107
111
|
+ "that finding's check, cite its URL and record it as you would a fresh one; ask only what none of them covers.";
|
|
108
112
|
}
|
|
113
|
+
|
|
114
|
+
// ── A REPLAY ANSWERS FROM THE RUN'S OWN RESEARCH, AND ASKS NOTHING NEW ────────────────────────────────────────────────
|
|
115
|
+
//
|
|
116
|
+
// A stage replayed offline in the experiment sandbox would send its research questions out again: a cost, and an
|
|
117
|
+
// answer that is not the one the run got. With `--research replay` the sandbox holds the run's own answers and says so
|
|
118
|
+
// in this marker, and the research server answers a question the run already asked from what the run got, and refuses
|
|
119
|
+
// a question it did not ask, sending nothing out. Only the sandbox writes the marker; a run never does.
|
|
120
|
+
export const RESEARCH_REPLAY_FILE = "research-replay.json";
|
|
121
|
+
|
|
122
|
+
/** True when this run folder is a replay of a run's research. IMPURE (reads the marker). */
|
|
123
|
+
export const researchReplayOn = (runDir) => Boolean(runDir) && existsSync(driverDir(runDir, RESEARCH_REPLAY_FILE));
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The run's latest answer to exactly this question, whichever of its stages asked it, or null. The question is
|
|
127
|
+
* matched as written, trimmed at its ends: a question worded differently is a new question. IMPURE (reads the record).
|
|
128
|
+
*/
|
|
129
|
+
export function savedResearchAnswer(runDir, task) {
|
|
130
|
+
const want = typeof task === "string" ? task.trim() : "";
|
|
131
|
+
if (!runDir || !want) return null;
|
|
132
|
+
let raw = "";
|
|
133
|
+
try { raw = readFileSync(driverDir(runDir, RESEARCH_ANSWERS_FILE), "utf8"); } catch { return null; }
|
|
134
|
+
let found = null;
|
|
135
|
+
for (const line of raw.split("\n")) {
|
|
136
|
+
if (!line.trim()) continue;
|
|
137
|
+
let row;
|
|
138
|
+
try { row = JSON.parse(line); } catch { continue; }
|
|
139
|
+
if (typeof row?.task !== "string" || row.task.trim() !== want || !isResearchAnswer(row.text)) continue;
|
|
140
|
+
if (!found || String(row.ts ?? "") >= String(found.ts ?? "")) found = { task: row.task, text: row.text, ts: String(row.ts ?? ""), session: row.session ?? null };
|
|
141
|
+
}
|
|
142
|
+
return found;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** How many questions the run's record answers: the experiment refuses a replay with nothing to replay. IMPURE. */
|
|
146
|
+
export function savedResearchCount(runDir) {
|
|
147
|
+
let raw = "";
|
|
148
|
+
try { raw = readFileSync(driverDir(runDir, RESEARCH_ANSWERS_FILE), "utf8"); } catch { return 0; }
|
|
149
|
+
const tasks = new Set();
|
|
150
|
+
for (const line of raw.split("\n")) {
|
|
151
|
+
try { const row = JSON.parse(line); if (typeof row?.task === "string" && row.task.trim() && isResearchAnswer(row.text)) tasks.add(row.task.trim()); }
|
|
152
|
+
catch { /* a torn line answers nothing */ }
|
|
153
|
+
}
|
|
154
|
+
return tasks.size;
|
|
155
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
//
|
|
4
|
+
// saved-findings-block.mjs — what a later attempt of the writer is told about the findings this run already saved.
|
|
5
|
+
//
|
|
6
|
+
// The writer saves each finding as it completes it (synthesis-record.mjs, `saveFindings`), and an attempt that is
|
|
7
|
+
// stopped keeps them. A later attempt is handed them as a file, never in its message: a saved finding carries its
|
|
8
|
+
// evidence, and a few hundred of them would swamp the message. The message says how many there are, where they are,
|
|
9
|
+
// that they stand, and which positions are still to settle. A first attempt has none, and its message is unchanged.
|
|
10
|
+
// The model reads this block, so its words change only as model-facing wording does.
|
|
11
|
+
//
|
|
12
|
+
// A pure leaf, so the stage table can import it.
|
|
13
|
+
|
|
14
|
+
/** The file in the run folder a later attempt is handed its run's saved findings in. */
|
|
15
|
+
export const SAVED_FINDINGS_FILE = "synthesis-saved.json";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The block a later attempt's message ends with, or "" when nothing is saved. `h` is what
|
|
19
|
+
* synthesis-record.mjs's `handSavedFindings` returned: `{ findings, declined, outstanding }`, where `outstanding` is
|
|
20
|
+
* null when the run cannot say which positions are still owed. PURE.
|
|
21
|
+
*/
|
|
22
|
+
export function savedFindingsBlock(h) {
|
|
23
|
+
if (!h || !(h.findings > 0)) return "";
|
|
24
|
+
const saved = h.findings === 1 ? "saved 1 finding" : `saved ${h.findings} findings`;
|
|
25
|
+
const declined = h.declined > 0 ? ` and declined ${h.declined === 1 ? "1 record" : `${h.declined} records`} through record_declination` : "";
|
|
26
|
+
const shown = Array.isArray(h.outstanding) ? h.outstanding.slice(0, 40) : [];
|
|
27
|
+
const more = Array.isArray(h.outstanding) && h.outstanding.length > shown.length ? `, and ${h.outstanding.length - shown.length} more` : "";
|
|
28
|
+
const owed = !Array.isArray(h.outstanding) ? ""
|
|
29
|
+
: h.outstanding.length ? ` Still to settle: ${h.outstanding.length === 1 ? "1 position" : `${h.outstanding.length} positions`}: ${shown.join(", ")}${more}.`
|
|
30
|
+
: " Every position you were handed is already settled by a saved finding or a declination.";
|
|
31
|
+
return `SAVED IN THIS RUN: an earlier attempt of this stage ${saved} through record_synthesis${declined}. The saved findings are in ${SAVED_FINDINGS_FILE}, `
|
|
32
|
+
+ "each as it was accepted, with its evidence. They stand: save one again only for identified contrary evidence, a reasoning error "
|
|
33
|
+
+ `or an applicable change in the instructions.${owed} Save each remaining finding as you complete it. Then send the whole record, `
|
|
34
|
+
+ "`narrative` with `findings`: it takes every saved finding for a position its `findings[]` does not carry, so that list need hold "
|
|
35
|
+
+ "only the findings not saved yet, and the overall conclusions answer for the complete set.";
|
|
36
|
+
}
|
|
@@ -218,7 +218,9 @@ Every cross-check (including "checked, found nothing") logged in the unified aud
|
|
|
218
218
|
|
|
219
219
|
## Step 3.5 — Actual-use and enforcement check (mandatory for register hits rated medium risk or higher)
|
|
220
220
|
|
|
221
|
-
|
|
221
|
+
For **every register finding** advisory-rated on a rung **the framework rates medium risk or higher** (the stage message names those rungs; a rung below that is not checked, on any client's ladder), run a marketplace check via `perplexity_research` to determine whether the registered mark is **actually being used in the marketplace within the scope of the current search** (i.e. for the goods/services and field this clearance targets). **The same query also asks for the owner's enforcement record:** whether the owner has opposed, disputed or sued over this mark or a similar one.
|
|
222
|
+
|
|
223
|
+
Apply the existing triggers for actual-use, enforcement, and owner checks after the relevant legal assessment. First use a suitable answer already held by this run, including an earlier stage or attempt, where it answers the same owner, mark, goods, territory, and material time question. A source requirement is not a requirement for a new call. One answer may support several findings only where it answers each finding's question. Ask only what remains unanswered and record the result against every finding it supports.
|
|
222
224
|
|
|
223
225
|
Example: a JELLY register hit owned by Jellycat (a toy company) for Class 9 video game software — check whether Jellycat actually sells or markets video games under JELLY. If they don't (their commercial use is toys), note the filed-vs-used distinction in Key Factors and weigh it as a Stage-2 factor together with revocation-vulnerability — no automatic downgrade (see `synthesis-rules.md` → *Actual-use assessment results*).
|
|
224
226
|
|
|
@@ -245,7 +247,7 @@ If the marketplace query genuinely returned nothing, write the honest value verb
|
|
|
245
247
|
|
|
246
248
|
The driver's `narrative` validator (`verify.mjs` → `findUseCheckViolations`) **rejects the whole narrative** if a
|
|
247
249
|
Composite 3+ use-negative finding lacks this line, so the synthesis stage cannot ship an *inferred* "no use" —
|
|
248
|
-
it must actually run the scoped query (owner + mark + goods/field) and cite the result, or state the honest
|
|
250
|
+
it must actually run the scoped query (owner + mark + goods/field), unless an answer this run already holds answers that question, and cite the result, or state the honest
|
|
249
251
|
"no result". Never dress an un-run check as a clean negative. Also mirror the result into the matching `# Actions`
|
|
250
252
|
item: "confirm the owner's actual use" becomes the stated result, not a task handed onward (this is what fills
|
|
251
253
|
spec 09's "Checks we ran — what we found" bucket). On a perplexity outage the "no result" value is the accepted
|
package/driver/stage-context.mjs
CHANGED
|
@@ -177,6 +177,7 @@ export const TOOL_GROUP_EDGES = {
|
|
|
177
177
|
// the policy asks on the question's own depth instead, so the arm would measure a different tier.
|
|
178
178
|
perplexity: (P) => [
|
|
179
179
|
{ path: driverDir(P.runDir, "search-policy.json"), why: "perplexity_research sends every question on the tier the run's frozen policy pins (web.questionPreset); without it a question goes out on its own depth" },
|
|
180
|
+
{ path: driverDir(P.runDir, "research-answers.jsonl"), why: "under --research replay, perplexity_research answers a question the run already asked from the answer the run kept here (research-answers.mjs); without it a replay has nothing to answer from" },
|
|
180
181
|
],
|
|
181
182
|
declination: (P) => [
|
|
182
183
|
{ path: driverDir(P.runDir, "declination-spec.json"), why: "record_declination resolves every row_index against this spec; without it the tool refuses the call and the seat cannot decline at all" },
|
|
@@ -191,6 +192,21 @@ export const TOOL_GROUP_EDGES = {
|
|
|
191
192
|
};
|
|
192
193
|
|
|
193
194
|
// ── the edges a declaration cannot carry ─────────────────────────────────────────────────────────────
|
|
195
|
+
// PLACEMENT'S SET-ASIDES, as the reading step and the writer are told them when their message is built
|
|
196
|
+
// (placement-decisions.mjs, `standingPlacementDecisions`): the form's driver copy, judged against the shape, the
|
|
197
|
+
// positions, the scope, what the band tools served and the goods evidence, under the era stamp that says a form is
|
|
198
|
+
// required. An arm without them was told of no set-aside at all.
|
|
199
|
+
const placementSetAsideReads = (P) => [
|
|
200
|
+
{ path: driverDir(P.runDir, "placement-form.form.json"), kind: "driver-side", why: "standingPlacementDecisions reads placement's set-asides from the form's driver copy" },
|
|
201
|
+
{ path: driverDir(P.runDir, "stage-contracts.json"), kind: "driver-side", why: "the era stamp that says this run's placement records a form; without it a missing form reads as none set aside" },
|
|
202
|
+
{ path: P.placementModel, kind: "driver-side", why: "the placed records a set-aside is checked against (placedUriRows)" },
|
|
203
|
+
{ path: P.bandShape, kind: "driver-side", why: "readPlacementFormInput: the floors and groups a set-aside is judged against" },
|
|
204
|
+
{ path: P.registerPositions, kind: "driver-side", why: "readPlacementFormInput: the positions a named set-aside resolves to" },
|
|
205
|
+
{ path: driverDir(P.runDir, "instructed-scope.json"), kind: "driver-side", why: "readPlacementFormInput: the applied-for marks a ground is refused on" },
|
|
206
|
+
{ path: driverDir(P.runDir, "reading-log.jsonl"), kind: "driver-side", why: "readServed: the lines a group row may cover are the lines placement was shown" },
|
|
207
|
+
{ path: driverDir(P.runDir, "goods-evidence.json"), kind: "driver-side", why: "readGoodsStates: a ground on a record's goods is checked against what its goods rest on" },
|
|
208
|
+
];
|
|
209
|
+
|
|
194
210
|
const UNDECLARED = {
|
|
195
211
|
// The verbatim forward is named in the prompt only when job.rawRequest exists — and matter-frame,
|
|
196
212
|
// unlike blind-frame, never declared it at all (stages.mjs "matter-frame": []).
|
|
@@ -224,6 +240,7 @@ const UNDECLARED = {
|
|
|
224
240
|
// and one that built it and lost it in the copy refuses by name. Listing it costs nothing on the first and
|
|
225
241
|
// is the whole point on the second.
|
|
226
242
|
"register-digest": (P) => [
|
|
243
|
+
...placementSetAsideReads(P),
|
|
227
244
|
{ path: P.registerHitList,
|
|
228
245
|
why: "the digest's own message names this file and asks the model to mark lines on it (hit-list.mjs reads the marks back); without it in the copy a marking lands nowhere and the stage passes having been given a different input than the real one" },
|
|
229
246
|
],
|
|
@@ -231,6 +248,7 @@ const UNDECLARED = {
|
|
|
231
248
|
// non-zero count (stages.mjs synthesis message). Undeclared on purpose — it is aim-attention only and
|
|
232
249
|
// must never move a freshness stamp — but a sandbox without it hands a different prompt.
|
|
233
250
|
synthesis: (P) => [
|
|
251
|
+
...placementSetAsideReads(P),
|
|
234
252
|
{ path: P.enforcerSignals, kind: "conditional",
|
|
235
253
|
why: "named in the prompt only when ctx.enforcerSignals > 0; aim-attention telemetry, deliberately outside the freshness map" },
|
|
236
254
|
// The job's own scope. The prompt never names it, so nothing declared it, and the sandbox rebuilt the
|
package/driver/stages.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { join, basename } from "node:path";
|
|
18
18
|
import { driverRel } from "../shared/driver-dir.mjs"; //
|
|
19
|
-
import { researchAnsweredBlock } from "./research-answers.mjs"; // the research an earlier attempt got, handed to a later one
|
|
19
|
+
import { researchAnsweredBlock } from "./research-answers.mjs"; import { savedFindingsBlock } from "./saved-findings-block.mjs"; // the research an earlier attempt got, and the findings it saved, handed to a later one
|
|
20
20
|
import { awaitsReadingTurn } from "../providers/_shared/plan-guards.mjs"; // — a guard the dictation misreads is a question the model never knows it may ask
|
|
21
21
|
import { isNonLatinTerm } from "../providers/_shared/script-form.mjs";
|
|
22
22
|
import { latinQuestionsOfRomanisedForm } from "./register-plan.mjs"; // sentence 3 — the Latin question a script question repeats
|
|
@@ -3099,7 +3099,7 @@ export const STAGES = {
|
|
|
3099
3099
|
why: "THE SECOND frame_scope_missing. The driver holds the value in _driver/framework.json, writes it into the dispatch, asks the model to retype it, and fails the file when the retyping does not match its own copy. The audit table groups it under the tool-written envelope row; pre-bound is the sharper discharge and either removes it from the model",
|
|
3100
3100
|
},
|
|
3101
3101
|
},
|
|
3102
|
-
message: ({ paths: P, job, customerUnknown, profile, intakeAsks, enforcerSignals, framework, frameworkMethod, jxAim, registerOnly, crowdContext, dispatchBlocks, findingsSurface, depth, placementSetAsides, researchAnswered }) => lines(
|
|
3102
|
+
message: ({ paths: P, job, customerUnknown, profile, intakeAsks, enforcerSignals, framework, frameworkMethod, jxAim, registerOnly, crowdContext, dispatchBlocks, findingsSurface, depth, placementSetAsides, researchAnswered, savedSynthesis }) => lines(
|
|
3103
3103
|
// C4 — pharma matters (class 5 / pharma-shaped goods) FORCE-READ the field module: the
|
|
3104
3104
|
// therapeutic-goods discipline, no-use ≠ safety (pipelines run 5-10y), practitioner/pharmacist
|
|
3105
3105
|
// confusion. Conditional read (frameworkFor pattern); doctrine TABLES live elsewhere, untouched.
|
|
@@ -3122,7 +3122,7 @@ export const STAGES = {
|
|
|
3122
3122
|
// WS2 (spec 11) — ENFORCED, not advisory: validators.narrative HARD-REJECTS this file if a
|
|
3123
3123
|
// use-dependent finding above the framework's lowest band asserts a use-negative without a searched
|
|
3124
3124
|
// source line. So actually run the query.
|
|
3125
|
-
`USE-CHECK (enforced): run the scoped use check (phase2-execution.md Step 3.5) on EVERY finding banded ${(framework && materialBandPhrase(framework)) || "medium risk or higher"} — the rungs this framework rates medium risk or higher; a lower rung is not checked — and ask in the SAME query for the owner's enforcement record (oppositions, disputes or court actions over this mark or a similar one). After the search, use is "confirmed" or "not-confirmed", never "unknown". What the query finds about enforcement is a LEAD under ENFORCEMENT AUTHORITY below: enforcer basis "inferred-from-signal", its URL cited inline in that finding's prose; where it finds none, the enforcer is "low" with basis "inferred-from-signal" and the finding says the search found no enforcement record. ALSO, for EVERY finding whose mitigant/verdict turns on the ABSENCE of use (regardless of its band) ("not in actual use", "unused", "no marketplace use found", non-use revocation/cancellation as the mitigant, "owner's use unknown"), run ONE scoped \`perplexity_research\` query (owner + mark + the goods/field) and END that finding's actual-use line with a literal "- **Use-check source:** <result URL | "perplexity_research — no result">" line. Mirror the result into the matching # Actions item (the deferred "confirm the owner's use" becomes a stated result). NEVER assert a use-negative you did not search — an inference from the owner's profile is not a searched result. On a perplexity outage write "- **Use-check source:** perplexity_research — no result" and proceed (the run still delivers). ALSO populate that finding's use_check field in findings.json: {"source": "<the result URL | perplexity_research — no result>"} — the structured field is the authoritative cite the report renders from (the prose line is for narrative readability).`,
|
|
3125
|
+
`USE-CHECK (enforced): run the scoped use check (phase2-execution.md Step 3.5) on EVERY finding banded ${(framework && materialBandPhrase(framework)) || "medium risk or higher"} — the rungs this framework rates medium risk or higher; a lower rung is not checked — and ask in the SAME query for the owner's enforcement record (oppositions, disputes or court actions over this mark or a similar one). ${USE_CHECK_ANSWERS_HELD} After the search, use is "confirmed" or "not-confirmed", never "unknown". What the query finds about enforcement is a LEAD under ENFORCEMENT AUTHORITY below: enforcer basis "inferred-from-signal", its URL cited inline in that finding's prose; where it finds none, the enforcer is "low" with basis "inferred-from-signal" and the finding says the search found no enforcement record. ALSO, for EVERY finding whose mitigant/verdict turns on the ABSENCE of use (regardless of its band) ("not in actual use", "unused", "no marketplace use found", non-use revocation/cancellation as the mitigant, "owner's use unknown"), run ONE scoped \`perplexity_research\` query (owner + mark + the goods/field) unless an answer this run already holds answers that question, and END that finding's actual-use line with a literal "- **Use-check source:** <result URL | "perplexity_research — no result">" line. Mirror the result into the matching # Actions item (the deferred "confirm the owner's use" becomes a stated result). NEVER assert a use-negative you did not search — an inference from the owner's profile is not a searched result. On a perplexity outage write "- **Use-check source:** perplexity_research — no result" and proceed (the run still delivers). ALSO populate that finding's use_check field in findings.json: {"source": "<the result URL | perplexity_research — no result>"} — the structured field is the authoritative cite the report renders from (the prose line is for narrative readability).`,
|
|
3126
3126
|
// Spec-v3 A4 — ENFORCED the same way (validators.narrative): own-rights reliance needs its evidence.
|
|
3127
3127
|
// PR-8 review fix: synthesis holds the FROZEN band, not the live register — and the band is class-
|
|
3128
3128
|
// and mark-text-scoped to this matter's dispatched slices, so the client's OWN house-mark
|
|
@@ -3150,7 +3150,7 @@ export const STAGES = {
|
|
|
3150
3150
|
// (house-brand attachment / seasonality / scale). Context, never a rule that decides a finding.
|
|
3151
3151
|
job?.campaignShape ? `Stated campaign shape (verbatim from intake — how the mark will be deployed; facts, not judgment): ${job.campaignShape}` : "",
|
|
3152
3152
|
`Inputs: register findings ${P.registerFindings}${registerOnly ? "" : `; common-law ${P.commonLaw}`}; placements ${P.placement} (structured tiers + reasons: ${P.placementModel}); matter frame ${P.matterContext}; manifest ${P.variantManifest}.`,
|
|
3153
|
-
placementSetAsideBlock(placementSetAsides),
|
|
3153
|
+
placementSetAsideBlock(placementSetAsides), WRITER_STARTS_FROM_THE_WORK, WRITER_RECORDS_AS_IT_GOES,
|
|
3154
3154
|
// The skeptic's shipped sentence, word for word: a granted tool is named, or the seat cannot know it holds it.
|
|
3155
3155
|
`Re-read any of this run's own artifacts with the \`search_run_artifacts\` tool — ONE file per call, named relative to the run directory (e.g. "register-findings.md"), with \`terms\` as literal substrings OR-matched per line.`,
|
|
3156
3156
|
PLACEMENT_BORDERLINE_NOTE,
|
|
@@ -3208,8 +3208,8 @@ export const STAGES = {
|
|
|
3208
3208
|
// judgment-relocation (2026-06-23): you are Layer B — the ONE judgment layer. The funnel ENUMERATED the
|
|
3209
3209
|
// dangerous named band or reported HONEST INCOMPLETENESS; you read the complete material and decide
|
|
3210
3210
|
// relevance, risk, sufficiency and prioritisation. PR-8: the reading goes through the band tools
|
|
3211
|
-
// (
|
|
3212
|
-
|
|
3211
|
+
// (logged lookups, as an assessment needs them) — nothing was sampled away upstream, nothing sliced by hand.
|
|
3212
|
+
BAND_READING_CONTRACT_FOR_THE_WRITER,
|
|
3213
3213
|
// PR-8: synthesis holds NO live register tools — by design, never an outage. The funnel owns
|
|
3214
3214
|
// enumeration; a register check this stage still wants is either answerable from the frozen
|
|
3215
3215
|
// material (the band tools) or it is NEW SEARCH WORK, which enters only through the supplemental
|
|
@@ -3438,13 +3438,13 @@ export const STAGES = {
|
|
|
3438
3438
|
// MEANS — the net contract, the band contract, the meters, the disposition logic, the voice
|
|
3439
3439
|
// rules. What goes is the seat formatting them into a file for the driver to read back.
|
|
3440
3440
|
`RECORD YOUR WORK WITH \`record_synthesis\`. THE DISPATCH NAMES NO FILE FOR YOU TO WRITE, deliberately — the driver renders both the narrative and the findings record from what you send. You hold no Write or Edit tool for either and nothing you write by hand is read.`,
|
|
3441
|
-
`WHAT THE CALL TAKES. \`narrative\`: your CROSS-FINDING read as sections — \`spine\` (the dominant-element spine), \`verdict\` (what the findings together mean for this client), \`coverage.read\` (the honest coverage prose) and \`calibration\` ([{challenge, answer}]). \`findings\`: the findings document exactly as specified above — schema_version, rated_under_framework, findings[], coverage[], ask_answers[] and the top-level registers. It is the same values it always was; it arrives on the call instead of as a file you write.`,
|
|
3441
|
+
`WHAT THE CALL TAKES. \`narrative\`: your CROSS-FINDING read as sections — \`spine\` (the dominant-element spine), \`verdict\` (what the findings together mean for this client), \`coverage.read\` (the honest coverage prose) and \`calibration\` ([{challenge, answer}]). \`findings\`: the findings document exactly as specified above — schema_version, rated_under_framework, findings[], coverage[], ask_answers[] and the top-level registers. It is the same values it always was; it arrives on the call instead of as a file you write. \`save_findings\`: a finding you have completed, alone or a few at a time, saved before the whole record exists; each is checked as the record checks a finding and kept as soon as it passes, and the call carrying \`narrative\` with \`findings\` takes every saved finding for a position it does not carry.`,
|
|
3442
3442
|
`THE PER-FINDING WRITE-UPS ARE NOT A FIELD, and that is a ruling rather than an omission: the report card holds the single authored wording for a finding and the typed fields carry the reads it is written from. Do not restate a finding's reasoning in the narrative sections — the spine is what the findings mean TOGETHER, which is the read no other artifact on this run holds.`,
|
|
3443
3443
|
`COVERAGE ROWS AND ASK ANSWERS ARE SENT ONCE, ON THE RECORD, AND THE DRIVER RENDERS THEM INTO THE NARRATIVE TOO. Do not repeat them as narrative sections — a second authored copy is refused, because two copies of one statement can disagree and it is the record's copy that ships to the report, the workbook and the portal.`,
|
|
3444
3444
|
`YOUR COVERAGE ROWS ARE JOINED TO THIS RUN'S PLAN-EXECUTION RECEIPT AS YOU SEND THEM. \`confirmed-clean\` asserts a search ran and came back empty, so a confirmed-clean row naming a slice the receipt does not record as executed is REFUSED — in this turn, where restating it costs nothing, rather than at a gate whose only repair is re-asking this whole stage. Name the area as the receipt names the slice. The honest alternatives are \`coverage-limited\` and \`not-searched\`, and neither is a worse answer than a clean claim that is not true.`,
|
|
3445
3445
|
// THE RESEARCH AN EARLIER ATTEMPT GOT, handed on as a file (research-answers.mjs). Last, and absent on a first
|
|
3446
3446
|
// attempt, whose message is unchanged.
|
|
3447
|
-
researchAnsweredBlock(researchAnswered),
|
|
3447
|
+
researchAnsweredBlock(researchAnswered), savedFindingsBlock(savedSynthesis),
|
|
3448
3448
|
),
|
|
3449
3449
|
// A FRESH RETRY IS REBUILT, so it is handed the research its earlier attempts got. Nothing else in this stage's
|
|
3450
3450
|
// context changes between attempts: the findings surface and its positions are frozen for the stage, and placement's
|
|
@@ -4831,3 +4831,31 @@ export const REFUTATION_RECORDING_LINES = Object.freeze([
|
|
|
4831
4831
|
`TYPE EACH CORRECTION: \`kind\` is ONE of coverage-disposition | fact | rating | narrative — pick the one your own legal read says the correction IS: coverage-disposition (a coverage row / disposition placement is wrong or dishonest), fact (a factual defect — record values, owners, statuses, dates, grounding), rating (a band/rating you challenge), narrative (prose, structure or voice of the narrative). DECLARE the kind from what the correction is about, never from how severe it is.`,
|
|
4832
4832
|
`A BLOCKING verdict REQUIRES at least one flag. A refusal to sign that names nothing is refused by the call itself, in this turn, rather than at the gate — where its only repair is one forced re-ask of this whole stage.`,
|
|
4833
4833
|
]);
|
|
4834
|
+
|
|
4835
|
+
// ── THE WRITER WRITES FROM THE WORK IT IS HANDED (ruled 2026-10-27) ─────────────────────────────────────────────────
|
|
4836
|
+
//
|
|
4837
|
+
// On a test run of a crowded matter the writer's second try made 1,095 record reads of 707 distinct records and 117
|
|
4838
|
+
// web research calls, and recorded nothing before its time ran out. It was re-reading the matter from the start: the
|
|
4839
|
+
// band's shape, then each position's records one by one, then the owners. The steps before it had already settled
|
|
4840
|
+
// relevance and kept the evidence. So the writer starts from that work: it inherits supported decisions, completes the
|
|
4841
|
+
// assessment that is its own, and reads or searches further only where the evidence it holds does not answer a
|
|
4842
|
+
// question. Its duties do not change: the ratings, the cross-finding read and the required checks stay with it.
|
|
4843
|
+
export const WRITER_STARTS_FROM_THE_WORK = lines(
|
|
4844
|
+
"Start from the retained entries, the recorded decisions, and the evidence this run already holds. Use completed work in your judgment and write what the findings mean together for this client. Complete the legal and practical assessment that remains yours under the framework.",
|
|
4845
|
+
"Reopen a settled decision only for identified contrary evidence or an identified error in its reasoning. Complete an outstanding required question from the evidence already held; read or search further only where that evidence does not answer it. Do not repeat the investigation because a new stage or attempt has started.",
|
|
4846
|
+
"Assess each retained position on its relevant evidence. A list of its constituent records does not require you to open every record. Open an additional record when a difference in its goods, status, priority, territory, identity, or other material evidence could change the assessment and the supplied evidence does not resolve it. Missing or incomplete evidence does not establish an exclusion or a clean result.",
|
|
4847
|
+
);
|
|
4848
|
+
// The band as the writer is told of it: what the tools are for, never a read it owes. The contract the steps before it
|
|
4849
|
+
// read begins "call `band_shape` FIRST" and says each part of a large shape names the next call. The standards that
|
|
4850
|
+
// contract sets for any reading are kept word for word, cut from it so the two cannot drift apart. The name keeps
|
|
4851
|
+
// "BAND_READING_CONTRACT" in it: the stages that read the band are found by it (BAND_READING_STAGES, band-size.mjs).
|
|
4852
|
+
export const BAND_READING_STANDARDS = BAND_READING_CONTRACT.slice(BAND_READING_CONTRACT.indexOf("A crowd descriptor is an open slice"));
|
|
4853
|
+
export const BAND_READING_CONTRACT_FOR_THE_WRITER = "THE REGISTER BAND: Use the band tools (`band_shape`, `band_lookup`, `band_record`) when a specific assessment needs evidence beyond the material handed to you. The available groups and pages are ways to find that evidence, not a list you must read to completion. "
|
|
4854
|
+
+ BAND_READING_STANDARDS;
|
|
4855
|
+
// An answer the run already holds can be a finding's check (ruled 2026-10-27): a use-check source needs evidence, not
|
|
4856
|
+
// a fresh call. The triggers are unchanged. Step 3.5 of phase2-execution.md carries the same words.
|
|
4857
|
+
export const USE_CHECK_ANSWERS_HELD = "Apply the existing triggers for actual-use, enforcement, and owner checks after the relevant legal assessment. First use a suitable answer already held by this run, including an earlier stage or attempt, where it answers the same owner, mark, goods, territory, and material time question. A source requirement is not a requirement for a new call. One answer may support several findings only where it answers each finding's question. Ask only what remains unanswered and record the result against every finding it supports.";
|
|
4858
|
+
// Saving as it goes (ruled 2026-10-27): a try that is stopped keeps the findings and exclusions it accepted, and the
|
|
4859
|
+
// next try continues from them. Exclusions already go through record_declination in batches; record_synthesis takes
|
|
4860
|
+
// a part of the findings before the whole document exists.
|
|
4861
|
+
export const WRITER_RECORDS_AS_IT_GOES = "Record completed findings (`record_synthesis`) and exclusions (`record_declination`) as you complete them, individually or in small groups; do not wait for the whole report. On a resumed attempt, continue from this run's accepted work and complete what remains. Revise accepted work only for identified contrary evidence, a reasoning error, or an applicable change in the instructions. Finish the overall conclusions against the complete set before declaring the assessment complete.";
|