alphacouncil-agent 1.0.1 → 1.0.3
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-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +82 -0
- package/README.md +24 -15
- package/README.zh-CN.md +18 -10
- package/data/authored/core-seats.mjs +11 -7
- package/data/authored/quant-seats.mjs +2 -2
- package/data/authored/value-seats.mjs +4 -4
- package/data/build-profile.v1.json +1 -1
- package/data/persona-v3-build-specs.v1.mjs +1 -1
- package/docs/plans/v1.1-say-something-worth-reading.md +116 -0
- package/knowledge/ai-assisted-solo/experiments/runs/a.json +2 -2
- 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 +15 -15
- package/knowledge/ai-assisted-solo/experiments/runs/d26.json +27 -27
- package/knowledge/ai-assisted-solo/experiments/runs/e-d13.json +18 -18
- package/knowledge/ai-assisted-solo/experiments/runs/e-d26.json +30 -30
- package/knowledge/ai-assisted-solo/experiments/runs/h_ai_reference.json +31 -31
- package/knowledge/ai-assisted-solo/experiments/simulation-input.json +9 -9
- package/knowledge/ai-assisted-solo/experiments/simulation-manifest.json +19 -19
- package/knowledge/solo-test/masters/master_bogle/decision_policy.json +4 -3
- package/knowledge/solo-test/masters/master_bogle/provisional-index.json +3 -0
- package/knowledge/solo-test/masters/master_bogle/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_graham/decision_policy.json +1 -1
- package/knowledge/solo-test/masters/master_klarman/decision_policy.json +1 -1
- package/knowledge/solo-test/masters/master_pabrai/decision_policy.json +1 -1
- package/mcp/lib/basket-news.mjs +181 -0
- package/mcp/lib/breadth.mjs +24 -0
- package/mcp/lib/constants.mjs +3 -0
- package/mcp/lib/cross-market.mjs +180 -0
- package/mcp/lib/fundamentals.mjs +11 -0
- package/mcp/lib/grounding.mjs +101 -3
- package/mcp/lib/industry.mjs +23 -2
- package/mcp/lib/insider-ownership.mjs +23 -12
- package/mcp/lib/instrument-facts.mjs +3 -0
- package/mcp/lib/markdown.mjs +41 -8
- package/mcp/lib/orchestrator.mjs +10 -2
- package/mcp/lib/personas/engine.mjs +66 -9
- package/mcp/lib/personas-v3/deterministic-executor.mjs +39 -2
- package/mcp/lib/personas-v3/grounding-adapter.mjs +143 -0
- package/mcp/lib/personas-v3/runtime.mjs +9 -2
- package/mcp/lib/screen.mjs +27 -1
- package/mcp/lib/sec.mjs +80 -17
- package/mcp/lib/voice-from-decision.mjs +211 -0
- package/package.json +1 -1
- package/scripts/lib/persona-v3-solo-formula-pipeline.mjs +11 -0
package/mcp/lib/fundamentals.mjs
CHANGED
|
@@ -781,6 +781,17 @@ export function deriveFundamentals({ companyFacts, asOf = null, maintenanceCapex
|
|
|
781
781
|
});
|
|
782
782
|
}
|
|
783
783
|
|
|
784
|
+
/**
|
|
785
|
+
* True when a registrant has filed no XBRL at all.
|
|
786
|
+
*
|
|
787
|
+
* SEC's own ticker file maps XOM to ExxonMobil Holdings Corp -- a newly formed entity with zero
|
|
788
|
+
* us-gaap tags -- while the operating history sits under a different CIK. Every metric then
|
|
789
|
+
* reported as missing, individually, and nothing said the registrant itself was empty.
|
|
790
|
+
*/
|
|
791
|
+
export function hasNoXbrlHistory(companyFacts) {
|
|
792
|
+
return Object.keys(companyFacts?.facts?.["us-gaap"] || {}).length === 0;
|
|
793
|
+
}
|
|
794
|
+
|
|
784
795
|
/**
|
|
785
796
|
* Thin fetching wrapper: resolve the filer, pull Company Facts through the throttled and
|
|
786
797
|
* User-Agent-bearing sec.mjs client, then hand the JSON to the pure function above.
|
package/mcp/lib/grounding.mjs
CHANGED
|
@@ -5,7 +5,28 @@ import { fetchMacroSeries } from "./fred.mjs";
|
|
|
5
5
|
import { fetchFundamentals } from "./fundamentals.mjs";
|
|
6
6
|
import { fetchInsiderOwnership } from "./insider-ownership.mjs";
|
|
7
7
|
import { INDEX_PROXIES, normalizeIndexSymbol } from "./index-aggregate.mjs";
|
|
8
|
+
import { SECTOR_SPDRS, fetchCrossMarket, fetchSectorDispersion } from "./cross-market.mjs";
|
|
9
|
+
import { fetchBasketNews } from "./basket-news.mjs";
|
|
8
10
|
import { gatherInstrumentFacts, LOOK_THROUGH_FACT_IDS } from "./instrument-facts.mjs";
|
|
11
|
+
|
|
12
|
+
/** The index a US company is measured against when a method asks what the market costs. */
|
|
13
|
+
const BROAD_MARKET_INDEX = "^GSPC";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Facts that describe the market rather than the subject.
|
|
17
|
+
*
|
|
18
|
+
* Everything else the index block produces -- leverage, growth, breadth of a basket -- is about
|
|
19
|
+
* a portfolio the company is not, and forwarding it would answer a different question in the
|
|
20
|
+
* subject's name.
|
|
21
|
+
*/
|
|
22
|
+
const MARKET_LEVEL_FACTS = new Set([
|
|
23
|
+
"index.aggregate_pe_ttm",
|
|
24
|
+
"index.aggregate_pe_forward",
|
|
25
|
+
"index.aggregate_earnings_yield",
|
|
26
|
+
"index.dividend_yield",
|
|
27
|
+
"valuation.implied_erp",
|
|
28
|
+
"cycle.valuation_percentile",
|
|
29
|
+
]);
|
|
9
30
|
import { fetchOptionsChain } from "./options.mjs";
|
|
10
31
|
import { screenTicker } from "./screen.mjs";
|
|
11
32
|
import { resolveIndustry, industryCoverage } from "./industry.mjs";
|
|
@@ -93,6 +114,7 @@ export async function gatherGrounding({
|
|
|
93
114
|
now = new Date(),
|
|
94
115
|
language = "English",
|
|
95
116
|
signal,
|
|
117
|
+
budgetMs = null,
|
|
96
118
|
} = {}) {
|
|
97
119
|
const snapshotPolicy = liveSnapshotPolicy(asOf, { now });
|
|
98
120
|
const gatheredAt = now instanceof Date ? now.toISOString() : new Date(now).toISOString();
|
|
@@ -200,6 +222,22 @@ export async function gatherGrounding({
|
|
|
200
222
|
}));
|
|
201
223
|
}
|
|
202
224
|
|
|
225
|
+
// What else is this a bet on. Only for baskets: a single company's correlation to KOSPI is a
|
|
226
|
+
// fact about its sector, not about the company, and the seats that reason about crowding and
|
|
227
|
+
// position size are asking about the basket.
|
|
228
|
+
if (symbol && isFundOrIndex(out.instrument)) {
|
|
229
|
+
jobs.push(safely("cross-market", () => fetchCrossMarket(symbol, { signal })).then((r) => {
|
|
230
|
+
if (!r.ok) { out.unavailable.push(r.error); return; }
|
|
231
|
+
if (r.value.facts.length) out.cross_market = r.value.facts;
|
|
232
|
+
out.unavailable.push(...r.value.unavailable);
|
|
233
|
+
}));
|
|
234
|
+
jobs.push(safely("sector dispersion", () => fetchSectorDispersion(SECTOR_SPDRS, { signal })).then((r) => {
|
|
235
|
+
if (!r.ok) { out.unavailable.push(r.error); return; }
|
|
236
|
+
if (r.value.available) out.sector_dispersion = r.value;
|
|
237
|
+
out.unavailable.push(...(r.value.unavailable || []));
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
|
|
203
241
|
if (cik) {
|
|
204
242
|
if (snapshotPolicy.allowed) {
|
|
205
243
|
// Resolve the registrant before deciding whether Company Facts applies. Scheduling
|
|
@@ -302,11 +340,32 @@ export async function gatherGrounding({
|
|
|
302
340
|
out.unavailable.push(`structured financials for ${symbol}: this adapter is not point-in-time versioned; current data was not fetched for a historical cutoff`);
|
|
303
341
|
}
|
|
304
342
|
|
|
343
|
+
// The market's own valuation, for every US subject rather than only for baskets.
|
|
344
|
+
//
|
|
345
|
+
// Three seats require `index.aggregate_earnings_yield` and reported it missing on every
|
|
346
|
+
// single-company run this product has ever done. They were right to ask: the market's
|
|
347
|
+
// aggregate earnings yield is not a property of the subject, it is the yardstick the subject
|
|
348
|
+
// is measured against. Damodaran's implied premium needs it, Asness's Fed-model critique IS
|
|
349
|
+
// about it, and Marks reads it as where the cycle stands. The subject decides whose holdings
|
|
350
|
+
// get read; it does not decide whether the market has a price-earnings ratio.
|
|
351
|
+
if (symbol && !isFundOrIndex(out.instrument) && symbolMarket?.id === "US" && snapshotPolicy.allowed) {
|
|
352
|
+
jobs.push(safely("market valuation", () => gatherInstrumentFacts({
|
|
353
|
+
symbol: BROAD_MARKET_INDEX, instrument: { research_model: "index_aggregate", index_like: true },
|
|
354
|
+
asOf, signal, lookThroughFactIds: [],
|
|
355
|
+
})).then((r) => {
|
|
356
|
+
if (!r.ok) { out.unavailable.push(r.error); return; }
|
|
357
|
+
// Only the market-level block. A company's own leverage and growth come from its filings,
|
|
358
|
+
// and taking them from the index would be silently answering a different question.
|
|
359
|
+
const marketFacts = (r.value.facts || []).filter((fact) => MARKET_LEVEL_FACTS.has(fact.fact_id));
|
|
360
|
+
if (marketFacts.length) out.market_valuation = { ...r.value, facts: marketFacts };
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
|
|
305
364
|
// A fund or index has no issuer financials, so this is where its evidence comes from
|
|
306
365
|
// instead: published holdings, index-level valuation and the look-through aggregates that
|
|
307
366
|
// let an operating-company method run against a basket at all.
|
|
308
367
|
if (symbol && isFundOrIndex(out.instrument) && snapshotPolicy.allowed) {
|
|
309
|
-
|
|
368
|
+
const instrumentJob = safely("instrument aggregate", () => gatherInstrumentFacts({
|
|
310
369
|
symbol, instrument: out.instrument, asOf, signal,
|
|
311
370
|
// The operating-company facts a basket can supply at all: everything the method seats
|
|
312
371
|
// ask of a company, aggregated by weight across the constituents that publish it.
|
|
@@ -315,7 +374,21 @@ export async function gatherGrounding({
|
|
|
315
374
|
if (!r.ok) { out.unavailable.push(r.error); return; }
|
|
316
375
|
out.instrument_aggregate = r.value;
|
|
317
376
|
out.unavailable.push(...(r.value.unavailable || []));
|
|
318
|
-
})
|
|
377
|
+
});
|
|
378
|
+
jobs.push(instrumentJob);
|
|
379
|
+
// A basket has no press office and files nothing, so its news comes from what it holds.
|
|
380
|
+
// Chained after the instrument pass rather than run beside it, because the holdings are
|
|
381
|
+
// what name the industry -- fetching them twice to avoid the wait would cost more than it
|
|
382
|
+
// saves.
|
|
383
|
+
jobs.push((async () => {
|
|
384
|
+
await instrumentJob;
|
|
385
|
+
const holdings = out.instrument_aggregate?.holdings;
|
|
386
|
+
if (!holdings?.length) return;
|
|
387
|
+
const news = await safely("basket news", () => fetchBasketNews(holdings, { asOf, signal }));
|
|
388
|
+
if (!news.ok) { out.unavailable.push(news.error); return; }
|
|
389
|
+
if (news.value.available) out.basket_news = news.value;
|
|
390
|
+
out.unavailable.push(...(news.value.unavailable || []));
|
|
391
|
+
})());
|
|
319
392
|
}
|
|
320
393
|
|
|
321
394
|
if (symbol && isFundOrIndex(out.instrument)) {
|
|
@@ -347,7 +420,32 @@ export async function gatherGrounding({
|
|
|
347
420
|
out.unavailable.push("industry map: the curated map is not publication-versioned and was excluded from the historical information set");
|
|
348
421
|
}
|
|
349
422
|
|
|
350
|
-
|
|
423
|
+
// Return what arrived, not nothing.
|
|
424
|
+
//
|
|
425
|
+
// A caller that raced this whole function against a timer threw away a completed quote, a
|
|
426
|
+
// completed screen and a completed filing set because one slow feed had not landed -- and the
|
|
427
|
+
// analysts downstream, handed an empty object, reported "no ticker was provided". The symbol
|
|
428
|
+
// had been provided; the fetch had not finished. Settling at the budget keeps everything that
|
|
429
|
+
// did arrive and names what did not.
|
|
430
|
+
if (Number.isFinite(budgetMs) && budgetMs > 0) {
|
|
431
|
+
let settled = false;
|
|
432
|
+
const all = Promise.all(jobs).then(() => { settled = true; });
|
|
433
|
+
let timer;
|
|
434
|
+
await Promise.race([
|
|
435
|
+
all,
|
|
436
|
+
new Promise((resolve) => { timer = setTimeout(resolve, budgetMs); }),
|
|
437
|
+
]);
|
|
438
|
+
clearTimeout(timer);
|
|
439
|
+
if (!settled) {
|
|
440
|
+
out.partial = true;
|
|
441
|
+
out.unavailable.push(
|
|
442
|
+
`grounding budget of ${Math.round(budgetMs)}ms elapsed before every source answered;`
|
|
443
|
+
+ " this run carries what arrived by then and names the rest as gaps rather than discarding it",
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
await Promise.all(jobs);
|
|
448
|
+
}
|
|
351
449
|
|
|
352
450
|
// Coverage across every symbol in play, so a report cannot quietly become US-only.
|
|
353
451
|
const inPlay = [symbol, ...(out.industry?.participants || []).map((p) => p.symbol)].filter(Boolean);
|
package/mcp/lib/industry.mjs
CHANGED
|
@@ -119,24 +119,45 @@ export const SIC_GROUPS = [
|
|
|
119
119
|
{ id: "mining_energy", range: [1000, 1499], title: { zh: "采矿与能源", en: "Mining and energy" } },
|
|
120
120
|
{ id: "construction", range: [1500, 1799], title: { zh: "建筑", en: "Construction" } },
|
|
121
121
|
{ id: "food_beverage", range: [2000, 2199], title: { zh: "食品饮料", en: "Food and beverage" } },
|
|
122
|
+
{ id: "textiles_apparel", range: [2200, 2399], title: { zh: "纺织服装", en: "Textiles and apparel" } },
|
|
123
|
+
{ id: "paper_packaging", range: [2400, 2679], title: { zh: "造纸与包装", en: "Paper and packaging" } },
|
|
124
|
+
{ id: "publishing", range: [2700, 2799], title: { zh: "出版印刷", en: "Publishing and printing" } },
|
|
122
125
|
{ id: "chemicals", range: [2800, 2899], title: { zh: "化工", en: "Chemicals" } },
|
|
123
126
|
{ id: "pharma_biotech", range: [2833, 2836], title: { zh: "医药与生物科技", en: "Pharma and biotech" } },
|
|
124
127
|
{ id: "energy_refining", range: [2900, 2999], title: { zh: "炼化", en: "Refining" } },
|
|
128
|
+
{ id: "rubber_plastics", range: [3000, 3199], title: { zh: "橡胶塑料", en: "Rubber and plastics" } },
|
|
129
|
+
{ id: "building_materials", range: [3200, 3299], title: { zh: "建材", en: "Building materials" } },
|
|
125
130
|
{ id: "metals", range: [3300, 3399], title: { zh: "金属", en: "Metals" } },
|
|
131
|
+
{ id: "fabricated_metal", range: [3400, 3499], title: { zh: "金属制品", en: "Fabricated metal products" } },
|
|
126
132
|
{ id: "machinery", range: [3500, 3569], title: { zh: "机械", en: "Machinery" } },
|
|
127
133
|
{ id: "computers_hardware", range: [3570, 3579], title: { zh: "计算机硬件", en: "Computers and hardware" } },
|
|
134
|
+
{ id: "industrial_equipment", range: [3580, 3599], title: { zh: "工业设备", en: "Industrial equipment" } },
|
|
128
135
|
{ id: "electronics", range: [3600, 3673], title: { zh: "电子", en: "Electronics" } },
|
|
129
136
|
{ id: "semiconductors", range: [3674, 3674], title: { zh: "半导体", en: "Semiconductors" } },
|
|
130
|
-
{ id: "
|
|
137
|
+
{ id: "electrical_equipment", range: [3675, 3699], title: { zh: "电气设备", en: "Electrical equipment" } },
|
|
138
|
+
{ id: "autos", range: [3700, 3719], title: { zh: "汽车", en: "Automobiles" } },
|
|
139
|
+
{ id: "aerospace_defense", range: [3720, 3799], title: { zh: "航空航天与国防", en: "Aerospace and defense" } },
|
|
140
|
+
{ id: "instruments_medical", range: [3800, 3879], title: { zh: "仪器与医疗器械", en: "Instruments and medical devices" } },
|
|
141
|
+
{ id: "manufacturing_other", range: [3880, 3999], title: { zh: "其他制造", en: "Other manufacturing" } },
|
|
131
142
|
{ id: "transportation", range: [4000, 4799], title: { zh: "运输", en: "Transportation" } },
|
|
132
143
|
{ id: "telecom", range: [4800, 4899], title: { zh: "电信", en: "Telecom" } },
|
|
133
144
|
{ id: "utilities", range: [4900, 4999], title: { zh: "公用事业", en: "Utilities" } },
|
|
145
|
+
{ id: "wholesale", range: [5000, 5199], title: { zh: "批发分销", en: "Wholesale and distribution" } },
|
|
134
146
|
{ id: "retail", range: [5200, 5999], title: { zh: "零售", en: "Retail" } },
|
|
135
|
-
{ id: "banks", range: [
|
|
147
|
+
{ id: "banks", range: [6000, 6199], title: { zh: "银行", en: "Banks" } },
|
|
148
|
+
{ id: "brokers_asset_managers", range: [6200, 6299], title: { zh: "券商与资产管理", en: "Brokers and asset managers" } },
|
|
136
149
|
{ id: "insurance", range: [6300, 6411], title: { zh: "保险", en: "Insurance" } },
|
|
150
|
+
{ id: "financial_services", range: [6412, 6499], title: { zh: "金融服务", en: "Financial services" } },
|
|
137
151
|
{ id: "real_estate", range: [6500, 6799], title: { zh: "房地产", en: "Real estate" } },
|
|
152
|
+
{ id: "hospitality_leisure", range: [7000, 7099], title: { zh: "酒店与休闲", en: "Hospitality and leisure" } },
|
|
153
|
+
{ id: "business_services", range: [7100, 7369], title: { zh: "商业服务", en: "Business services" } },
|
|
138
154
|
{ id: "software", range: [7370, 7379], title: { zh: "软件与IT服务", en: "Software and IT services" } },
|
|
155
|
+
{ id: "consumer_services", range: [7380, 7799], title: { zh: "消费服务", en: "Consumer services" } },
|
|
156
|
+
{ id: "media_entertainment", range: [7800, 7999], title: { zh: "影视与娱乐", en: "Media and entertainment" } },
|
|
139
157
|
{ id: "healthcare_services", range: [8000, 8099], title: { zh: "医疗服务", en: "Healthcare services" } },
|
|
158
|
+
{ id: "professional_services", range: [8100, 8730], title: { zh: "专业服务", en: "Professional services" } },
|
|
159
|
+
{ id: "research_services", range: [8731, 8734], title: { zh: "研究服务", en: "Research services" } },
|
|
160
|
+
{ id: "other_services", range: [8735, 8999], title: { zh: "其他服务", en: "Other services" } },
|
|
140
161
|
];
|
|
141
162
|
|
|
142
163
|
export function sicGroupFor(sic) {
|
|
@@ -26,6 +26,9 @@ const OWNERSHIP_FORMS = new Set(["3", "4", "5", "3/A", "4/A", "5/A"]);
|
|
|
26
26
|
/** A registrant with more insiders than this is not a governance question, it is a bad parse. */
|
|
27
27
|
const MAX_DISTINCT_OWNERS = 60;
|
|
28
28
|
|
|
29
|
+
/** Filings read per round. SEC's ~10 req/s guidance is enforced by the client's own throttle. */
|
|
30
|
+
const OWNERSHIP_BATCH = 8;
|
|
31
|
+
|
|
29
32
|
/** Below this share of the register the aggregate says nothing, so it refuses instead. */
|
|
30
33
|
export const MIN_OWNER_COVERAGE = 1;
|
|
31
34
|
|
|
@@ -105,19 +108,27 @@ export async function fetchInsiderOwnership(cik, { sharesOutstanding, asOf = nul
|
|
|
105
108
|
const byOwner = new Map();
|
|
106
109
|
const sources = [];
|
|
107
110
|
let newestFiling = null;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
// Read in bounded concurrent batches rather than one filing at a time. Sequentially this was
|
|
112
|
+
// up to sixty throttled round trips on the critical path of every company run, which is what
|
|
113
|
+
// pushed grounding past its budget -- and a budget overrun used to discard the whole run's
|
|
114
|
+
// evidence. Newest-first order is preserved across batches, so an owner is still taken from
|
|
115
|
+
// their most recent filing.
|
|
116
|
+
const candidates = ownershipFilings.slice(0, MAX_DISTINCT_OWNERS * 3);
|
|
117
|
+
for (let offset = 0; offset < candidates.length && byOwner.size < MAX_DISTINCT_OWNERS; offset += OWNERSHIP_BATCH) {
|
|
118
|
+
const batch = candidates.slice(offset, offset + OWNERSHIP_BATCH);
|
|
119
|
+
const documents = await Promise.all(batch.map((filing) => (
|
|
120
|
+
fetchFilingDocument(index.cik, filing.accession, filing.primary_document, { signal })
|
|
121
|
+
.then((document) => ({ filing, document }))
|
|
122
|
+
.catch(() => null)
|
|
123
|
+
)));
|
|
124
|
+
for (const entry of documents) {
|
|
125
|
+
if (!entry) continue;
|
|
126
|
+
const parsed = parseOwnershipDocument(entry.document.text);
|
|
127
|
+
if (!parsed || byOwner.has(parsed.owner_cik)) continue;
|
|
128
|
+
byOwner.set(parsed.owner_cik, { ...parsed, filing_date: entry.filing.filing_date, url: entry.document.url });
|
|
129
|
+
sources.push(`sec:ownership:${index.cik}:${entry.filing.accession}`);
|
|
130
|
+
if (!newestFiling || entry.filing.filing_date > newestFiling) newestFiling = entry.filing.filing_date;
|
|
115
131
|
}
|
|
116
|
-
const parsed = parseOwnershipDocument(document.text);
|
|
117
|
-
if (!parsed || byOwner.has(parsed.owner_cik)) continue;
|
|
118
|
-
byOwner.set(parsed.owner_cik, { ...parsed, filing_date: filing.filing_date, url: document.url });
|
|
119
|
-
sources.push(`sec:ownership:${index.cik}:${filing.accession}`);
|
|
120
|
-
if (!newestFiling || filing.filing_date > newestFiling) newestFiling = filing.filing_date;
|
|
121
132
|
}
|
|
122
133
|
if (byOwner.size < MIN_OWNER_COVERAGE) {
|
|
123
134
|
return gap(`insider ownership: no Section 16 document for ${index.name || cik} could be parsed`);
|
|
@@ -839,6 +839,9 @@ export async function gatherInstrumentFacts({
|
|
|
839
839
|
symbol,
|
|
840
840
|
research_model: instrument?.research_model || "market_instrument",
|
|
841
841
|
provenance,
|
|
842
|
+
// The disclosed positions themselves, so a caller that needs the basket's identity rather
|
|
843
|
+
// than its aggregates -- news, for one -- does not fetch the holdings file a second time.
|
|
844
|
+
holdings: holdings?.holdings || null,
|
|
842
845
|
facts,
|
|
843
846
|
// Several facts have more than one route -- the implied ERP and the valuation percentile
|
|
844
847
|
// are computed from the published workbook when the index feed cannot supply them -- and
|
package/mcp/lib/markdown.mjs
CHANGED
|
@@ -118,25 +118,33 @@ const MASTER_STATEMENT_COPY = Object.freeze({
|
|
|
118
118
|
heading: "\u9010\u5e2d\u65b9\u6cd5\u8f93\u51fa", acted: "\u6709\u5224\u65ad\u7684\u5e2d\u4f4d", abstained: "\u8bf4\u8fd9\u4e0d\u5f52\u5b83\u7ba1\u7684\u5e2d\u4f4d",
|
|
119
119
|
stance: "\u7acb\u573a", intent: "\u610f\u5411", origin: "\u9648\u8bcd\u6765\u6e90", statement: "\u672c\u8f6e\u53d1\u8a00\uff08\u4e0d\u662f\u672c\u4eba\u5f15\u8bed\uff09",
|
|
120
120
|
findings: "\u5173\u952e\u53d1\u73b0", disagreements: "\u4e0e\u5206\u6790\u5e08\u5206\u6b67", change: "\u6539\u53d8\u5224\u65ad\u6761\u4ef6", sources: "\u6765\u6e90\u6216\u660e\u786e\u7f3a\u53e3",
|
|
121
|
-
abstainLead: (n) => `\u53e6\u6709 ${n} \u5e2d\
|
|
121
|
+
abstainLead: (n) => `\u53e6\u6709 ${n} \u5e2d\u672c\u8f6e\u672a\u80fd\u53d6\u5f97\u5176\u65b9\u6cd5\u5fc5\u9700\u7684\u8f93\u5165\uff0c\u56e0\u6b64\u6ca1\u6709\u7ed9\u51fa\u65b9\u5411\u3002\u8fd9\u662f\u6570\u636e\u7f3a\u53e3\uff0c\u4e0d\u662f\u770b\u7a7a\u7968\uff1a`,
|
|
122
|
+
declined: "\u770b\u8fc7\u4e4b\u540e\u51b3\u5b9a\u4e0d\u53c2\u4e0e\u7684\u5e2d\u4f4d",
|
|
123
|
+
declinedLead: (n) => `\u4ee5\u4e0b ${n} \u5e2d\u7684\u65b9\u6cd5\u8dd1\u5b8c\u4e86\uff0c\u5e76\u4e14\u5f97\u51fa\u4e86\u201c\u4e0d\u662f\u8fd9\u4e2a\u201d\u3002\u8fd9\u662f\u5224\u65ad\uff0c\u4e0d\u662f\u7f3a\u6570\u636e\uff1a`,
|
|
122
124
|
},
|
|
123
125
|
en: {
|
|
124
126
|
heading: "Method-Seat Outputs", acted: "Seats with a view", abstained: "Seats that say this is not theirs to call",
|
|
125
127
|
stance: "Stance", intent: "Intent", origin: "Statement source", statement: "Recorded statement (not a quote)",
|
|
126
128
|
findings: "Key findings", disagreements: "Disagreements", change: "What would change the view", sources: "Sources or explicit gaps",
|
|
127
|
-
abstainLead: (n) => `A further ${n} seat(s) issue no direction
|
|
129
|
+
abstainLead: (n) => `A further ${n} seat(s) issue no direction because a method-critical input did not arrive this round. This is a data gap, not a bearish vote:`,
|
|
130
|
+
declined: "Seats whose method examined this and declined",
|
|
131
|
+
declinedLead: (n) => `${n} seat(s) ran their method to completion and it returned "not this one". These are judgments, not missing data:`,
|
|
128
132
|
},
|
|
129
133
|
ja: {
|
|
130
134
|
heading: "\u30e1\u30bd\u30c3\u30c9\u5e2d\u3054\u3068\u306e\u51fa\u529b", acted: "\u5224\u65ad\u3092\u793a\u3057\u305f\u5e2d", abstained: "\u81ea\u5206\u306e\u62c5\u5f53\u3067\u306f\u306a\u3044\u3068\u3057\u305f\u5e2d",
|
|
131
135
|
stance: "\u30b9\u30bf\u30f3\u30b9", intent: "\u610f\u5411", origin: "\u898b\u89e3\u306e\u751f\u6210\u5143", statement: "\u4eca\u56de\u306e\u767a\u8a00\uff08\u672c\u4eba\u306e\u5f15\u7528\u3067\u306f\u3042\u308a\u307e\u305b\u3093\uff09",
|
|
132
136
|
findings: "\u4e3b\u306a\u6240\u898b", disagreements: "\u5206\u6790\u62c5\u5f53\u3068\u306e\u76f8\u9055", change: "\u5224\u65ad\u304c\u5909\u308f\u308b\u6761\u4ef6", sources: "\u51fa\u5178\u307e\u305f\u306f\u660e\u793a\u7684\u306a\u6b20\u843d",
|
|
133
|
-
abstainLead: (n) => `\u4ed6\u306b ${n} \u5e2d\u306f\u4eca\u56de\u65b9\u5411\u6027\u3092\u793a\u3057\u307e\u305b\u3093\u3002\
|
|
137
|
+
abstainLead: (n) => `\u4ed6\u306b ${n} \u5e2d\u306f\u3001\u30e1\u30bd\u30c3\u30c9\u306b\u5fc5\u8981\u306a\u5165\u529b\u304c\u4eca\u56de\u5c4a\u304b\u306a\u304b\u3063\u305f\u305f\u3081\u65b9\u5411\u6027\u3092\u793a\u3057\u307e\u305b\u3093\u3002\u30c7\u30fc\u30bf\u306e\u6b20\u843d\u3067\u3042\u308a\u3001\u5f31\u6c17\u7968\u3067\u306f\u3042\u308a\u307e\u305b\u3093\uff1a`,
|
|
138
|
+
declined: "\u691c\u8a0e\u3057\u305f\u4e0a\u3067\u898b\u9001\u3063\u305f\u5e2d",
|
|
139
|
+
declinedLead: (n) => `\u6b21\u306e ${n} \u5e2d\u306f\u30e1\u30bd\u30c3\u30c9\u3092\u6700\u5f8c\u307e\u3067\u5b9f\u884c\u3057\u3001\u300c\u3053\u308c\u3067\u306f\u306a\u3044\u300d\u3068\u7d50\u8ad6\u3057\u307e\u3057\u305f\u3002\u30c7\u30fc\u30bf\u4e0d\u8db3\u3067\u306f\u306a\u304f\u5224\u65ad\u3067\u3059\uff1a`,
|
|
134
140
|
},
|
|
135
141
|
ko: {
|
|
136
142
|
heading: "\ubc29\ubc95\ub860 \uc88c\uc11d\ubcc4 \ucd9c\ub825", acted: "\ud310\ub2e8\uc744 \ub0b8 \uc88c\uc11d", abstained: "\uc790\uae30 \uc18c\uad00\uc774 \uc544\ub2c8\ub77c\uace0 \ubc1d\ud78c \uc88c\uc11d",
|
|
137
143
|
stance: "\uc785\uc7a5", intent: "\uc758\ud5a5", origin: "\ubc1c\uc5b8 \ucd9c\ucc98", statement: "\uc774\ubc88 \ubc1c\uc5b8(\ubcf8\uc778 \uc778\uc6a9\uc774 \uc544\ub2d8)",
|
|
138
144
|
findings: "\ud575\uc2ec \ubc1c\uacac", disagreements: "\ubd84\uc11d\uac00\uc640\uc758 \uc774\uacac", change: "\ud310\ub2e8 \ubcc0\uacbd \uc870\uac74", sources: "\ucd9c\ucc98 \ub610\ub294 \uba85\uc2dc\uc801 \ub370\uc774\ud130 \uacf5\ubc31",
|
|
139
|
-
abstainLead: (n) => `\uadf8 \uc678 ${n}\uac1c \uc88c\uc11d\uc740 \uc774\ubc88 \ud68c\ucc28\uc5d0 \ubc29\ud5a5\uc744 \uc81c\uc2dc\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \
|
|
145
|
+
abstainLead: (n) => `\uadf8 \uc678 ${n}\uac1c \uc88c\uc11d\uc740 \ubc29\ubc95\ub860\uc5d0 \ud544\uc694\ud55c \uc785\ub825\uc774 \uc774\ubc88 \ud68c\ucc28\uc5d0 \ub3c4\ucc29\ud558\uc9c0 \uc54a\uc544 \ubc29\ud5a5\uc744 \uc81c\uc2dc\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ub370\uc774\ud130 \uacf5\ubc31\uc774\uba70 \uc57d\uc138 \ud22c\ud45c\uac00 \uc544\ub2d9\ub2c8\ub2e4:`,
|
|
146
|
+
declined: "\uac80\ud1a0 \ud6c4 \ucc38\uc5ec\ud558\uc9c0 \uc54a\uae30\ub85c \ud55c \uc88c\uc11d",
|
|
147
|
+
declinedLead: (n) => `\ub2e4\uc74c ${n}\uac1c \uc88c\uc11d\uc740 \ubc29\ubc95\ub860\uc744 \ub05d\uae4c\uc9c0 \uc218\ud589\ud588\uace0 "\uc774\uac83\uc740 \uc544\ub2c8\ub2e4"\ub77c\ub294 \uacb0\ub860\uc5d0 \ub3c4\ub2ec\ud588\uc2b5\ub2c8\ub2e4. \ub370\uc774\ud130 \ubd80\uc871\uc774 \uc544\ub2c8\ub77c \ud310\ub2e8\uc785\ub2c8\ub2e4:`,
|
|
140
148
|
},
|
|
141
149
|
});
|
|
142
150
|
|
|
@@ -183,12 +191,25 @@ function renderMasterStatements(run) {
|
|
|
183
191
|
const sections = [`### ${copy.heading}`, voiceDisclaimer(run.language)];
|
|
184
192
|
if (acted.length) sections.push(`#### ${copy.acted}`, acted.map(seatBlock).join("\n\n"));
|
|
185
193
|
if (abstained.length) {
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
194
|
+
// Two reasons wear the same word and they are not the same event. A seat whose method
|
|
195
|
+
// examined the subject and declined has ANSWERED -- Graham finding no asset floor, a
|
|
196
|
+
// volatility seat finding no testable observation -- and reporting that as "missing an
|
|
197
|
+
// input" tells a reader the system broke when the method spoke. A seat whose required
|
|
198
|
+
// fact never arrived genuinely is a gap. Rendering them together made every run read as
|
|
199
|
+
// the second kind, which is the complaint this split exists to answer.
|
|
200
|
+
// One paragraph each, not one row per seat: the stable IDs stay visible so the gate and
|
|
201
|
+
// the reader can both account for every selected seat.
|
|
202
|
+
const merged = (group) => group
|
|
189
203
|
.map((opinion) => `${masterTitle(opinion.master, run.language)} (\`${opinion.master}\`) \u2014 ${opinion.voice_statement}`)
|
|
190
204
|
.join(" ");
|
|
191
|
-
|
|
205
|
+
const declined = abstained.filter((opinion) => methodDeclined(opinion));
|
|
206
|
+
const ungrounded = abstained.filter((opinion) => !methodDeclined(opinion));
|
|
207
|
+
if (declined.length) {
|
|
208
|
+
sections.push(`#### ${copy.declined}`, `${copy.declinedLead(declined.length)}\n\n${merged(declined)}`);
|
|
209
|
+
}
|
|
210
|
+
if (ungrounded.length) {
|
|
211
|
+
sections.push(`#### ${copy.abstained}`, `${copy.abstainLead(ungrounded.length)}\n\n${merged(ungrounded)}`);
|
|
212
|
+
}
|
|
192
213
|
}
|
|
193
214
|
return sections.join("\n\n");
|
|
194
215
|
}
|
|
@@ -202,6 +223,18 @@ function renderMasterStatements(run) {
|
|
|
202
223
|
* weakest thing a council produces and the dissenting seat is the informative one. Stating
|
|
203
224
|
* this next to the opinions is the difference between a bench and a vote count.
|
|
204
225
|
*/
|
|
226
|
+
/**
|
|
227
|
+
* Did the seat's own method reach this, or did the data never arrive?
|
|
228
|
+
*
|
|
229
|
+
* A fired veto and a false eligibility condition are both the method running to completion and
|
|
230
|
+
* returning "not this one". Anything else -- an unmet required fact type, a coverage shortfall,
|
|
231
|
+
* an executor refusal -- is the run failing to give the method what it asked for.
|
|
232
|
+
*/
|
|
233
|
+
export function methodDeclined(opinion) {
|
|
234
|
+
const reason = String(opinion?.decision?.reason || opinion?.reason || "");
|
|
235
|
+
return reason === "veto" || reason === "eligibility";
|
|
236
|
+
}
|
|
237
|
+
|
|
205
238
|
export function masterCorrelationNote(run) {
|
|
206
239
|
const opinions = run?.master_opinions || [];
|
|
207
240
|
if (!opinions.length) return "";
|
package/mcp/lib/orchestrator.mjs
CHANGED
|
@@ -1200,7 +1200,15 @@ export async function groundingForHeadlessRun({ symbol, asOf, grounding, dryRun,
|
|
|
1200
1200
|
let timer;
|
|
1201
1201
|
const controller = Number.isFinite(timeoutMs) ? new AbortController() : null;
|
|
1202
1202
|
try {
|
|
1203
|
-
|
|
1203
|
+
// The budget is handed DOWN so grounding can settle and return what it has. Racing it from
|
|
1204
|
+
// out here discarded a completed quote and a completed screen whenever one feed was slow,
|
|
1205
|
+
// and the analysts then reported a missing ticker for a symbol that had been supplied.
|
|
1206
|
+
// The outer race stays as a backstop, with headroom, for a call that hangs entirely.
|
|
1207
|
+
const work = gather({
|
|
1208
|
+
symbol, asOf,
|
|
1209
|
+
...(Number.isFinite(timeoutMs) ? { budgetMs: timeoutMs } : {}),
|
|
1210
|
+
...(controller ? { signal: controller.signal } : {}),
|
|
1211
|
+
});
|
|
1204
1212
|
if (!Number.isFinite(timeoutMs)) return await work;
|
|
1205
1213
|
return await Promise.race([
|
|
1206
1214
|
work,
|
|
@@ -1209,7 +1217,7 @@ export async function groundingForHeadlessRun({ symbol, asOf, grounding, dryRun,
|
|
|
1209
1217
|
const error = new Error(`quick grounding timed out after ${Math.round(timeoutMs)}ms`);
|
|
1210
1218
|
reject(error);
|
|
1211
1219
|
controller?.abort(error);
|
|
1212
|
-
}, timeoutMs);
|
|
1220
|
+
}, timeoutMs + LIMITS.GROUNDING_SETTLE_HEADROOM_MS);
|
|
1213
1221
|
}),
|
|
1214
1222
|
]);
|
|
1215
1223
|
} catch (error) {
|
|
@@ -13,6 +13,8 @@ import { typedFactPackFromGrounding } from "../personas-v3/grounding-adapter.mjs
|
|
|
13
13
|
import { buildAnonymousPreDecision, freezeAnonymousDecision } from "../personas-v3/runtime.mjs";
|
|
14
14
|
import { executeDeterministicPersonaPolicy } from "../personas-v3/deterministic-executor.mjs";
|
|
15
15
|
import { languageKey, localized } from "../lang.mjs";
|
|
16
|
+
import { displayMasterLabel } from "../markdown.mjs";
|
|
17
|
+
import { voiceFromDecision, voiceFromDecline } from "../voice-from-decision.mjs";
|
|
16
18
|
import {
|
|
17
19
|
declinedOpinion as v2DeclinedOpinion,
|
|
18
20
|
planMasters as planLegacyMasters,
|
|
@@ -77,7 +79,19 @@ function planV3Seat(run, id, pack) {
|
|
|
77
79
|
pack,
|
|
78
80
|
};
|
|
79
81
|
}
|
|
80
|
-
|
|
82
|
+
// A seat with SOME of its required facts still runs its own policy.
|
|
83
|
+
//
|
|
84
|
+
// Several seats are authored with a veto that says, in the method's own words, what an absent
|
|
85
|
+
// fact means -- Pabrai passing without a downside floor, Graham without an asset floor. Those
|
|
86
|
+
// facts are also tool inputs, so a missing one used to end the run at this gate and the veto
|
|
87
|
+
// written for exactly that case never executed. The seat then reported "missing X", which is
|
|
88
|
+
// the runtime describing itself rather than the method answering.
|
|
89
|
+
//
|
|
90
|
+
// Vetoes are evaluated before scoring, and every tool, veto and rule already declares its own
|
|
91
|
+
// `on_missing` and `on_uncomputable` behaviour, so letting the policy run does not invent an
|
|
92
|
+
// answer: it reaches the authored one. A seat with NONE of its required facts is still a hard
|
|
93
|
+
// decline -- there is no method left to run.
|
|
94
|
+
if (preDecision.eligibility.status === "out_of_scope") {
|
|
81
95
|
const frozenDecision = freezeAnonymousDecision(preDecision);
|
|
82
96
|
return {
|
|
83
97
|
id,
|
|
@@ -106,9 +120,27 @@ function planV3Seat(run, id, pack) {
|
|
|
106
120
|
decision: v3DecisionRecord(id, pack, preDecision, frozenDecision),
|
|
107
121
|
};
|
|
108
122
|
} catch (error) {
|
|
109
|
-
// A
|
|
110
|
-
//
|
|
111
|
-
//
|
|
123
|
+
// A seat that could not run because a fact it needs is absent has DECLINED, not broken.
|
|
124
|
+
// Partially grounded seats now execute so their authored vetoes can answer, and a seat
|
|
125
|
+
// without such a veto reaches the same clean abstention it always did -- reporting that as
|
|
126
|
+
// a blocked policy would turn "there was nothing to compute with" into "the system
|
|
127
|
+
// failed", which is the confusion this whole pass exists to remove.
|
|
128
|
+
if (preDecision?.eligibility?.status === "insufficient_grounding" && error?.code === "MISSING_TOOL_INPUT") {
|
|
129
|
+
const frozenDecision = freezeAnonymousDecision(preDecision);
|
|
130
|
+
return {
|
|
131
|
+
id,
|
|
132
|
+
engine: "v3_method_runtime",
|
|
133
|
+
declined: true,
|
|
134
|
+
reason: preDecision.eligibility.status,
|
|
135
|
+
pack,
|
|
136
|
+
preDecision,
|
|
137
|
+
frozenDecision,
|
|
138
|
+
decision: v3DecisionRecord(id, pack, preDecision, frozenDecision),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// A malformed policy, unknown operation, or arithmetic failure blocks this physical v3
|
|
142
|
+
// seat. The seat still owns the ID and never falls through to a legacy prompt or an
|
|
143
|
+
// LLM-authored stance.
|
|
112
144
|
return {
|
|
113
145
|
id,
|
|
114
146
|
engine: "v3_method_runtime",
|
|
@@ -191,8 +223,12 @@ export function completedMasterOpinion(run, item) {
|
|
|
191
223
|
}
|
|
192
224
|
const result = item.frozenDecision.structured_decision.result;
|
|
193
225
|
const locale = languageKey(run.language);
|
|
194
|
-
|
|
195
|
-
|
|
226
|
+
// The pack's admitted label carries a maturity suffix -- "provisional operator lens" and its
|
|
227
|
+
// translations -- which belongs in the assurance section, not inside a sentence about the
|
|
228
|
+
// company. Left in, every statement read "using the X provisional operator lens method",
|
|
229
|
+
// which is both ungrammatical and a second copy of a disclosure the report already makes.
|
|
230
|
+
const label = displayMasterLabel(item.pack.admitted_label?.[locale]
|
|
231
|
+
|| item.pack.admitted_label?.en || item.id);
|
|
196
232
|
const copy = localized(run.language, {
|
|
197
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` },
|
|
198
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} 变为满足` },
|
|
@@ -223,6 +259,16 @@ export function completedMasterOpinion(run, item) {
|
|
|
223
259
|
verdict: copy.verdict(result.stance, result.reason),
|
|
224
260
|
summary: deterministicStatement,
|
|
225
261
|
voice_statement: deterministicStatement,
|
|
262
|
+
// The five fields, composed from the frozen decision rather than written about it: which
|
|
263
|
+
// facts were read and their values, what the tools produced, which scoring conditions held
|
|
264
|
+
// and which did not, and what would move the verdict. A one-line statement told a reader
|
|
265
|
+
// the stance and nothing about how it was reached.
|
|
266
|
+
voice: voiceFromDecision({
|
|
267
|
+
result,
|
|
268
|
+
policy: item.pack.components?.decision_policy,
|
|
269
|
+
factPack: item.preDecision?.fact_pack,
|
|
270
|
+
language: run.language,
|
|
271
|
+
}),
|
|
226
272
|
voice_status: "deterministic_fallback",
|
|
227
273
|
voice_language: run.language,
|
|
228
274
|
statement_origin: "deterministic_policy_fallback",
|
|
@@ -263,8 +309,12 @@ export function declinedMasterOpinion(run, item) {
|
|
|
263
309
|
if (item?.engine !== "v3_method_runtime") return v2DeclinedOpinion(run, item.id, item.decision);
|
|
264
310
|
const eligibility = item.preDecision.eligibility;
|
|
265
311
|
const locale = languageKey(run.language);
|
|
266
|
-
|
|
267
|
-
|
|
312
|
+
// The pack's admitted label carries a maturity suffix -- "provisional operator lens" and its
|
|
313
|
+
// translations -- which belongs in the assurance section, not inside a sentence about the
|
|
314
|
+
// company. Left in, every statement read "using the X provisional operator lens method",
|
|
315
|
+
// which is both ungrammatical and a second copy of a disclosure the report already makes.
|
|
316
|
+
const label = displayMasterLabel(item.pack.admitted_label?.[locale]
|
|
317
|
+
|| item.pack.admitted_label?.en || item.id);
|
|
268
318
|
const instrument = run?.grounding?.instrument;
|
|
269
319
|
const fundOrIndex = instrument?.fund_like === true || instrument?.index_like === true
|
|
270
320
|
|| ["etf", "mutual_fund", "index"].includes(instrument?.asset_type);
|
|
@@ -284,7 +334,14 @@ export function declinedMasterOpinion(run, item) {
|
|
|
284
334
|
verdict: copy.verdict,
|
|
285
335
|
summary: copy.summary(missing),
|
|
286
336
|
voice_statement: deterministicStatement,
|
|
287
|
-
|
|
337
|
+
// The five fields, composed from the frozen decision rather than written about it: which
|
|
338
|
+
// facts were read and their values, what the tools produced, which scoring conditions held
|
|
339
|
+
// and which did not, and what would move the verdict. A one-line statement told a reader
|
|
340
|
+
// the stance and nothing about how it was reached.
|
|
341
|
+
// An abstention is readable too: what the method did have, what it needed, and why it will
|
|
342
|
+
// not substitute a proxy for the difference.
|
|
343
|
+
voice: voiceFromDecline({ eligibility, language: run.language }),
|
|
344
|
+
voice_status: "deterministic_scope",
|
|
288
345
|
voice_language: run.language,
|
|
289
346
|
statement_origin: "deterministic_scope_fallback",
|
|
290
347
|
key_findings: [deterministicStatement],
|
|
@@ -701,9 +701,33 @@ function nativeMetrics(policy, facts, outputs) {
|
|
|
701
701
|
return { metrics, status };
|
|
702
702
|
}
|
|
703
703
|
|
|
704
|
+
/** True when a condition reads only typed facts and literals, so no tool has to run first. */
|
|
705
|
+
function readsOnlyFacts(node) {
|
|
706
|
+
if (!node || typeof node !== "object") return true;
|
|
707
|
+
if (Object.hasOwn(node, "output_id")) return false;
|
|
708
|
+
return Object.values(node).every((value) => (
|
|
709
|
+
Array.isArray(value) ? value.every(readsOnlyFacts) : readsOnlyFacts(value)
|
|
710
|
+
));
|
|
711
|
+
}
|
|
712
|
+
|
|
704
713
|
function executeValidatedPolicy(preDecision, policy, tools, pipeline) {
|
|
705
714
|
const facts = new Map(preDecision.fact_pack.facts.map((fact) => [fact.fact_id, fact]));
|
|
706
715
|
const outputs = new Map();
|
|
716
|
+
|
|
717
|
+
// A veto that reads only facts is evaluated before any tool runs.
|
|
718
|
+
//
|
|
719
|
+
// Several seats are authored with a veto that says what an ABSENT fact means -- Pabrai
|
|
720
|
+
// passing without a downside floor, Graham without an asset floor. That same fact is usually
|
|
721
|
+
// a tool input, and a tool declaring `on_missing: "fail"` aborts the whole policy before any
|
|
722
|
+
// veto is reached, so the seat reported a missing input and the author's answer never ran.
|
|
723
|
+
// Hoisting fact-only vetoes changes no arithmetic: they need nothing a tool produces, and a
|
|
724
|
+
// veto that reads a tool output still waits for it.
|
|
725
|
+
const vetoedOnFactsAlone = policy.hard_vetoes.some((record, index) => {
|
|
726
|
+
if (!readsOnlyFacts(record.condition)) return false;
|
|
727
|
+
const early = evaluateCondition(record.condition, facts, outputs, `decision_policy.hard_vetoes[${index}].condition`);
|
|
728
|
+
return early.computable ? Boolean(early.value) : record.on_uncomputable.action === "trigger";
|
|
729
|
+
});
|
|
730
|
+
|
|
707
731
|
const toolById = new Map(tools.map((tool) => [tool.id, tool]));
|
|
708
732
|
const toolTrace = [];
|
|
709
733
|
for (const toolId of pipeline) {
|
|
@@ -712,7 +736,12 @@ function executeValidatedPolicy(preDecision, policy, tools, pipeline) {
|
|
|
712
736
|
const resolved = tool.inputs.map((operand) => resolveOperand(operand, facts, outputs));
|
|
713
737
|
const missing = unique(resolved.flatMap((input) => input.missing_input_ids));
|
|
714
738
|
if (missing.length) {
|
|
715
|
-
|
|
739
|
+
// A tool that cannot run is fatal, EXCEPT when a fact-only veto has already decided this
|
|
740
|
+
// seat. Then the arithmetic it would have fed is irrelevant to the outcome, and aborting
|
|
741
|
+
// reports a missing input in place of the answer the author wrote for exactly this case.
|
|
742
|
+
if (tool.on_missing === "fail" && !vetoedOnFactsAlone) {
|
|
743
|
+
policyFail("MISSING_TOOL_INPUT", `tool ${tool.id} is missing an input`, { tool_id: tool.id, missing_input_ids: missing });
|
|
744
|
+
}
|
|
716
745
|
toolTrace.push({
|
|
717
746
|
tool_id: tool.id,
|
|
718
747
|
tool_version: tool.version,
|
|
@@ -894,7 +923,15 @@ function executeValidatedPolicy(preDecision, policy, tools, pipeline) {
|
|
|
894
923
|
/** Execute and freeze one ready anonymous pre-decision. */
|
|
895
924
|
export function executeDeterministicPersonaPolicy(preDecision) {
|
|
896
925
|
if (!isObject(preDecision) || preDecision.phase !== "anonymous_pre_decision") policyFail("INVALID_PRE_DECISION", "expected an anonymous_pre_decision payload");
|
|
897
|
-
|
|
926
|
+
// `insufficient_grounding` runs. Every tool, veto and rule declares its own `on_missing` and
|
|
927
|
+
// `on_uncomputable` behaviour, and those declarations are how a method states what an absent
|
|
928
|
+
// input means -- several seats are authored with a veto that answers exactly that case.
|
|
929
|
+
// Refusing to execute reported this gate instead of the method's own answer. Nothing is
|
|
930
|
+
// invented: a fact that is absent stays absent, and every reader of it takes its declared
|
|
931
|
+
// path. `out_of_scope` -- no required fact present at all -- is still a hard stop.
|
|
932
|
+
if (!["ready", "insufficient_grounding"].includes(preDecision.eligibility?.status)) {
|
|
933
|
+
policyFail("PRE_DECISION_NOT_READY", `typed-fact gate is ${preDecision.eligibility?.status || "invalid"}`);
|
|
934
|
+
}
|
|
898
935
|
const contract = preDecision.anonymous_method_contract;
|
|
899
936
|
const policy = contract?.decision_policy;
|
|
900
937
|
const tools = contract?.tools;
|