alphacouncil-agent 1.0.3 → 1.0.13
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/.claude/agents/alphacouncil-insider_sec.md +19 -0
- package/.claude/commands/alpha.md +48 -19
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.grok/agents/alphacouncil-insider_sec.md +19 -0
- package/.grok/commands/alpha.md +48 -19
- package/.opencode/agent/alphacouncil-insider_sec.md +19 -0
- package/.opencode/command/alpha.md +48 -19
- package/CHANGELOG.md +307 -0
- package/CLAUDE.md +55 -13
- package/README.ja.md +42 -16
- package/README.md +70 -27
- package/README.zh-CN.md +53 -20
- package/SECURITY.md +11 -1
- package/commands/alpha.md +48 -19
- package/data/build-profile.v1.json +1 -1
- package/docs/INSTALL.md +9 -6
- package/docs/examples/final_report.SOX.zh.md +442 -0
- package/docs/report-contract.md +27 -3
- package/knowledge/ai-assisted-solo/experiments/runs/a.json +4 -4
- package/knowledge/ai-assisted-solo/experiments/runs/b.json +2 -2
- package/knowledge/ai-assisted-solo/experiments/runs/c.json +2 -2
- package/knowledge/ai-assisted-solo/experiments/runs/d13.json +16 -16
- package/knowledge/ai-assisted-solo/experiments/runs/d26.json +30 -30
- package/knowledge/ai-assisted-solo/experiments/runs/e-d13.json +19 -19
- package/knowledge/ai-assisted-solo/experiments/runs/e-d26.json +33 -33
- package/knowledge/ai-assisted-solo/experiments/runs/h_ai_reference.json +34 -34
- package/knowledge/ai-assisted-solo/experiments/simulation-input.json +84 -84
- package/knowledge/ai-assisted-solo/experiments/simulation-manifest.json +19 -19
- package/knowledge/solo-test/masters/master_ackman/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_aschenbrenner/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_asness/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_bogle/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_buffett/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_burry/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_cathie_wood/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_dalio/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_damodaran/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_druckenmiller/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_duan_yongping/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_fisher/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_forensic_short/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_graham/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_jhunjhunwala/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_klarman/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_li_lu/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_lynch/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_marks/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_munger/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_natenberg/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_pabrai/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_simons/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_sinclair/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_soros/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_taleb/manifest.json +8 -8
- package/knowledge/solo-test/masters/master_thorp/manifest.json +8 -8
- package/mcp/lib/constants.mjs +82 -4
- package/mcp/lib/council-options.mjs +8 -4
- package/mcp/lib/council-selection.mjs +79 -1
- package/mcp/lib/fundamentals.mjs +50 -10
- package/mcp/lib/gates.mjs +42 -0
- package/mcp/lib/grounding.mjs +27 -1
- package/mcp/lib/industry.mjs +9 -1
- package/mcp/lib/markdown.mjs +25 -1
- package/mcp/lib/orchestrator.mjs +187 -24
- package/mcp/lib/personas/engine.mjs +57 -12
- package/mcp/lib/personas-v3/runtime.mjs +28 -0
- package/mcp/lib/prompts.mjs +72 -3
- package/mcp/lib/rpc.mjs +93 -7
- package/mcp/lib/run-store.mjs +3 -0
- package/mcp/lib/screen.mjs +46 -46
- package/mcp/lib/sec.mjs +25 -10
- package/mcp/lib/voice-from-decision.mjs +13 -5
- package/mcp/lib/weights.mjs +9 -2
- package/mcp/lib/xbrl-series.mjs +31 -5
- package/package.json +2 -1
- package/personas/analysts/insider_sec.md +38 -0
- package/scripts/lib/persona-v3-solo-test-packs.mjs +14 -8
- package/scripts/review-persona-v3-solo-formulas.mjs +5 -3
- package/skills/alphacouncil-agent/SKILL.md +80 -19
package/mcp/lib/markdown.mjs
CHANGED
|
@@ -13,6 +13,16 @@ import { intentLabel, VOICE_FIELDS, voiceDisclaimer, voiceFieldLabel } from "./v
|
|
|
13
13
|
import { agentState, appendEvent, artifactPaths, runPath, taskState } from "./run-store.mjs";
|
|
14
14
|
import { personaTitle, registry } from "./personas/registry.mjs";
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Per-seat statement budget in the handoff.
|
|
18
|
+
*
|
|
19
|
+
* The governance contract requires every selected seat's statement in the handoff, so this is
|
|
20
|
+
* the seat's only appearance there. At the old one-line budget a seat's conclusion was always
|
|
21
|
+
* the part that got cut, which is how seven seats that all spoke read as seven seats that had
|
|
22
|
+
* not. The complete statement stays in the report and in `master_<id>.md`.
|
|
23
|
+
*/
|
|
24
|
+
const MASTER_STATEMENT_CHARS = 2000;
|
|
25
|
+
|
|
16
26
|
export function renderPacketMarkdown(packet, index = 0, language = packet?.language) {
|
|
17
27
|
const key = languageKey(language);
|
|
18
28
|
const label = {
|
|
@@ -1170,7 +1180,21 @@ export function userResponseMarkdown(run, manager) {
|
|
|
1170
1180
|
if (!opinion) return `- ${masterTitle(id, run.language)} (\`${id}\`) [${localizedDisplayValue(state.status, run.language)}]: ${localizedFailure(state.error, run.language) || copy.missing}`;
|
|
1171
1181
|
const statementSource = opinion.dedicated_worker?.status || opinion.voice_status || "unknown";
|
|
1172
1182
|
const frozenRecord = opinion.statement_origin || opinion.reason || opinion.engine || "recorded";
|
|
1173
|
-
|
|
1183
|
+
// A seat's statement opens with the evidence it read and closes with what it would do, so
|
|
1184
|
+
// clipping the opening to one line spent the whole budget on background and cut the
|
|
1185
|
+
// conclusion. Lead with the reading that decided it and the action it implies -- both are
|
|
1186
|
+
// already composed from the frozen decision -- then quote the statement at a budget that
|
|
1187
|
+
// survives a paragraph, and leave provenance to its own line instead of the same sentence.
|
|
1188
|
+
const lead = [opinion.voice?.how_my_method_reads_it, opinion.voice?.would_i_act]
|
|
1189
|
+
.filter((part) => typeof part === "string" && part.trim().length)
|
|
1190
|
+
.join(" ");
|
|
1191
|
+
const statement = opinion.voice_statement || opinion.summary || opinion.verdict || "";
|
|
1192
|
+
return [
|
|
1193
|
+
`- ${masterTitle(id, run.language)} (\`${id}\`)`,
|
|
1194
|
+
lead ? ` - ${clipAtBoundary(lead, 700)}` : "",
|
|
1195
|
+
statement ? ` - ${clipAtBoundary(statement, MASTER_STATEMENT_CHARS)}` : "",
|
|
1196
|
+
` - [${copy.record}: ${localizedDisplayValue(opinion.stance, run.language)}/${localizedDisplayValue(opinion.confidence || "low", run.language)}; ${copy.worker}: ${localizedDisplayValue(statementSource, run.language)}; ${frozenRecord}]`,
|
|
1197
|
+
].filter(Boolean).join("\n");
|
|
1174
1198
|
}).join("\n") || `- ${copy.missing}`;
|
|
1175
1199
|
const analystLines = (run.tasks || []).map((task) => {
|
|
1176
1200
|
const packet = (run.packets || []).find((item) => item.task === task);
|
package/mcp/lib/orchestrator.mjs
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { COUNCIL_MODES, DEBATE_ROLES, DEFAULT_TASKS, LIMITS, OUTPUT_MODES, QUICK_TASKS } from "./constants.mjs";
|
|
3
|
+
import { COUNCIL_MODES, DEBATE_ROLES, DEFAULT_TASKS, LIMITS, OUTPUT_MODES, QUICK_TASKS, councilPaceProfile } from "./constants.mjs";
|
|
4
4
|
import { invalidParams } from "./errors.mjs";
|
|
5
5
|
import { readJson, writeJson } from "./fsutil.mjs";
|
|
6
6
|
import { registry } from "./personas/registry.mjs";
|
|
7
7
|
import { assertReaderLanguage, isChineseLanguage, localized, resolveLanguage } from "./lang.mjs";
|
|
8
8
|
import { cleanLog } from "./text.mjs";
|
|
9
|
-
import { completenessStatus, masterSeatIncomplete, verificationStatus } from "./gates.mjs";
|
|
9
|
+
import { authoredReportSectionGaps, completenessStatus, masterSeatIncomplete, requiredReportSectionAliases, verificationStatus } from "./gates.mjs";
|
|
10
10
|
import { agentState, appendEvent, artifactPaths, existingDebate, runPath, runId, safeSymbol, saveRun, taskState, today, updateAgent, updateTask, writeSourceManifest, writeStatus } from "./run-store.mjs";
|
|
11
11
|
import { writeAllAgentsMarkdown, writeAnalystMarkdownFiles, writeArtifactIndex, writeFinalArtifacts } from "./markdown.mjs";
|
|
12
12
|
import { debateFailurePacket, debateFromCodex, debateQnaGate, dryDebate, dryPacket, extractJson, firstFailedDebateResult, managerFallback, mergeDebateRounds, normalizeDebate, normalizeMasterOpinion, normalizeMasterVoice, normalizePacket, rawRecordText } from "./packets.mjs";
|
|
13
13
|
import { mapLimit, runCodex } from "./codex.mjs";
|
|
14
14
|
import { debatePrompt, masterPrompt, masterVoicePrompt, selectedMasters, taskPrompt } from "./prompts.mjs";
|
|
15
15
|
import { resolveSeatWeights } from "./weights.mjs";
|
|
16
|
-
import { completedMasterOpinion, declinedMasterOpinion, planMasterSeats, reconcileMasterOpinion } from "./personas/engine.mjs";
|
|
16
|
+
import { completedMasterOpinion, declinedMasterOpinion, needsMethodVoiceWorker, planMasterSeats, reconcileMasterOpinion } from "./personas/engine.mjs";
|
|
17
17
|
import { gatherGrounding } from "./grounding.mjs";
|
|
18
18
|
import { councilOptions } from "./council-options.mjs";
|
|
19
19
|
import { sha256 } from "./personas-v3/canonical.mjs";
|
|
@@ -28,11 +28,28 @@ function plannedTasks(args = {}) {
|
|
|
28
28
|
return DEFAULT_TASKS;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* The pace a run executes at, and the per-stage caps that come with it.
|
|
33
|
+
*
|
|
34
|
+
* Quick has one fixed shape and does not take a pace: it is a smaller contract, not a slower
|
|
35
|
+
* one. Full carries the pace on the run so every stage reads the same profile and `status.json`
|
|
36
|
+
* records which one was used.
|
|
37
|
+
*/
|
|
38
|
+
export function runPace(run) {
|
|
39
|
+
if (run?.council_mode === "quick") return null;
|
|
40
|
+
return councilPaceProfile(run?.council_pace);
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
function councilTiming(args, startedAt) {
|
|
32
44
|
const mode = councilMode(args);
|
|
33
45
|
const requested = Number(args.total_timeout_ms);
|
|
34
|
-
const
|
|
35
|
-
|
|
46
|
+
const profile = mode === "quick" ? null : councilPaceProfile(args.council_pace);
|
|
47
|
+
// The ceiling a call is held to is its own pace's total, not the outer bound of the schema.
|
|
48
|
+
// A caller may still lower it; `LIMITS.FULL_TOTAL_MS` keeps the operator env override.
|
|
49
|
+
const hardMaximum = mode === "quick" ? LIMITS.QUICK_HARD_MAX_MS : profile.total_ms;
|
|
50
|
+
const defaultBudget = mode === "quick"
|
|
51
|
+
? LIMITS.QUICK_TOTAL_MS
|
|
52
|
+
: Math.min(profile.total_ms, LIMITS.FULL_TOTAL_OVERRIDE_MS ?? profile.total_ms);
|
|
36
53
|
const timeBudgetMs = Number.isFinite(requested) && requested > 0
|
|
37
54
|
? Math.min(requested, hardMaximum)
|
|
38
55
|
: defaultBudget;
|
|
@@ -40,6 +57,7 @@ function councilTiming(args, startedAt) {
|
|
|
40
57
|
|| new Date(Date.parse(startedAt) + timeBudgetMs).toISOString();
|
|
41
58
|
return {
|
|
42
59
|
council_mode: mode,
|
|
60
|
+
council_pace: profile?.pace || null,
|
|
43
61
|
debate_format: mode === "quick" ? "single_round_parallel" : "three_round_cross_exam_parallel_per_round",
|
|
44
62
|
time_budget_ms: timeBudgetMs,
|
|
45
63
|
deadline_at: deadlineAt,
|
|
@@ -62,7 +80,7 @@ function remainingCouncilBudget(run, capMs) {
|
|
|
62
80
|
if (!run.deadline_at) return capMs;
|
|
63
81
|
const configuredReserve = run.council_mode === "quick"
|
|
64
82
|
? LIMITS.QUICK_FINALIZE_RESERVE_MS
|
|
65
|
-
:
|
|
83
|
+
: runPace(run).finalize_reserve_ms;
|
|
66
84
|
const reserve = Math.min(
|
|
67
85
|
configuredReserve,
|
|
68
86
|
Math.max(100, Math.floor(Number(run.time_budget_ms || LIMITS.FULL_TOTAL_MS) * 0.1)),
|
|
@@ -268,7 +286,7 @@ export function visibleAgentSpecs(run, userPrompt = "") {
|
|
|
268
286
|
ja: `AlphaCouncil Agent ${run.symbol} ${task} 証拠サブエージェント`,
|
|
269
287
|
ko: `AlphaCouncil Agent ${run.symbol} ${task} 증거 하위 에이전트`,
|
|
270
288
|
}),
|
|
271
|
-
prompt: taskPrompt(task, run.symbol, run.as_of, userPrompt, run.language, run.grounding),
|
|
289
|
+
prompt: taskPrompt(task, run.symbol, run.as_of, userPrompt, run.language, run.grounding, run.council_pace),
|
|
272
290
|
output_contract: localized(run.language, {
|
|
273
291
|
en: `Return one JSON evidence packet with reader-facing fields in ${run.language}.`,
|
|
274
292
|
zh: "只返回一个 JSON evidence packet。",
|
|
@@ -374,16 +392,23 @@ export function visibleAgentSpecs(run, userPrompt = "") {
|
|
|
374
392
|
item.engine,
|
|
375
393
|
));
|
|
376
394
|
}
|
|
395
|
+
// A seat that executed its policy and still abstained is in the same position as a
|
|
396
|
+
// declined seat: the frozen stance is out_of_scope, no worker may change it, and the
|
|
397
|
+
// deterministic statement already names the gate that closed and says an abstention is
|
|
398
|
+
// not a bearish vote. Only seats that reached a stance have a reading to explain.
|
|
399
|
+
const requiresVisibleVoice = needsMethodVoiceWorker(byId.get(item.id));
|
|
377
400
|
const alreadyVoiced = run.master_status?.[item.id]?.status === "completed"
|
|
378
401
|
&& byId.get(item.id)?.voice_status === "completed";
|
|
379
402
|
run.master_status[item.id] = {
|
|
380
403
|
...(run.master_status[item.id] || {}),
|
|
381
404
|
master: item.id,
|
|
382
|
-
status: alreadyVoiced ? "completed" : "waiting",
|
|
405
|
+
status: alreadyVoiced || !requiresVisibleVoice ? "completed" : "waiting",
|
|
383
406
|
engine: item.engine,
|
|
384
407
|
deterministic_execution: true,
|
|
385
|
-
voice_required:
|
|
386
|
-
completed_at: alreadyVoiced
|
|
408
|
+
voice_required: requiresVisibleVoice,
|
|
409
|
+
completed_at: alreadyVoiced || !requiresVisibleVoice
|
|
410
|
+
? (run.master_status[item.id]?.completed_at || new Date().toISOString())
|
|
411
|
+
: null,
|
|
387
412
|
};
|
|
388
413
|
}
|
|
389
414
|
run.master_opinions = selectedMasters(run).map((id) => byId.get(id)).filter(Boolean);
|
|
@@ -402,6 +427,7 @@ export function visibleAgentSpecs(run, userPrompt = "") {
|
|
|
402
427
|
const v3VoiceAgents = plan.completed
|
|
403
428
|
.filter((item) => item.engine === "v3_method_runtime")
|
|
404
429
|
.filter((item) => run.master_status?.[item.id]?.status !== "completed")
|
|
430
|
+
.filter((item) => needsMethodVoiceWorker(frozenById.get(item.id)))
|
|
405
431
|
.map((item) => {
|
|
406
432
|
const frozenOpinion = frozenById.get(item.id);
|
|
407
433
|
return {
|
|
@@ -432,10 +458,21 @@ export function visibleAgentSpecs(run, userPrompt = "") {
|
|
|
432
458
|
.map((id) => master_agents.find((agent) => agent.role === id))
|
|
433
459
|
.filter(Boolean);
|
|
434
460
|
saveRun(run);
|
|
461
|
+
const prompts = externalizeVisiblePrompts(run, [
|
|
462
|
+
{ kind: "evidence", agents: evidence_agents },
|
|
463
|
+
{ kind: "master", agents: orderedMasterAgents },
|
|
464
|
+
{ kind: "debate", agents: debate_agents },
|
|
465
|
+
]);
|
|
435
466
|
return {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
467
|
+
// Every planned prompt is on disk either way. `prompts_inline` says whether this result
|
|
468
|
+
// also carries them, so a host never has to guess whether `prompt_template` is missing
|
|
469
|
+
// because the plan failed or because it was too large to return.
|
|
470
|
+
prompts_inline: prompts.inline,
|
|
471
|
+
prompt_dir: prompts.prompt_dir,
|
|
472
|
+
prompt_chars_total: prompts.prompt_chars_total,
|
|
473
|
+
evidence_agents: prompts.byKind.get("evidence"),
|
|
474
|
+
master_agents: prompts.byKind.get("master"),
|
|
475
|
+
debate_agents: prompts.byKind.get("debate"),
|
|
439
476
|
// Recorded, not hidden: a reader must be able to tell a method that judged from a method
|
|
440
477
|
// that could not look, and neither from a seat that was never offered.
|
|
441
478
|
master_decisions: plan.decisions,
|
|
@@ -468,6 +505,67 @@ export function visibleAgentSpecs(run, userPrompt = "") {
|
|
|
468
505
|
};
|
|
469
506
|
}
|
|
470
507
|
|
|
508
|
+
/**
|
|
509
|
+
* Total prompt characters a visible plan may return inline.
|
|
510
|
+
*
|
|
511
|
+
* The plan carries one full prompt per agent, and every prompt embeds the grounding and
|
|
512
|
+
* evidence JSON. A real eight-seat run returned 311,007 characters in a single tool result and
|
|
513
|
+
* the host rejected the whole thing for exceeding its result ceiling -- the plan was produced
|
|
514
|
+
* correctly and then thrown away. Below this budget nothing changes for an existing host;
|
|
515
|
+
* above it the prompts move to disk and each agent carries the path instead.
|
|
516
|
+
*/
|
|
517
|
+
const VISIBLE_PLAN_INLINE_PROMPT_CHARS = 120000;
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* The size that decides this is the grounding each prompt embeds, not the number of seats: a
|
|
521
|
+
* bench-wide plan with no grounding is well inside the budget, while eight seats against a
|
|
522
|
+
* full macro series is what produced the 311k result. Operators may lower the budget for a
|
|
523
|
+
* host with a tighter ceiling; they may not raise it past what that host would reject anyway.
|
|
524
|
+
*/
|
|
525
|
+
function inlinePromptBudget(env = process.env) {
|
|
526
|
+
const requested = Number(env?.ALPHACOUNCIL_VISIBLE_INLINE_PROMPT_CHARS);
|
|
527
|
+
if (!Number.isFinite(requested) || requested < 0) return VISIBLE_PLAN_INLINE_PROMPT_CHARS;
|
|
528
|
+
return Math.min(requested, VISIBLE_PLAN_INLINE_PROMPT_CHARS);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Write every planned prompt to the run directory, and drop the inline copies when returning
|
|
533
|
+
* them all at once would exceed what a host will accept.
|
|
534
|
+
*/
|
|
535
|
+
function externalizeVisiblePrompts(run, groups) {
|
|
536
|
+
const dir = join(runPath(run.run_id), "prompts");
|
|
537
|
+
mkdirSync(dir, { recursive: true });
|
|
538
|
+
// Roles are generated ids, but a filename is still a filename: keep it to one path segment.
|
|
539
|
+
const fileSafe = (role) => String(role || "agent").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80) || "agent";
|
|
540
|
+
// Evidence agents carry `prompt`; master and debate agents carry `prompt_template`. Reading
|
|
541
|
+
// only one of them wrote empty files for the other group.
|
|
542
|
+
const promptKey = (agent) => (typeof agent.prompt_template === "string" ? "prompt_template" : "prompt");
|
|
543
|
+
const total = groups.reduce((sum, { agents }) => sum + agents
|
|
544
|
+
.reduce((inner, agent) => inner + String(agent[promptKey(agent)] || "").length, 0), 0);
|
|
545
|
+
const inline = total <= inlinePromptBudget();
|
|
546
|
+
const externalized = groups.map(({ kind, agents }) => ({
|
|
547
|
+
kind,
|
|
548
|
+
agents: agents.map((agent) => {
|
|
549
|
+
const key = promptKey(agent);
|
|
550
|
+
const file = join(dir, `${kind}.${fileSafe(agent.role)}.prompt.md`);
|
|
551
|
+
const prompt = String(agent[key] || "");
|
|
552
|
+
writeFileSync(file, prompt.endsWith("\n") ? prompt : `${prompt}\n`, { encoding: "utf8", mode: 0o600 });
|
|
553
|
+
return {
|
|
554
|
+
...agent,
|
|
555
|
+
prompt_file: file,
|
|
556
|
+
prompt_chars: prompt.length,
|
|
557
|
+
...(inline ? {} : { [key]: null }),
|
|
558
|
+
};
|
|
559
|
+
}),
|
|
560
|
+
}));
|
|
561
|
+
return {
|
|
562
|
+
inline,
|
|
563
|
+
prompt_dir: dir,
|
|
564
|
+
prompt_chars_total: total,
|
|
565
|
+
byKind: new Map(externalized.map(({ kind, agents }) => [kind, agents])),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
471
569
|
/**
|
|
472
570
|
* The stance is already decided. The prompt says so, in the run's language, so the model
|
|
473
571
|
* writes an explanation rather than a verdict.
|
|
@@ -941,14 +1039,48 @@ function recordVisiblePortfolioManager(run, args) {
|
|
|
941
1039
|
execution_mode: "visible_host_threads",
|
|
942
1040
|
}, "portfolio_manager", run, rawRecordText(args.packet));
|
|
943
1041
|
assertVisibleReaderLanguage(debateReaderText(packet), run, "visible portfolio_manager decision");
|
|
1042
|
+
// Reject a report body that cannot pass the report gate before the packet takes the
|
|
1043
|
+
// idempotency lock, and say which headings are owed. Previously the gate ran only after the
|
|
1044
|
+
// report was assembled, so a submission with no `report_markdown` at all was accepted, the
|
|
1045
|
+
// report was built from the summary fallback, and the author learned about 21 missing
|
|
1046
|
+
// sections only after the entire PM turn had been spent.
|
|
1047
|
+
const authoredGaps = authoredReportSectionGaps(packet.report_markdown, run);
|
|
1048
|
+
if (authoredGaps.length) {
|
|
1049
|
+
rejectVisibleDecision(
|
|
1050
|
+
run,
|
|
1051
|
+
"VISIBLE_PM_REPORT_SECTIONS_MISSING",
|
|
1052
|
+
packet.report_markdown
|
|
1053
|
+
? "portfolio_manager rejected: report_markdown does not carry every required report-contract section."
|
|
1054
|
+
: "portfolio_manager rejected: packet.report_markdown is required and must be the complete report body, not an execution note.",
|
|
1055
|
+
{
|
|
1056
|
+
report_contract: run.council_mode === "quick" ? "quick_v1" : "full_v2",
|
|
1057
|
+
report_markdown_characters: String(packet.report_markdown || "").length,
|
|
1058
|
+
gaps: authoredGaps,
|
|
1059
|
+
required_sections: requiredReportSectionAliases(run),
|
|
1060
|
+
},
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
944
1063
|
const contentHash = visibleDecisionContentHash(packet);
|
|
945
1064
|
if (state.portfolio_manager) {
|
|
946
|
-
|
|
1065
|
+
// A report that failed the structure gate must stay revisable. The first submission takes
|
|
1066
|
+
// the idempotency lock before the gate runs, so a PM whose report_markdown was thin left the
|
|
1067
|
+
// run stuck at needs_revision forever: the fix was rejected as a conflicting replay and there
|
|
1068
|
+
// was no other way in. A passed report is still frozen -- revision is for repairing a
|
|
1069
|
+
// rejected report, not for changing a verdict that already stands.
|
|
1070
|
+
const priorQualityPath = join(dir, "report_quality.json");
|
|
1071
|
+
const priorQuality = existsSync(priorQualityPath) ? readJson(priorQualityPath) : null;
|
|
1072
|
+
const revisable = priorQuality && priorQuality.status !== "passed";
|
|
1073
|
+
if (state.portfolio_manager.content_hash !== contentHash && !revisable) {
|
|
947
1074
|
rejectVisibleDecision(run, "VISIBLE_PM_CONFLICT", "Conflicting portfolio_manager replay.", {
|
|
948
1075
|
existing_content_hash: state.portfolio_manager.content_hash,
|
|
949
1076
|
submitted_content_hash: contentHash,
|
|
1077
|
+
report_quality: priorQuality?.status || null,
|
|
950
1078
|
});
|
|
951
1079
|
}
|
|
1080
|
+
// Revision accepted: drop the lock so the normal record path below runs again.
|
|
1081
|
+
if (state.portfolio_manager.content_hash !== contentHash) state.portfolio_manager = null;
|
|
1082
|
+
}
|
|
1083
|
+
if (state.portfolio_manager) {
|
|
952
1084
|
const handoffPath = join(dir, "user_response.md");
|
|
953
1085
|
const reportPath = join(dir, "final_report.md");
|
|
954
1086
|
const qualityPath = join(dir, "report_quality.json");
|
|
@@ -1138,8 +1270,12 @@ function readerStrings(value, skipKeys = new Set()) {
|
|
|
1138
1270
|
}
|
|
1139
1271
|
|
|
1140
1272
|
function visibleEvidenceReaderText(packet) {
|
|
1273
|
+
// `title` joins the machine set for the same reason `url` is already there: a source title is
|
|
1274
|
+
// the publisher's own words. Counting English headlines against a Chinese run's language ratio
|
|
1275
|
+
// rejected packets whose every authored sentence was Chinese, and the only way to pass was to
|
|
1276
|
+
// translate the citation -- which is falsifying the source, not localising the report.
|
|
1141
1277
|
const machine = new Set([
|
|
1142
|
-
"task", "symbol", "as_of", "source_ids", "id", "url", "published_at", "retrieved_at",
|
|
1278
|
+
"task", "symbol", "as_of", "source_ids", "id", "url", "title", "published_at", "retrieved_at",
|
|
1143
1279
|
"confidence", "information_richness", "thread_id", "execution_mode", "raw_text",
|
|
1144
1280
|
]);
|
|
1145
1281
|
return [
|
|
@@ -1314,7 +1450,7 @@ export async function collectEvidence(args) {
|
|
|
1314
1450
|
const requestedTimeoutMs = Number.isFinite(args.timeout_ms) ? args.timeout_ms : LIMITS.CODEX_TIMEOUT_MS;
|
|
1315
1451
|
const timeoutMs = timing.council_mode === "quick"
|
|
1316
1452
|
? Math.min(requestedTimeoutMs, LIMITS.QUICK_EVIDENCE_MS)
|
|
1317
|
-
: Math.min(requestedTimeoutMs,
|
|
1453
|
+
: Math.min(requestedTimeoutMs, councilPaceProfile(timing.council_pace).evidence_ms);
|
|
1318
1454
|
const defaultConcurrency = timing.council_mode === "quick"
|
|
1319
1455
|
? QUICK_TASKS.length
|
|
1320
1456
|
: Math.min(tasks.length, LIMITS.FULL_EVIDENCE_CONCURRENCY);
|
|
@@ -1334,7 +1470,7 @@ export async function collectEvidence(args) {
|
|
|
1334
1470
|
dryRun,
|
|
1335
1471
|
timeoutMs: remainingCouncilBudget(
|
|
1336
1472
|
timing,
|
|
1337
|
-
timing.council_mode === "quick" ? LIMITS.QUICK_GROUNDING_MS :
|
|
1473
|
+
timing.council_mode === "quick" ? LIMITS.QUICK_GROUNDING_MS : councilPaceProfile(timing.council_pace).grounding_ms,
|
|
1338
1474
|
),
|
|
1339
1475
|
});
|
|
1340
1476
|
const dir = runPath(id);
|
|
@@ -1436,7 +1572,7 @@ export async function collectEvidence(args) {
|
|
|
1436
1572
|
: "QUICK COUNCIL PRIORITY: return only the 4-6 highest-information claims needed for a directional read. Keep the packet concise, source every claim, and make unknowns explicit."
|
|
1437
1573
|
: "";
|
|
1438
1574
|
const workerObjective = [args.prompt || "", quickPriority].filter(Boolean).join("\n\n");
|
|
1439
|
-
const prompt = taskPrompt(task, symbol, asOfDate, workerObjective, language, run.grounding);
|
|
1575
|
+
const prompt = taskPrompt(task, symbol, asOfDate, workerObjective, language, run.grounding, run.council_pace);
|
|
1440
1576
|
updateTask(run, task, "running", { started_at: new Date().toISOString() });
|
|
1441
1577
|
if (dryRun) {
|
|
1442
1578
|
const packet = dryPacket(task, symbol, asOfDate, prompt, language);
|
|
@@ -1637,7 +1773,7 @@ export async function runHeadlessMasters(run, args = {}) {
|
|
|
1637
1773
|
const requestedTimeoutMs = Number.isFinite(args.timeout_ms) ? args.timeout_ms : LIMITS.CODEX_TIMEOUT_MS;
|
|
1638
1774
|
const timeoutMs = run.council_mode === "quick"
|
|
1639
1775
|
? Math.min(requestedTimeoutMs, LIMITS.QUICK_MASTER_MS)
|
|
1640
|
-
: Math.min(requestedTimeoutMs,
|
|
1776
|
+
: Math.min(requestedTimeoutMs, runPace(run).master_ms);
|
|
1641
1777
|
const defaultConcurrency = run.council_mode === "quick"
|
|
1642
1778
|
? 4
|
|
1643
1779
|
: Math.min(selected.length, LIMITS.FULL_MASTER_CONCURRENCY);
|
|
@@ -1684,12 +1820,39 @@ export async function runHeadlessMasters(run, args = {}) {
|
|
|
1684
1820
|
})),
|
|
1685
1821
|
...plan.to_run,
|
|
1686
1822
|
];
|
|
1823
|
+
// A frozen abstention has no reading for a worker to explain, and its deterministic statement
|
|
1824
|
+
// already carries what the contract asks of an out_of_scope seat. Publish it directly instead
|
|
1825
|
+
// of spending one model turn per seat to restate it.
|
|
1826
|
+
const abstainedWithoutWorker = [];
|
|
1827
|
+
const votingWorkerItems = workerItems.filter((item) => {
|
|
1828
|
+
if (!item.frozenOpinion || needsMethodVoiceWorker(item.frozenOpinion)) return true;
|
|
1829
|
+
abstainedWithoutWorker.push(item);
|
|
1830
|
+
return false;
|
|
1831
|
+
});
|
|
1687
1832
|
for (const item of workerItems) {
|
|
1688
1833
|
if (!item.frozenOpinion) continue;
|
|
1689
1834
|
writeJson(join(dir, `${item.id}.deterministic.json`), item.frozenOpinion);
|
|
1690
1835
|
}
|
|
1691
1836
|
|
|
1692
|
-
const
|
|
1837
|
+
const abstainedOutcomes = abstainedWithoutWorker.map(({ id, engine, frozenOpinion }) => ({
|
|
1838
|
+
id,
|
|
1839
|
+
engine,
|
|
1840
|
+
opinion: {
|
|
1841
|
+
...frozenOpinion,
|
|
1842
|
+
deterministic_summary: frozenOpinion.summary,
|
|
1843
|
+
voice_statement: frozenOpinion.voice_statement || frozenOpinion.summary,
|
|
1844
|
+
voice_status: "deterministic_scope",
|
|
1845
|
+
voice_language: run.language,
|
|
1846
|
+
statement_origin: "deterministic_scope_fallback",
|
|
1847
|
+
dedicated_worker: {
|
|
1848
|
+
status: "not_required_frozen_abstention",
|
|
1849
|
+
language: run.language,
|
|
1850
|
+
execution_mode: "deterministic_only",
|
|
1851
|
+
},
|
|
1852
|
+
},
|
|
1853
|
+
}));
|
|
1854
|
+
|
|
1855
|
+
const outcomes = await mapLimit(votingWorkerItems, maxConcurrency, async ({ id, decision, engine, frozenOpinion, deterministic_decline, deterministic_execution }) => {
|
|
1693
1856
|
const prompt = frozenOpinion
|
|
1694
1857
|
? masterVoicePrompt(id, run, frozenOpinion)
|
|
1695
1858
|
: [
|
|
@@ -1827,7 +1990,7 @@ export async function runHeadlessMasters(run, args = {}) {
|
|
|
1827
1990
|
raw: cleanLog(error?.message || error),
|
|
1828
1991
|
}));
|
|
1829
1992
|
|
|
1830
|
-
for (const outcome of outcomes) {
|
|
1993
|
+
for (const outcome of [...abstainedOutcomes, ...outcomes]) {
|
|
1831
1994
|
if (!outcome.opinion) {
|
|
1832
1995
|
const diagnosticPath = join(dir, `${outcome.id}.failure.json`);
|
|
1833
1996
|
writeJson(diagnosticPath, {
|
|
@@ -2153,7 +2316,7 @@ export async function synthesizeDecision(run, args) {
|
|
|
2153
2316
|
: Number(args.timeout_ms || LIMITS.CODEX_TIMEOUT_MS);
|
|
2154
2317
|
const timeoutMs = run.council_mode === "quick"
|
|
2155
2318
|
? Math.min(requestedSynthesisMs, LIMITS.QUICK_SYNTHESIS_MS)
|
|
2156
|
-
: Math.min(requestedSynthesisMs,
|
|
2319
|
+
: Math.min(requestedSynthesisMs, runPace(run).debate_ms);
|
|
2157
2320
|
const outputMode = OUTPUT_MODES.includes(args.output_mode) ? args.output_mode : "public_equity";
|
|
2158
2321
|
run.phase = "debate";
|
|
2159
2322
|
run.status = "running";
|
|
@@ -2319,7 +2482,7 @@ export async function synthesizeDecision(run, args) {
|
|
|
2319
2482
|
});
|
|
2320
2483
|
writeAllAgentsMarkdown(run, { bull, bear });
|
|
2321
2484
|
|
|
2322
|
-
const pmBudget = remainingCouncilBudget(run, Math.min(requestedSynthesisMs,
|
|
2485
|
+
const pmBudget = remainingCouncilBudget(run, Math.min(requestedSynthesisMs, runPace(run).pm_ms));
|
|
2323
2486
|
const managerStep = await runDebateRole(run, "portfolio_manager", { bull, bear, outputMode }, pmBudget);
|
|
2324
2487
|
const managerOk = !debateFailure(managerStep);
|
|
2325
2488
|
const manager = managerOk ? managerStep.packet : managerFallback(run, args.prompt || "");
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { compiledPersonaPacks } from "../personas-v3/registry.mjs";
|
|
11
11
|
import { buildFactPack } from "../personas-v3/typed-facts.mjs";
|
|
12
12
|
import { typedFactPackFromGrounding } from "../personas-v3/grounding-adapter.mjs";
|
|
13
|
-
import { buildAnonymousPreDecision, freezeAnonymousDecision } from "../personas-v3/runtime.mjs";
|
|
13
|
+
import { buildAnonymousPreDecision, freezeAnonymousDecision, technicalIdReadableMap } from "../personas-v3/runtime.mjs";
|
|
14
14
|
import { executeDeterministicPersonaPolicy } from "../personas-v3/deterministic-executor.mjs";
|
|
15
15
|
import { languageKey, localized } from "../lang.mjs";
|
|
16
16
|
import { displayMasterLabel } from "../markdown.mjs";
|
|
@@ -216,6 +216,25 @@ function uniqueStrings(values) {
|
|
|
216
216
|
return [...new Set(values.filter((value) => typeof value === "string" && value.length))];
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Whether a frozen seat still needs a model worker to become readable.
|
|
221
|
+
*
|
|
222
|
+
* A seat that reached a stance has a reading worth explaining: which condition decided it, at
|
|
223
|
+
* what number, and what would move it. A seat whose gate never opened has no reading. Its
|
|
224
|
+
* deterministic statement already names the condition that closed the gate and says an
|
|
225
|
+
* abstention is not a bearish vote, which is the whole of what an out_of_scope seat is asked
|
|
226
|
+
* to say -- so an isolated worker there buys prose, not information. The declined path reached
|
|
227
|
+
* this conclusion earlier for the same reason; this applies it to a seat that executed its
|
|
228
|
+
* policy and abstained. On a real seven-seat run four such seats took most of the method phase
|
|
229
|
+
* to report, at length, that they had no opinion.
|
|
230
|
+
*
|
|
231
|
+
* Set ALPHACOUNCIL_VOICE_ABSTAINING_SEATS=1 to give every seat a worker again.
|
|
232
|
+
*/
|
|
233
|
+
export function needsMethodVoiceWorker(opinion, { env = process.env } = {}) {
|
|
234
|
+
if (env?.ALPHACOUNCIL_VOICE_ABSTAINING_SEATS === "1") return true;
|
|
235
|
+
return (opinion?.stance || "out_of_scope") !== "out_of_scope";
|
|
236
|
+
}
|
|
237
|
+
|
|
219
238
|
/** Render a post-freeze, model-free opinion from the DSL result. */
|
|
220
239
|
export function completedMasterOpinion(run, item) {
|
|
221
240
|
if (item?.engine !== "v3_method_runtime" || !item?.completed) {
|
|
@@ -230,15 +249,21 @@ export function completedMasterOpinion(run, item) {
|
|
|
230
249
|
const label = displayMasterLabel(item.pack.admitted_label?.[locale]
|
|
231
250
|
|| item.pack.admitted_label?.en || item.id);
|
|
232
251
|
const copy = localized(run.language, {
|
|
233
|
-
en: { withheld: (pct) => `${pct}% coverage; score withheld`, unscored: "not scored", verdict: (stance, reason) => `${label} frozen decision: ${stance} (${reason})`, summary: (score) => `Using the ${label} method on ${run.symbol}, the PersonaPack v3 deterministic policy executed with score ${score}. Typed facts, hard vetoes, and score bands produced this stance; no language model selected it.`, hit: (id) => `score hit: ${id}`, miss: (id) => `score miss: ${id}`, eligibility: (id) => `eligibility condition ${id} becomes satisfied`, veto: (id) => `hard veto ${id} no longer triggers`, score: (id) => `score condition ${id} becomes satisfied` },
|
|
234
|
-
zh: { withheld: (pct) => `覆盖率 ${pct}%,分数被保留不发布`, unscored: "未评分", verdict: (stance, reason) => `${label}的冻结结论:${stance}(${reason})`, summary: (score) => `按${label}方法审视 ${run.symbol}:PersonaPack v3 确定性政策已执行,得分 ${score}。该立场由结构化事实、硬否决和评分带产生,没有让语言模型选择立场。`, hit: (id) => `评分命中:${id}`, miss: (id) => `评分未命中:${id}`, eligibility: (id) => `资格条件 ${id} 变为满足`, veto: (id) => `硬否决 ${id} 不再触发`, score: (id) => `评分条件 ${id} 变为满足` },
|
|
235
|
-
ja: { withheld: (pct) => `カバレッジ ${pct}% のためスコアは非公開`, unscored: "未採点", verdict: (stance, reason) => `${label}の凍結済み判断:${stance}(${reason})`, summary: (score) => `${label}の方法で ${run.symbol} を評価し、PersonaPack v3 の決定論的ポリシーを実行した結果、スコアは ${score}。構造化事実、ハード拒否条件、スコア帯がこの立場を生成しており、言語モデルは立場を選択していない。`, hit: (id) => `採点条件を満たす:${id}`, miss: (id) => `採点条件を満たさない:${id}`, eligibility: (id) => `適格条件 ${id} が満たされる`, veto: (id) => `ハード拒否条件 ${id} が解除される`, score: (id) => `採点条件 ${id} が満たされる` },
|
|
236
|
-
ko: { withheld: (pct) => `커버리지 ${pct}%로 점수를 공개하지 않음`, unscored: "미채점", verdict: (stance, reason) => `${label}의 동결된 판단: ${stance}(${reason})`, summary: (score) => `${label} 방법으로 ${run.symbol}을 검토하여 PersonaPack v3 결정론적 정책을 실행했으며 점수는 ${score}입니다. 구조화된 사실, 하드 거부 조건, 점수 구간이 이 입장을 만들었고 언어 모델은 입장을 선택하지 않았습니다.`, hit: (id) => `점수 조건 충족: ${id}`, miss: (id) => `점수 조건 미충족: ${id}`, eligibility: (id) => `적격 조건 ${id} 충족`, veto: (id) => `하드 거부 조건 ${id} 해제`, score: (id) => `점수 조건 ${id} 충족` },
|
|
252
|
+
en: { abstain: (reasons) => `Using the ${label} method, I do not issue a directional view on ${run.symbol} in this run: the method gate did not open (${reasons}). This is neither bearish nor a vote against the asset; the seat reassesses only once that gate can be satisfied.`, withheld: (pct) => `${pct}% coverage; score withheld`, unscored: "not scored", verdict: (stance, reason) => `${label} frozen decision: ${stance} (${reason})`, summary: (score) => `Using the ${label} method on ${run.symbol}, the PersonaPack v3 deterministic policy executed with score ${score}. Typed facts, hard vetoes, and score bands produced this stance; no language model selected it.`, hit: (id) => `score hit: ${id}`, miss: (id) => `score miss: ${id}`, eligibility: (id) => `eligibility condition ${id} becomes satisfied`, veto: (id) => `hard veto ${id} no longer triggers`, score: (id) => `score condition ${id} becomes satisfied` },
|
|
253
|
+
zh: { abstain: (reasons) => `按${label}方法审视 ${run.symbol},本轮不作方向判断:方法闸门未打开(${reasons})。这不是看空,也不是一张反对票;只有该闸门可被满足后,该席位才会重新评估。`, withheld: (pct) => `覆盖率 ${pct}%,分数被保留不发布`, unscored: "未评分", verdict: (stance, reason) => `${label}的冻结结论:${stance}(${reason})`, summary: (score) => `按${label}方法审视 ${run.symbol}:PersonaPack v3 确定性政策已执行,得分 ${score}。该立场由结构化事实、硬否决和评分带产生,没有让语言模型选择立场。`, hit: (id) => `评分命中:${id}`, miss: (id) => `评分未命中:${id}`, eligibility: (id) => `资格条件 ${id} 变为满足`, veto: (id) => `硬否决 ${id} 不再触发`, score: (id) => `评分条件 ${id} 变为满足` },
|
|
254
|
+
ja: { abstain: (reasons) => `${label}の方法では、メソッドのゲートが開かなかったため(${reasons})、今回は ${run.symbol} の方向判断を出しません。弱気判断や反対票ではなく、そのゲートが満たせるようになった時点で再評価します。`, withheld: (pct) => `カバレッジ ${pct}% のためスコアは非公開`, unscored: "未採点", verdict: (stance, reason) => `${label}の凍結済み判断:${stance}(${reason})`, summary: (score) => `${label}の方法で ${run.symbol} を評価し、PersonaPack v3 の決定論的ポリシーを実行した結果、スコアは ${score}。構造化事実、ハード拒否条件、スコア帯がこの立場を生成しており、言語モデルは立場を選択していない。`, hit: (id) => `採点条件を満たす:${id}`, miss: (id) => `採点条件を満たさない:${id}`, eligibility: (id) => `適格条件 ${id} が満たされる`, veto: (id) => `ハード拒否条件 ${id} が解除される`, score: (id) => `採点条件 ${id} が満たされる` },
|
|
255
|
+
ko: { abstain: (reasons) => `${label} 방법으로는 방법론 게이트가 열리지 않아(${reasons}) 이번 실행에서 ${run.symbol}의 방향 판단을 내리지 않습니다. 이는 약세 판단이나 반대표가 아니며, 해당 게이트가 충족될 수 있을 때에만 재평가합니다.`, withheld: (pct) => `커버리지 ${pct}%로 점수를 공개하지 않음`, unscored: "미채점", verdict: (stance, reason) => `${label}의 동결된 판단: ${stance}(${reason})`, summary: (score) => `${label} 방법으로 ${run.symbol}을 검토하여 PersonaPack v3 결정론적 정책을 실행했으며 점수는 ${score}입니다. 구조화된 사실, 하드 거부 조건, 점수 구간이 이 입장을 만들었고 언어 모델은 입장을 선택하지 않았습니다.`, hit: (id) => `점수 조건 충족: ${id}`, miss: (id) => `점수 조건 미충족: ${id}`, eligibility: (id) => `적격 조건 ${id} 충족`, veto: (id) => `하드 거부 조건 ${id} 해제`, score: (id) => `점수 조건 ${id} 충족` },
|
|
237
256
|
});
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
257
|
+
// Every condition id in a frozen decision was hashed before the policy ran so the decision
|
|
258
|
+
// layer could not recognise the seat. Past this point the seat is named in the report and in
|
|
259
|
+
// the explanation worker's own prompt, so the hash only stops the seat from telling a reader
|
|
260
|
+
// which condition decided it. Resolve each id back to the one the pack declares.
|
|
261
|
+
const readableIds = technicalIdReadableMap(item.pack);
|
|
262
|
+
const readable = (id) => readableIds.get(id) || id;
|
|
263
|
+
const vetoIds = (result.vetoes_triggered || []).map((veto) => readable(veto.veto_id));
|
|
264
|
+
const unmet = (result.eligibility?.unmet_condition_ids || []).map(readable);
|
|
265
|
+
const hitIds = (result.score?.hits || []).map((rule) => readable(rule.rule_id));
|
|
266
|
+
const missIds = (result.score?.misses || []).map((rule) => readable(rule.rule_id));
|
|
242
267
|
const sourceIds = uniqueStrings([
|
|
243
268
|
...(result.eligibility?.checks || []).flatMap((check) => check.source_ids || []),
|
|
244
269
|
...(result.vetoes_triggered || []).flatMap((veto) => veto.source_ids || []),
|
|
@@ -250,14 +275,29 @@ export function completedMasterOpinion(run, item) {
|
|
|
250
275
|
: result.score
|
|
251
276
|
? `${result.score.score}/${result.score.max_possible} (${Math.round((result.ratio || 0) * 100)}%)`
|
|
252
277
|
: copy.unscored;
|
|
253
|
-
|
|
278
|
+
// An eligibility gate that closes produces a frozen out_of_scope with no score band, and the
|
|
279
|
+
// generic execution sentence says nothing a reader can use: not which condition closed the
|
|
280
|
+
// gate, and not that an abstention is not a bearish vote. The decline path has said both for
|
|
281
|
+
// a while; say the same here so no seat has to spend a model worker to state it.
|
|
282
|
+
const abstained = result.stance === "out_of_scope";
|
|
283
|
+
const abstainReasons = uniqueStrings([
|
|
284
|
+
...(result.eligibility?.unmet_condition_ids || []).map(readable),
|
|
285
|
+
...(result.eligibility?.uncomputable_condition_ids || []).map(readable),
|
|
286
|
+
...(result.eligibility?.missing_required_fact_types || []),
|
|
287
|
+
]);
|
|
288
|
+
// `summary` stays the provenance sentence -- it is what records that no language model chose
|
|
289
|
+
// the stance. Only the reader-facing statement changes, exactly as the decline path splits it.
|
|
290
|
+
const provenanceSummary = copy.summary(scoreText);
|
|
291
|
+
const deterministicStatement = abstained
|
|
292
|
+
? copy.abstain(abstainReasons.join("; ") || result.reason || copy.unscored)
|
|
293
|
+
: provenanceSummary;
|
|
254
294
|
return {
|
|
255
295
|
master: item.id,
|
|
256
296
|
symbol: run.symbol,
|
|
257
297
|
as_of: run.as_of,
|
|
258
298
|
stance: result.stance,
|
|
259
299
|
verdict: copy.verdict(result.stance, result.reason),
|
|
260
|
-
summary:
|
|
300
|
+
summary: provenanceSummary,
|
|
261
301
|
voice_statement: deterministicStatement,
|
|
262
302
|
// The five fields, composed from the frozen decision rather than written about it: which
|
|
263
303
|
// facts were read and their values, what the tools produced, which scoring conditions held
|
|
@@ -267,6 +307,7 @@ export function completedMasterOpinion(run, item) {
|
|
|
267
307
|
result,
|
|
268
308
|
policy: item.pack.components?.decision_policy,
|
|
269
309
|
factPack: item.preDecision?.fact_pack,
|
|
310
|
+
readableIds,
|
|
270
311
|
language: run.language,
|
|
271
312
|
}),
|
|
272
313
|
voice_status: "deterministic_fallback",
|
|
@@ -291,7 +332,11 @@ export function completedMasterOpinion(run, item) {
|
|
|
291
332
|
decision_reason: result.reason,
|
|
292
333
|
native_decision: result.native_decision,
|
|
293
334
|
native_state: result.native_decision.state,
|
|
294
|
-
|
|
335
|
+
// The frozen decision keeps its anonymised ids -- they are inside the hash. This is the
|
|
336
|
+
// reader-facing copy, so resolve the veto ids here and leave the frozen artifact alone.
|
|
337
|
+
common_projection: result.common_projection
|
|
338
|
+
? { ...result.common_projection, veto_ids: (result.common_projection.veto_ids || []).map(readable) }
|
|
339
|
+
: result.common_projection,
|
|
295
340
|
pack_hash: item.pack.pack_hash,
|
|
296
341
|
corpus_hash: item.pack.corpus_hash,
|
|
297
342
|
policy_hash: item.pack.policy_hash,
|
|
@@ -153,6 +153,34 @@ function technicalIdentityAliases(value, compiledPack) {
|
|
|
153
153
|
return aliases;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Reverse the pre-freeze aliasing for anything downstream of the freeze.
|
|
158
|
+
*
|
|
159
|
+
* The aliasing exists so the decision layer cannot recognise the seat it is deciding for. A
|
|
160
|
+
* reader is not the decision layer, and neither is the post-freeze explanation worker: its
|
|
161
|
+
* prompt opens by naming the persona, so an `anon_<hash>` there hides nothing and costs a
|
|
162
|
+
* great deal. It cost exactly this: two seats whose hard veto had been hashed guessed in
|
|
163
|
+
* their own recorded statements which condition had vetoed them, and both guessed wrong.
|
|
164
|
+
*
|
|
165
|
+
* Keyed by alias so a caller can resolve the ids that appear in a frozen decision. Only the
|
|
166
|
+
* seat's own declared condition ids are mapped; nothing else crosses back.
|
|
167
|
+
*/
|
|
168
|
+
export function technicalIdReadableMap(compiledPack) {
|
|
169
|
+
const policy = compiledPack?.components?.decision_policy || {};
|
|
170
|
+
const declared = [
|
|
171
|
+
...(policy.scoring?.rules || []).map((rule) => rule?.rule_id),
|
|
172
|
+
...(policy.hard_vetoes || []).map((veto) => veto?.veto_id),
|
|
173
|
+
...(policy.eligibility?.all || [])
|
|
174
|
+
.map((check) => check?.condition_id || check?.check_id || check?.id),
|
|
175
|
+
// The computed values a seat cites are named by the tool that produced them, so a hashed
|
|
176
|
+
// output id turns "it computed credit_spread_gap = -0.038" into "it computed
|
|
177
|
+
// anon_69f0... = -0.038" -- a number with no stated meaning.
|
|
178
|
+
...(compiledPack?.components?.tools || []).flatMap((tool) => [tool?.output_id, tool?.id]),
|
|
179
|
+
].filter((id) => nonEmptyString(id));
|
|
180
|
+
const aliases = technicalIdentityAliases(declared, compiledPack);
|
|
181
|
+
return new Map([...aliases].map(([physical, alias]) => [alias, physical]));
|
|
182
|
+
}
|
|
183
|
+
|
|
156
184
|
function applyTechnicalAliases(value, aliases) {
|
|
157
185
|
if (typeof value === "string") return aliases.get(value) || value;
|
|
158
186
|
if (Array.isArray(value)) return value.map((child) => applyTechnicalAliases(child, aliases));
|