alphacouncil-agent 1.0.4 → 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 +282 -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-selection.mjs +79 -1
- 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 +21 -2
- 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/rpc.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import readline from "node:readline";
|
|
4
|
-
import { COUNCIL_MODES, LIMITS, MASTER_STANCES, OUTPUT_MODES, QUICK_TASKS, SERVER_NAME, VERSION } from "./constants.mjs";
|
|
4
|
+
import { COUNCIL_MODES, COUNCIL_PACE_NAMES, DEFAULT_COUNCIL_PACE, LIMITS, MASTER_STANCES, OUTPUT_MODES, QUICK_TASKS, SERVER_NAME, VERSION, councilPaceProfile } from "./constants.mjs";
|
|
5
5
|
import { RpcCode, methodNotFound, invalidParams, toRpcError } from "./errors.mjs";
|
|
6
6
|
import { readJson, readJsonl, writeJson } from "./fsutil.mjs";
|
|
7
7
|
import { localized, resolveLanguage } from "./lang.mjs";
|
|
@@ -147,6 +147,36 @@ export function jsonContent(text, structuredContent = {}) {
|
|
|
147
147
|
};
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
// Series history and per-fact lineage are the two unbounded parts of a grounding object: on one
|
|
151
|
+
// real run they were 2.33 MB of a 2.54 MB plan payload, which exceeded the host's tool-result
|
|
152
|
+
// limit and pushed the entire plan to a scratch file. They are already saved in evidence.json and
|
|
153
|
+
// baked into every seat prompt, so the plan response replaces them with a pointer and a count.
|
|
154
|
+
const BULK_GROUNDING_FIELDS = ["macro_series", "typed_fact_sources"];
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The grounding a plan response carries: the same object minus its two unbounded fields.
|
|
158
|
+
* Shape, flags and gap lists are preserved so a host can still read what was established.
|
|
159
|
+
*/
|
|
160
|
+
export function compactGrounding(run) {
|
|
161
|
+
const g = run?.grounding;
|
|
162
|
+
if (!g || typeof g !== "object") return g;
|
|
163
|
+
const compact = { ...g };
|
|
164
|
+
const omitted = [];
|
|
165
|
+
for (const field of BULK_GROUNDING_FIELDS) {
|
|
166
|
+
if (compact[field] === undefined) continue;
|
|
167
|
+
const count = Array.isArray(compact[field]) ? compact[field].length : Object.keys(compact[field] || {}).length;
|
|
168
|
+
omitted.push(`${field} (${count} entries)`);
|
|
169
|
+
delete compact[field];
|
|
170
|
+
}
|
|
171
|
+
if (!omitted.length) return compact;
|
|
172
|
+
compact.omitted_from_this_response = {
|
|
173
|
+
fields: omitted,
|
|
174
|
+
reason: "kept out of the plan payload for size; every seat prompt already carries them",
|
|
175
|
+
saved_to: join(runPath(run.run_id), "evidence.json"),
|
|
176
|
+
};
|
|
177
|
+
return compact;
|
|
178
|
+
}
|
|
179
|
+
|
|
150
180
|
export function tool(name, description, inputSchema, annotations = {}) {
|
|
151
181
|
return { name, description, inputSchema, annotations };
|
|
152
182
|
}
|
|
@@ -197,12 +227,34 @@ function startValidation(args, entryTool) {
|
|
|
197
227
|
maximum_ms: LIMITS.QUICK_HARD_MAX_MS,
|
|
198
228
|
});
|
|
199
229
|
}
|
|
200
|
-
if (
|
|
201
|
-
throw invalidParams(`
|
|
202
|
-
reason: "
|
|
203
|
-
|
|
230
|
+
if (args.council_pace !== undefined && !COUNCIL_PACE_NAMES.includes(String(args.council_pace))) {
|
|
231
|
+
throw invalidParams(`council_pace must be one of ${COUNCIL_PACE_NAMES.join(", ")}.`, {
|
|
232
|
+
reason: "INVALID_COUNCIL_PACE",
|
|
233
|
+
allowed: COUNCIL_PACE_NAMES,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (mode === "quick" && args.council_pace !== undefined) {
|
|
237
|
+
throw invalidParams("council_pace applies to the full council only. Quick is a smaller contract, not a slower one.", {
|
|
238
|
+
reason: "QUICK_PACE_FORBIDDEN",
|
|
204
239
|
});
|
|
205
240
|
}
|
|
241
|
+
// A call is held to its own pace's budget, not to the outer bound of the schema: asking for
|
|
242
|
+
// an hour at the fast pace would buy idle time, because what bounds each worker is its
|
|
243
|
+
// per-stage cap and those are set by the pace.
|
|
244
|
+
if (mode === "full" && args.total_timeout_ms !== undefined) {
|
|
245
|
+
const profile = councilPaceProfile(args.council_pace);
|
|
246
|
+
if (args.total_timeout_ms > profile.total_ms) {
|
|
247
|
+
throw invalidParams(
|
|
248
|
+
`Full council total_timeout_ms cannot exceed ${profile.total_ms} at the ${profile.pace} pace. Raise council_pace to buy more time.`,
|
|
249
|
+
{
|
|
250
|
+
reason: "FULL_TOTAL_TIMEOUT_EXCEEDS_MAX",
|
|
251
|
+
council_pace: profile.pace,
|
|
252
|
+
maximum_ms: profile.total_ms,
|
|
253
|
+
paces: Object.fromEntries(COUNCIL_PACE_NAMES.map((name) => [name, councilPaceProfile(name).total_ms])),
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
206
258
|
if (mode === "quick" && args.synthesis === false) {
|
|
207
259
|
throw invalidParams("Quick council requires its one-round bull/bear and portfolio-manager synthesis.", {
|
|
208
260
|
reason: "QUICK_SYNTHESIS_REQUIRED",
|
|
@@ -263,12 +315,30 @@ function selectedRunArgs(args = {}, entryTool) {
|
|
|
263
315
|
prompt: typeof args.prompt === "string" ? args.prompt : "",
|
|
264
316
|
council_mode: args.council_mode || "full",
|
|
265
317
|
});
|
|
318
|
+
// The pace was approved at the gate, so the receipt is authoritative: an execution call may
|
|
319
|
+
// repeat it but not change it. Otherwise a user could approve fifteen minutes and get an
|
|
320
|
+
// hour, or the reverse, with nothing in the record showing the switch.
|
|
321
|
+
if (args.council_pace !== undefined
|
|
322
|
+
&& selection.council_pace
|
|
323
|
+
&& String(args.council_pace) !== String(selection.council_pace)) {
|
|
324
|
+
throw invalidParams(
|
|
325
|
+
`council_pace ${args.council_pace} does not match the pace confirmed at the selection gate (${selection.council_pace}).`,
|
|
326
|
+
{
|
|
327
|
+
reason: "COUNCIL_PACE_RECEIPT_MISMATCH",
|
|
328
|
+
confirmed_council_pace: selection.council_pace,
|
|
329
|
+
submitted_council_pace: String(args.council_pace),
|
|
330
|
+
remedy: "Send the confirmed pace or start a new selection to change it.",
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
}
|
|
266
334
|
return {
|
|
267
335
|
...args,
|
|
268
336
|
run_id: id,
|
|
269
337
|
entry_tool: entryTool,
|
|
270
338
|
masters: selection.selected_master_ids,
|
|
271
339
|
master_selection: selection,
|
|
340
|
+
// The gate's decision wins when the caller omitted it.
|
|
341
|
+
council_pace: args.council_pace ?? selection.council_pace ?? undefined,
|
|
272
342
|
existing_run: existing,
|
|
273
343
|
release_run_lock: releaseRunLock,
|
|
274
344
|
};
|
|
@@ -400,10 +470,16 @@ export function tools() {
|
|
|
400
470
|
timeout_ms: { type: "number", default: LIMITS.CODEX_TIMEOUT_MS },
|
|
401
471
|
synthesis: { type: "boolean", default: true, description: "Run bull, bear, and portfolio-manager synthesis after evidence collection." },
|
|
402
472
|
synthesis_timeout_ms: { type: "number", default: LIMITS.CODEX_TIMEOUT_MS },
|
|
473
|
+
council_pace: {
|
|
474
|
+
type: "string",
|
|
475
|
+
enum: COUNCIL_PACE_NAMES,
|
|
476
|
+
default: DEFAULT_COUNCIL_PACE,
|
|
477
|
+
description: "Full-council depth/time tier. fast = 15 minutes, normal = 30, slow = 60. The tier sets the total budget AND every per-stage cap together: slow gives each evidence seat 12 minutes instead of 6 and each debate round 6 minutes per side instead of 150 seconds, which is where the extra depth comes from. Raising total_timeout_ms alone buys idle time, not depth. Quick rejects this field.",
|
|
478
|
+
},
|
|
403
479
|
total_timeout_ms: {
|
|
404
480
|
type: "number",
|
|
405
481
|
maximum: LIMITS.FULL_HARD_MAX_MS,
|
|
406
|
-
description: "Hard queue-to-persistence wall-clock budget.
|
|
482
|
+
description: "Hard queue-to-persistence wall-clock budget. Cannot exceed the selected council_pace's total (fast 900000, normal 1800000, slow 3600000) or ten minutes for quick. Callers may lower the applicable ceiling, never raise it.",
|
|
407
483
|
},
|
|
408
484
|
output_mode: { type: "string", enum: OUTPUT_MODES, default: "public_equity", description: "Final synthesis target shape." },
|
|
409
485
|
selection_receipt: { type: "string", description: "One-run receipt returned by confirm_master_selection. Required for every council run and consumed exactly once." },
|
|
@@ -426,6 +502,11 @@ export function tools() {
|
|
|
426
502
|
items: { type: "string", enum: masterIds },
|
|
427
503
|
description: "Optional stable IDs inferred from masters explicitly named in the request. They are highlighted only; the user must still submit this run's selection.",
|
|
428
504
|
},
|
|
505
|
+
council_pace: {
|
|
506
|
+
type: "string",
|
|
507
|
+
enum: COUNCIL_PACE_NAMES,
|
|
508
|
+
description: "Prefill only, when the request already named a speed such as fast or slow. It highlights that tier in the returned pace_options and never confirms one. Full only.",
|
|
509
|
+
},
|
|
429
510
|
},
|
|
430
511
|
required: ["symbol"],
|
|
431
512
|
}, { readOnlyHint: false, destructiveHint: false, openWorldHint: false }),
|
|
@@ -438,6 +519,11 @@ export function tools() {
|
|
|
438
519
|
selected_master_ids: { type: "array", minItems: 1, uniqueItems: true, items: { type: "string", enum: masterIds } },
|
|
439
520
|
select_all: { type: "boolean", const: true },
|
|
440
521
|
selection: { type: "string", minLength: 1, description: "Text fallback: stable numbers/ranges/IDs, or all." },
|
|
522
|
+
council_pace: {
|
|
523
|
+
type: "string",
|
|
524
|
+
enum: COUNCIL_PACE_NAMES,
|
|
525
|
+
description: "The depth tier the user picked from the pace_options this selection returned. Full only; omit to accept the default (normal, ~20 min expected, 30 min ceiling). Binds into the receipt, so an execution call may repeat it but never change it.",
|
|
526
|
+
},
|
|
441
527
|
},
|
|
442
528
|
required: ["selection_id", "catalog_hash", "display_ack"],
|
|
443
529
|
oneOf: [
|
|
@@ -793,7 +879,7 @@ export async function handleToolCall(id, params) {
|
|
|
793
879
|
+ `Network preflight: ${preflight.status}. `
|
|
794
880
|
+ `Established facts: ${run.grounding && !run.grounding.facts_unavailable ? "attached to every seat" : "UNAVAILABLE -- seats will run without filings or quotes, say so in the report"}.`,
|
|
795
881
|
{
|
|
796
|
-
run,
|
|
882
|
+
run: { ...run, grounding: compactGrounding(run) },
|
|
797
883
|
idempotent_replay: Boolean(runArgs.existing_run),
|
|
798
884
|
preflight,
|
|
799
885
|
...specs,
|
package/mcp/lib/run-store.mjs
CHANGED
|
@@ -70,6 +70,9 @@ export function statusSnapshot(run) {
|
|
|
70
70
|
language: run.language,
|
|
71
71
|
execution_mode: run.execution_mode,
|
|
72
72
|
council_mode: run.council_mode || "full",
|
|
73
|
+
// Which depth/time tier produced this run. Two runs of the same symbol at different paces
|
|
74
|
+
// are not the same analysis, so the pace belongs in the audit record beside the budget.
|
|
75
|
+
council_pace: run.council_pace || null,
|
|
73
76
|
debate_format: run.debate_format || "three_round_cross_exam",
|
|
74
77
|
visible_debate_contract: visibleDebate?.contract || null,
|
|
75
78
|
visible_debate_rounds_expected: visibleDebate?.rounds_expected || null,
|
package/mcp/lib/screen.mjs
CHANGED
|
@@ -17,6 +17,21 @@ const pct = (x) => (x === null ? null : Number((x * 100).toFixed(2)));
|
|
|
17
17
|
const last = (series, n) => series.slice(-n);
|
|
18
18
|
const sum = (xs) => xs.reduce((a, b) => a + b, 0);
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Pair two annual series by fiscal period end, newest last, capped at n pairs.
|
|
22
|
+
*
|
|
23
|
+
* Pairing by array position instead put every year after a gap onto the wrong
|
|
24
|
+
* counterpart -- a filer that stopped tagging InterestExpense had this year's EBIT
|
|
25
|
+
* divided by a two-year-old interest figure, and an equity series missing one middle
|
|
26
|
+
* year shifted a decade of ROE by one year -- while the provenance block still showed
|
|
27
|
+
* a clean range. A year present on only one side is dropped, never matched to a
|
|
28
|
+
* neighbour; the pair count is what the rule reports as its coverage.
|
|
29
|
+
*/
|
|
30
|
+
const pairByEnd = (a, b, n) => {
|
|
31
|
+
const byEnd = new Map(b.map((row) => [row.end, row]));
|
|
32
|
+
return a.filter((row) => byEnd.has(row.end)).slice(-n).map((row) => [row, byEnd.get(row.end)]);
|
|
33
|
+
};
|
|
34
|
+
|
|
20
35
|
/**
|
|
21
36
|
* XBRL reports share counts under the `shares` unit, not `USD`. Requesting the default unit
|
|
22
37
|
* for a share concept returns nothing, so the dilution rule reported `skipped` for every
|
|
@@ -117,41 +132,36 @@ export function evaluateRules(facts, { asOf = null } = {}) {
|
|
|
117
132
|
};
|
|
118
133
|
|
|
119
134
|
add("roe_10y", { en: "10-year average ROE", zh: "10年平均ROE" }, () => {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const n = Math.min(ni.length, eq.length);
|
|
124
|
-
const roes = [];
|
|
125
|
-
for (let i = 0; i < n; i += 1) {
|
|
126
|
-
const e = eq[eq.length - n + i].val;
|
|
127
|
-
if (e > 0) roes.push(ni[ni.length - n + i].val / e);
|
|
128
|
-
}
|
|
135
|
+
const pairs = pairByEnd(netIncome, equity, 10);
|
|
136
|
+
if (pairs.length < 5) return null;
|
|
137
|
+
const roes = pairs.filter(([, e]) => e.val > 0).map(([n, e]) => n.val / e.val);
|
|
129
138
|
if (roes.length < 5) return null;
|
|
130
139
|
const avg = sum(roes) / roes.length;
|
|
131
140
|
return {
|
|
132
141
|
passed: avg >= 0.08, value: pct(avg), unit: "%", threshold: 8, direction: "min", years: roes.length,
|
|
133
|
-
...ruleProvenance([
|
|
142
|
+
...ruleProvenance([pairs.map(([n]) => n), pairs.map(([, e]) => e)]),
|
|
134
143
|
};
|
|
135
144
|
});
|
|
136
145
|
|
|
137
146
|
add("fcf_5y", { en: "5-year cumulative free cash flow", zh: "5年累计自由现金流" }, () => {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const n = Math.min(o.length, c.length);
|
|
142
|
-
const total = sum(o.slice(-n).map((x) => x.val)) - sum(c.slice(-n).map((x) => x.val));
|
|
147
|
+
const pairs = pairByEnd(ocf, capex, 5);
|
|
148
|
+
if (pairs.length < 3) return null;
|
|
149
|
+
const total = sum(pairs.map(([o, c]) => o.val - c.val));
|
|
143
150
|
// Raw dollars, not billions rounded to two places: rounding erased the entire figure
|
|
144
151
|
// for anything below ~$5m, which is most of the small-cap universe.
|
|
145
152
|
return {
|
|
146
|
-
passed: total >= 0, value: Math.round(total), unit: "USD", threshold: 0, direction: "min", years:
|
|
147
|
-
...ruleProvenance([
|
|
153
|
+
passed: total >= 0, value: Math.round(total), unit: "USD", threshold: 0, direction: "min", years: pairs.length,
|
|
154
|
+
...ruleProvenance([pairs.map(([o]) => o), pairs.map(([, c]) => c)]),
|
|
148
155
|
};
|
|
149
156
|
});
|
|
150
157
|
|
|
151
158
|
add("interest_cover", { en: "EBIT / interest cover", zh: "利息保障倍数" }, () => {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
159
|
+
// The interest figure must belong to the same period as the latest EBIT. A filer that
|
|
160
|
+
// stops tagging InterestExpense gets a skip here, not a ratio built on old debt.
|
|
161
|
+
const pairs = pairByEnd(last(operatingIncome, 1), interest, 1);
|
|
162
|
+
if (!pairs.length) return null;
|
|
163
|
+
const [ebit, int] = pairs[0];
|
|
164
|
+
if (int.val === 0) return null;
|
|
155
165
|
const cover = ebit.val / Math.abs(int.val);
|
|
156
166
|
return {
|
|
157
167
|
passed: cover >= 2, value: Number(cover.toFixed(2)), unit: "x", threshold: 2, direction: "min",
|
|
@@ -160,48 +170,38 @@ export function evaluateRules(facts, { asOf = null } = {}) {
|
|
|
160
170
|
});
|
|
161
171
|
|
|
162
172
|
add("gross_margin", { en: "long-run gross margin", zh: "长期毛利率" }, () => {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const n = Math.min(gp.length, rev.length);
|
|
167
|
-
const margins = [];
|
|
168
|
-
for (let i = 0; i < n; i += 1) {
|
|
169
|
-
const r = rev[rev.length - n + i].val;
|
|
170
|
-
if (r > 0) margins.push(gp[gp.length - n + i].val / r);
|
|
171
|
-
}
|
|
173
|
+
const pairs = pairByEnd(grossProfit, revenue, 5);
|
|
174
|
+
if (pairs.length < 3) return null;
|
|
175
|
+
const margins = pairs.filter(([, r]) => r.val > 0).map(([g, r]) => g.val / r.val);
|
|
172
176
|
if (!margins.length) return null;
|
|
173
177
|
const avg = sum(margins) / margins.length;
|
|
174
178
|
return {
|
|
175
179
|
passed: avg >= 0.15, value: pct(avg), unit: "%", threshold: 15, direction: "min", years: margins.length,
|
|
176
|
-
...ruleProvenance([
|
|
180
|
+
...ruleProvenance([pairs.map(([g]) => g), pairs.map(([, r]) => r)]),
|
|
177
181
|
};
|
|
178
182
|
});
|
|
179
183
|
|
|
180
184
|
add("ocf_over_ni", { en: "5-year OCF / net income", zh: "5年经营现金流/净利" }, () => {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const n = Math.min(o.length, ni.length);
|
|
185
|
-
const totalNi = sum(ni.slice(-n).map((x) => x.val));
|
|
185
|
+
const pairs = pairByEnd(ocf, netIncome, 5);
|
|
186
|
+
if (pairs.length < 3) return null;
|
|
187
|
+
const totalNi = sum(pairs.map(([, n]) => n.val));
|
|
186
188
|
if (totalNi <= 0) return null;
|
|
187
|
-
const ratio = sum(
|
|
189
|
+
const ratio = sum(pairs.map(([o]) => o.val)) / totalNi;
|
|
188
190
|
return {
|
|
189
|
-
passed: ratio >= 0.7, value: Number(ratio.toFixed(2)), unit: "x", threshold: 0.7, direction: "min", years:
|
|
190
|
-
...ruleProvenance([
|
|
191
|
+
passed: ratio >= 0.7, value: Number(ratio.toFixed(2)), unit: "x", threshold: 0.7, direction: "min", years: pairs.length,
|
|
192
|
+
...ruleProvenance([pairs.map(([o]) => o), pairs.map(([, n]) => n)]),
|
|
191
193
|
};
|
|
192
194
|
});
|
|
193
195
|
|
|
194
196
|
add("net_margin", { en: "long-run net margin", zh: "长期净利率" }, () => {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const n = Math.min(ni.length, rev.length);
|
|
199
|
-
const totalRev = sum(rev.slice(-n).map((x) => x.val));
|
|
197
|
+
const pairs = pairByEnd(netIncome, revenue, 5);
|
|
198
|
+
if (pairs.length < 3) return null;
|
|
199
|
+
const totalRev = sum(pairs.map(([, r]) => r.val));
|
|
200
200
|
if (totalRev <= 0) return null;
|
|
201
|
-
const margin = sum(
|
|
201
|
+
const margin = sum(pairs.map(([n]) => n.val)) / totalRev;
|
|
202
202
|
return {
|
|
203
|
-
passed: margin >= 0.05, value: pct(margin), unit: "%", threshold: 5, direction: "min", years:
|
|
204
|
-
...ruleProvenance([
|
|
203
|
+
passed: margin >= 0.05, value: pct(margin), unit: "%", threshold: 5, direction: "min", years: pairs.length,
|
|
204
|
+
...ruleProvenance([pairs.map(([n]) => n), pairs.map(([, r]) => r)]),
|
|
205
205
|
};
|
|
206
206
|
});
|
|
207
207
|
|
package/mcp/lib/sec.mjs
CHANGED
|
@@ -97,20 +97,35 @@ export async function fetchFilingIndex(cik, { signal } = {}) {
|
|
|
97
97
|
return { cik: padded, name: data?.name || null, filings: rows };
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* One filing document, fetched as text. Same throttle, User-Agent and 429 backoff as the JSON
|
|
102
|
+
* calls.
|
|
103
|
+
*
|
|
104
|
+
* The backoff was the part missing here. Every JSON endpoint retried a rate-limited response
|
|
105
|
+
* while this one threw on the first 429, so the documents that carry the actual disclosure --
|
|
106
|
+
* a Form 4's transaction table, an 8-K's item text -- were the easiest evidence in the run to
|
|
107
|
+
* lose. `www.sec.gov/Archives` is also throttled harder than `data.sec.gov`, which makes this
|
|
108
|
+
* the path most likely to be limited and was the least protected against it.
|
|
109
|
+
*/
|
|
101
110
|
export async function fetchFilingDocument(cik, accession, document, { signal } = {}) {
|
|
102
|
-
await throttle();
|
|
103
111
|
const stripped = String(cik).replace(/\D/gu, "").replace(/^0+/u, "");
|
|
104
112
|
const folder = String(accession).replace(/-/gu, "");
|
|
105
113
|
const url = `https://www.sec.gov/Archives/edgar/data/${stripped}/${folder}/${document}`;
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
const outcome = await withRateLimitRetry(async () => {
|
|
115
|
+
await throttle();
|
|
116
|
+
const abort = linkedAbort(LIMITS.QUOTE_FETCH_MS * 2, signal);
|
|
117
|
+
try {
|
|
118
|
+
const res = await fetch(url, { signal: abort.signal, headers: { "User-Agent": UA } });
|
|
119
|
+
if (isRateLimited(res.status)) {
|
|
120
|
+
return { rateLimited: true, error: new Error(`HTTP ${res.status} for ${url}`) };
|
|
121
|
+
}
|
|
122
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
|
|
123
|
+
return { rateLimited: false, value: { url, text: await res.text() } };
|
|
124
|
+
} finally {
|
|
125
|
+
abort.cleanup();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
return outcome.value;
|
|
114
129
|
}
|
|
115
130
|
|
|
116
131
|
/** The full US listed universe: ~10k entries of {cik, ticker, title}. */
|
|
@@ -119,8 +119,11 @@ function copyFor(language) {
|
|
|
119
119
|
*
|
|
120
120
|
* `result` is the frozen `structured_decision.result`; `policy` is the seat's decision policy,
|
|
121
121
|
* which supplies the human-readable condition ids the result refers to by index.
|
|
122
|
+
* `readableIds` maps each anonymised id back to the declared one exactly; prefer it over the
|
|
123
|
+
* positional fallback, which assumes the executor's hits/misses/uncomputable split happens to
|
|
124
|
+
* come back in declaration order and silently swaps two condition names when it does not.
|
|
122
125
|
*/
|
|
123
|
-
export function voiceFromDecision({ result, policy, factPack, language = "English" } = {}) {
|
|
126
|
+
export function voiceFromDecision({ result, policy, factPack, readableIds, language = "English" } = {}) {
|
|
124
127
|
const copy = copyFor(language);
|
|
125
128
|
const facts = new Map((factPack?.facts || []).map((fact) => [fact.fact_id, fact]));
|
|
126
129
|
const stance = result?.common_projection?.stance || "out_of_scope";
|
|
@@ -136,7 +139,7 @@ export function voiceFromDecision({ result, policy, factPack, language = "Englis
|
|
|
136
139
|
const computed = (result?.computations?.trace || [])
|
|
137
140
|
.filter((step) => step.status === "computed" && finite(step.value))
|
|
138
141
|
.slice(0, MAX_ITEMS)
|
|
139
|
-
.map((step) => `${step.output_id} = ${Number(step.value.toFixed(4))}`)
|
|
142
|
+
.map((step) => `${readableIds?.get?.(step.output_id) || step.output_id} = ${Number(step.value.toFixed(4))}`)
|
|
140
143
|
.join("; ");
|
|
141
144
|
|
|
142
145
|
// The executor publishes the rules it evaluated already split into hits, misses and the ones
|
|
@@ -157,13 +160,18 @@ export function voiceFromDecision({ result, policy, factPack, language = "Englis
|
|
|
157
160
|
.map((rule) => rule.rule_id)
|
|
158
161
|
.filter(Boolean)
|
|
159
162
|
.map((id, index) => [id, index]));
|
|
163
|
+
const declaredName = (anon) => (anon && readableIds?.get?.(anon)) || null;
|
|
160
164
|
const ruleName = (rule) => {
|
|
161
165
|
const anon = rule.rule_id || rule.id;
|
|
162
|
-
|
|
163
|
-
return readable || anon || "rule";
|
|
166
|
+
return declaredName(anon) || readableRuleIds.get(anonOrder.get(anon)) || anon || "rule";
|
|
164
167
|
};
|
|
165
168
|
|
|
166
|
-
|
|
169
|
+
// Vetoes went unmapped, so a hashed veto reached the reader while the scoring conditions
|
|
170
|
+
// beside it were readable. That asymmetry is what made two seats speculate about their own
|
|
171
|
+
// hard veto instead of naming it.
|
|
172
|
+
const vetoes = (result?.vetoes_triggered || [])
|
|
173
|
+
.map((veto) => declaredName(veto.veto_id) || veto.veto_id)
|
|
174
|
+
.filter(Boolean);
|
|
167
175
|
|
|
168
176
|
const whatISee = [
|
|
169
177
|
readList ? copy.readFacts(readList) : copy.noFacts,
|
package/mcp/lib/weights.mjs
CHANGED
|
@@ -76,8 +76,15 @@ export function resolveSeatWeights(run, overrides = {}) {
|
|
|
76
76
|
|
|
77
77
|
const debateIds = reg.ids("debate").filter((id) => id !== "portfolio_manager");
|
|
78
78
|
const masterIds = (run.master_opinions || []).map((o) => o.master);
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
// Evidence seats belong here too. The contract says a failed verification down-weights the
|
|
80
|
+
// seat that made the claim, but the enumeration stopped at debate and master seats, so a
|
|
81
|
+
// `contradicted` verdict against an analyst was recorded and then silently ignored.
|
|
82
|
+
const evidenceIds = [
|
|
83
|
+
...(run.tasks || []),
|
|
84
|
+
...(run.verifier_verdicts || []).map((v) => v?.seat).filter(Boolean),
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
for (const id of new Set([...debateIds, ...masterIds, ...evidenceIds])) {
|
|
81
88
|
const persona = reg.get(id);
|
|
82
89
|
if (!persona) continue;
|
|
83
90
|
const declared = baseWeight(persona);
|
package/mcp/lib/xbrl-series.mjs
CHANGED
|
@@ -68,9 +68,28 @@ export const FUNDAMENTAL_CONCEPTS = Object.freeze({
|
|
|
68
68
|
},
|
|
69
69
|
// Debt buckets are non-overlapping by construction so they can be summed. `debt_combined`
|
|
70
70
|
// is the whole of it in one tag and wins outright when a filer publishes it.
|
|
71
|
+
//
|
|
72
|
+
// Within a bucket the tags are ALIASES, not addends: an earlier alias wins a year outright.
|
|
73
|
+
// The convertible aliases are therefore appended last, which makes them strictly additive --
|
|
74
|
+
// a filer that already resolves a bucket resolves it to exactly the same number as before,
|
|
75
|
+
// and only a filer whose sole balance-sheet debt line is a convertible note changes at all.
|
|
76
|
+
//
|
|
77
|
+
// That filer is not rare. ServiceNow's FY2020 and FY2025 balance sheets carry no straight
|
|
78
|
+
// debt tag whatsoever: the only debt instant either year is ConvertibleLongTermNotesPayable
|
|
79
|
+
// ($1.64bn and $1.491bn). Total debt therefore read as unknown, and because unknown debt is
|
|
80
|
+
// correctly refused rather than treated as zero, three separate facts died from one absent
|
|
81
|
+
// alias -- leverage, downside asset value, and incremental return on capital -- which in turn
|
|
82
|
+
// is why a seat requiring incremental return on capital abstained on a company whose debt is
|
|
83
|
+
// plainly stated on the face of its balance sheet.
|
|
71
84
|
debt_combined: { tags: ["DebtLongtermAndShorttermCombinedAmount"], unit: "USD" },
|
|
72
|
-
debt_long_term_noncurrent: {
|
|
73
|
-
|
|
85
|
+
debt_long_term_noncurrent: {
|
|
86
|
+
tags: ["LongTermDebtNoncurrent", "LongTermDebtAndCapitalLeaseObligations", "ConvertibleLongTermNotesPayable"],
|
|
87
|
+
unit: "USD",
|
|
88
|
+
},
|
|
89
|
+
debt_long_term_current: {
|
|
90
|
+
tags: ["LongTermDebtCurrent", "LongTermDebtAndCapitalLeaseObligationsCurrent", "ConvertibleNotesPayableCurrent"],
|
|
91
|
+
unit: "USD",
|
|
92
|
+
},
|
|
74
93
|
debt_short_term: { tags: ["ShortTermBorrowings", "OtherShortTermBorrowings", "CommercialPaper"], unit: "USD" },
|
|
75
94
|
tax_expense: { tags: ["IncomeTaxExpenseBenefit"], unit: "USD" },
|
|
76
95
|
pretax_income: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alphacouncil-agent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "Multi-agent company, ETF and market-index research workflow for Codex & Claude Code: sourced evidence, method seats, bull/bear debate and portfolio-manager synthesis.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -127,6 +127,7 @@
|
|
|
127
127
|
".opencode/command/",
|
|
128
128
|
".grok/commands/",
|
|
129
129
|
"docs/INSTALL.md",
|
|
130
|
+
"docs/examples/",
|
|
130
131
|
"docs/plans/",
|
|
131
132
|
"docs/releases/",
|
|
132
133
|
"docs/evaluation/",
|
|
@@ -55,6 +55,25 @@
|
|
|
55
55
|
四、**股权稀释**
|
|
56
56
|
在外流通股数的多年变化。持续稀释会让每股口径的所有增长打折,而这一点在讨论增长时常被忽略。
|
|
57
57
|
|
|
58
|
+
## 怎么取到原文(EDGAR 会限流,这一节决定这个席位有没有真凭据)
|
|
59
|
+
|
|
60
|
+
EDGAR 对没有描述性 User-Agent 的客户端返回 **HTTP 429**,而且 `www.sec.gov` 比 `data.sec.gov` 限得更狠。**连续换 URL 猛试只会让限流变严**——一次真实运行里连试 8 个不同 URL 全部 429,最后一份原文都没读到。
|
|
61
|
+
|
|
62
|
+
按这个顺序取,取到就停:
|
|
63
|
+
|
|
64
|
+
1. `https://data.sec.gov/submissions/CIK##########.json`(10 位补零)—— 申报索引,JSON API 最宽容,一次拿到全部表单类型、accession 与 filed date。
|
|
65
|
+
2. `https://data.sec.gov/api/xbrl/companyfacts/CIK##########.json` —— 股数、股东权益这类 XBRL 数字。
|
|
66
|
+
3. `https://www.sec.gov/Archives/edgar/data/<无前导零CIK>/<去横线accession>/<主文档>` —— 具体那一份 Form 4 / 8-K 原文。
|
|
67
|
+
4. EDGAR 全文检索与 `cgi-bin/browse-edgar` **放到最后**:它们最容易被限流,且能给的信息前三步都能给。
|
|
68
|
+
|
|
69
|
+
遇到 429:**停下来等几秒再重试同一个 URL**,不要立刻换别的 URL。同一份文档最多重试三次;三次都 429 就记为取回失败,进入下面的降级纪律。
|
|
70
|
+
|
|
71
|
+
**降级纪律(必须照做,不许含糊)**:一份原文都没读到时,
|
|
72
|
+
- `summary` 第一句就写明「本包未直读任何 EDGAR 原文,全部内容经二级来源交叉核实」;
|
|
73
|
+
- `confidence` 最高只能是 `low`;
|
|
74
|
+
- 在 `open_questions` 里逐条列出失败的 URL 与状态码;
|
|
75
|
+
- 每条来自镜像/聚合站的内容都标注它是二手,**绝不能把镜像内容写成「已按申报原文核实」**。
|
|
76
|
+
|
|
58
77
|
## 硬纪律
|
|
59
78
|
|
|
60
79
|
- **每条必须带申报链接和申报日期**(filed date,不是期间结束日)。
|
|
@@ -87,6 +106,25 @@ Key items: 1.01 material agreement, 2.02 results, 4.01 auditor change, 4.02 prio
|
|
|
87
106
|
4. **Dilution**
|
|
88
107
|
Shares outstanding across several years. Persistent dilution discounts every per-share growth figure, and that is routinely ignored in growth discussions.
|
|
89
108
|
|
|
109
|
+
## How to actually retrieve the filings (EDGAR throttles; this section decides whether this seat has primary evidence at all)
|
|
110
|
+
|
|
111
|
+
EDGAR answers **HTTP 429** to a client without a descriptive User-Agent, and `www.sec.gov` is throttled harder than `data.sec.gov`. **Rapidly trying different URLs makes the throttling worse, not better** -- on one real run eight different URLs were tried in a row, all returned 429, and not a single filing was read.
|
|
112
|
+
|
|
113
|
+
Retrieve in this order and stop as soon as you have what you need:
|
|
114
|
+
|
|
115
|
+
1. `https://data.sec.gov/submissions/CIK##########.json` (CIK zero-padded to ten digits) -- the filing index. The most tolerant endpoint, and one call returns every form type, accession number and filed date.
|
|
116
|
+
2. `https://data.sec.gov/api/xbrl/companyfacts/CIK##########.json` -- XBRL numbers such as share count and stockholders' equity.
|
|
117
|
+
3. `https://www.sec.gov/Archives/edgar/data/<CIK without leading zeros>/<accession without dashes>/<primary document>` -- the specific Form 4 or 8-K text.
|
|
118
|
+
4. EDGAR full-text search and `cgi-bin/browse-edgar` **last**: they are throttled the hardest and tell you nothing the first three cannot.
|
|
119
|
+
|
|
120
|
+
On a 429: **wait a few seconds and retry the same URL** rather than immediately switching to a different one. Retry one document at most three times; if all three are limited, record the retrieval as failed and follow the degradation rule below.
|
|
121
|
+
|
|
122
|
+
**Degradation rule (mandatory, and not to be softened)** -- when no filing was read directly:
|
|
123
|
+
- the first sentence of `summary` must state that no EDGAR original was read and that everything was cross-checked through secondary sources instead;
|
|
124
|
+
- `confidence` is capped at `low`;
|
|
125
|
+
- `open_questions` lists each failed URL with its status code;
|
|
126
|
+
- every item taken from a mirror or aggregator is labelled second-hand. **Never write mirror content as "verified against the filing".**
|
|
127
|
+
|
|
90
128
|
## Hard rules
|
|
91
129
|
|
|
92
130
|
- **Every item carries the filing link and the filed date** -- the filed date, not the period end.
|
|
@@ -501,21 +501,27 @@ function buildDocuments({ seat, blueprint, rawTools, packVersion, formulaManifes
|
|
|
501
501
|
ja: `${publicTitle.en} 単独テスト`,
|
|
502
502
|
ko: `${publicTitle.en} 단독 테스트`,
|
|
503
503
|
},
|
|
504
|
+
// The owner reviewed and accepted these method attributions, so the catalog title no
|
|
505
|
+
// longer repeats a review-status warning next to every name. The machine-verified
|
|
506
|
+
// admission level is unchanged and still published; the label is just the method's name.
|
|
504
507
|
operator_label: {
|
|
505
|
-
en:
|
|
506
|
-
zh:
|
|
507
|
-
ja: `${publicTitle.en}
|
|
508
|
-
ko: `${publicTitle.en}
|
|
508
|
+
en: publicTitle.en,
|
|
509
|
+
zh: publicTitle.zh,
|
|
510
|
+
ja: `${publicTitle.en}・メソッドレンズ`,
|
|
511
|
+
ko: `${publicTitle.en} 방법론 렌즈`,
|
|
509
512
|
},
|
|
510
513
|
maturity: "operator_lens",
|
|
511
514
|
source_cutoff: SOURCE_DATE,
|
|
512
515
|
},
|
|
513
516
|
selection: {
|
|
517
|
+
// What a seat IS, not its review history. "Not the person's own words" stays because it
|
|
518
|
+
// guards against impersonation; the review-status clause is gone because the owner signed
|
|
519
|
+
// off on these attributions and a warning repeated 27 times stops being a warning.
|
|
514
520
|
identity: {
|
|
515
|
-
en: `${publicTitle.en};
|
|
516
|
-
zh: `${publicTitle.zh}
|
|
517
|
-
ja: `${publicTitle.en}
|
|
518
|
-
ko: `${publicTitle.en}.
|
|
521
|
+
en: `${publicTitle.en}; a method model, not the person's own words`,
|
|
522
|
+
zh: `${publicTitle.zh};方法模型,非本人言论`,
|
|
523
|
+
ja: `${publicTitle.en}。メソッドモデルであり、本人の発言や現在の見解ではない。`,
|
|
524
|
+
ko: `${publicTitle.en}. 방법론 모델이며, 본인의 발언이나 현재 견해가 아니다.`,
|
|
519
525
|
},
|
|
520
526
|
method: {
|
|
521
527
|
en: seat.method_scope.planning_hypothesis,
|
|
@@ -46,8 +46,8 @@ export function usage() {
|
|
|
46
46
|
return [
|
|
47
47
|
"Usage: node scripts/review-persona-v3-solo-formulas.mjs [--check|--plan|--write] [--json]",
|
|
48
48
|
"",
|
|
49
|
-
"--plan recompute
|
|
50
|
-
"--write write
|
|
49
|
+
"--plan recompute the planned machine review records without reading the review tree",
|
|
50
|
+
"--write write every planned review plus the manifest below isolated staging",
|
|
51
51
|
"--check require exact physical files and byte-stable recomputation (default)",
|
|
52
52
|
"--formula-root override the physical solo-test formula tree",
|
|
53
53
|
"--review-root override the isolated AI review tree",
|
|
@@ -93,7 +93,9 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
93
93
|
if (args.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
94
94
|
else process.stdout.write(compact(result));
|
|
95
95
|
const okay = result.review_count === PLANNED_TOOL_COUNT
|
|
96
|
-
|
|
96
|
+
// Derived, not hardcoded: the literal 156 (26 seats x 2 tools x 3 roles) survived the
|
|
97
|
+
// twenty-seventh seat and failed a review tree its own output line printed as 162/162.
|
|
98
|
+
&& result.role_count === PLANNED_TOOL_COUNT * INDEPENDENT_ROLES_PER_REVIEW
|
|
97
99
|
&& result.mechanical_pass_count === PLANNED_TOOL_COUNT
|
|
98
100
|
&& result.disagreement_count === 0
|
|
99
101
|
&& result.semantic_unknown_count === PLANNED_TOOL_COUNT
|