@senpi/runtime 3.0.55 → 3.0.57
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/dist/cli/senpi-commands.d.ts.map +1 -1
- package/dist/cli/senpi-commands.js +2 -0
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/cli/validate-command.d.ts +29 -0
- package/dist/cli/validate-command.d.ts.map +1 -0
- package/dist/cli/validate-command.js +89 -0
- package/dist/cli/validate-command.js.map +1 -0
- package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
- package/dist/dsl/monitor/process-one-position.js +5 -1
- package/dist/dsl/monitor/process-one-position.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/load-runtime-config.d.ts +38 -0
- package/dist/runtime/load-runtime-config.d.ts.map +1 -1
- package/dist/runtime/load-runtime-config.js +33 -25
- package/dist/runtime/load-runtime-config.js.map +1 -1
- package/dist/runtime/runtime-schema.d.ts +100 -100
- package/dist/scanners/external/scaffold/__main__.py +8 -0
- package/dist/scanners/external/scaffold/ctx.py +15 -1
- package/dist/scanners/external/scaffold/http_post.py +22 -0
- package/dist/scanners/external/scaffold/import_check.py +305 -0
- package/dist/scanners/external/scaffold/preflight.py +541 -0
- package/dist/scanners/external/scaffold/scaffold.py +11 -2
- package/dist/scanners/implementations/market-regime.d.ts +1 -1
- package/dist/telemetry/metrics.d.ts +6 -0
- package/dist/telemetry/metrics.d.ts.map +1 -1
- package/dist/telemetry/metrics.js +45 -0
- package/dist/telemetry/metrics.js.map +1 -1
- package/dist/utils/event-catalog.d.ts +22 -0
- package/dist/utils/event-catalog.d.ts.map +1 -1
- package/dist/utils/event-catalog.js +6 -0
- package/dist/utils/event-catalog.js.map +1 -1
- package/dist/validate/codes.d.ts +163 -0
- package/dist/validate/codes.d.ts.map +1 -0
- package/dist/validate/codes.js +375 -0
- package/dist/validate/codes.js.map +1 -0
- package/dist/validate/coverage.d.ts +34 -0
- package/dist/validate/coverage.d.ts.map +1 -0
- package/dist/validate/coverage.js +34 -0
- package/dist/validate/coverage.js.map +1 -0
- package/dist/validate/import-stage.d.ts +56 -0
- package/dist/validate/import-stage.d.ts.map +1 -0
- package/dist/validate/import-stage.js +238 -0
- package/dist/validate/import-stage.js.map +1 -0
- package/dist/validate/index.d.ts +28 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +27 -0
- package/dist/validate/index.js.map +1 -0
- package/dist/validate/install-gate.d.ts +44 -0
- package/dist/validate/install-gate.d.ts.map +1 -0
- package/dist/validate/install-gate.js +66 -0
- package/dist/validate/install-gate.js.map +1 -0
- package/dist/validate/live-stage.d.ts +108 -0
- package/dist/validate/live-stage.d.ts.map +1 -0
- package/dist/validate/live-stage.js +416 -0
- package/dist/validate/live-stage.js.map +1 -0
- package/dist/validate/proof.d.ts +104 -0
- package/dist/validate/proof.d.ts.map +1 -0
- package/dist/validate/proof.js +256 -0
- package/dist/validate/proof.js.map +1 -0
- package/dist/validate/recipe-checks.d.ts +79 -0
- package/dist/validate/recipe-checks.d.ts.map +1 -0
- package/dist/validate/recipe-checks.js +202 -0
- package/dist/validate/recipe-checks.js.map +1 -0
- package/dist/validate/recipe.d.ts +41 -0
- package/dist/validate/recipe.d.ts.map +1 -0
- package/dist/validate/recipe.js +215 -0
- package/dist/validate/recipe.js.map +1 -0
- package/dist/validate/report.d.ts +35 -0
- package/dist/validate/report.d.ts.map +1 -0
- package/dist/validate/report.js +78 -0
- package/dist/validate/report.js.map +1 -0
- package/dist/validate/run.d.ts +101 -0
- package/dist/validate/run.d.ts.map +1 -0
- package/dist/validate/run.js +444 -0
- package/dist/validate/run.js.map +1 -0
- package/dist/validate/signal-stage.d.ts +40 -0
- package/dist/validate/signal-stage.d.ts.map +1 -0
- package/dist/validate/signal-stage.js +116 -0
- package/dist/validate/signal-stage.js.map +1 -0
- package/dist/validate/target.d.ts +70 -0
- package/dist/validate/target.d.ts.map +1 -0
- package/dist/validate/target.js +231 -0
- package/dist/validate/target.js.map +1 -0
- package/dist/validate/telemetry.d.ts +24 -0
- package/dist/validate/telemetry.d.ts.map +1 -0
- package/dist/validate/telemetry.js +60 -0
- package/dist/validate/telemetry.js.map +1 -0
- package/dist/validate/types.d.ts +115 -0
- package/dist/validate/types.d.ts.map +1 -0
- package/dist/validate/types.js +31 -0
- package/dist/validate/types.js.map +1 -0
- package/dist/validate/wallet.d.ts +63 -0
- package/dist/validate/wallet.d.ts.map +1 -0
- package/dist/validate/wallet.js +66 -0
- package/dist/validate/wallet.js.map +1 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The proof of a passing run, and the function that checks it.
|
|
3
|
+
*
|
|
4
|
+
* A proof is a claim about *content*: this exact recipe and these exact scanner files passed a
|
|
5
|
+
* full, unscoped, live validation. It is content-addressed — hash the authored bytes, nothing
|
|
6
|
+
* else — so a copied package is still proven (same content, same claim) and a one-character edit
|
|
7
|
+
* is not. There is no expiry: content that has not changed does not become less proven by sitting
|
|
8
|
+
* still, and a TTL would only train people to re-run validation ritually rather than when it
|
|
9
|
+
* means something.
|
|
10
|
+
*
|
|
11
|
+
* `verifyProof` is deliberately pure observation: it reports, it never enforces and never throws.
|
|
12
|
+
* The deploy verb owns the decision; this module owns the fact. It also never parses the recipe —
|
|
13
|
+
* the record carries the file list and the scanner roots, so verification is a re-hash and a
|
|
14
|
+
* comparison. A recipe that no longer parses necessarily hashes differently than the one that
|
|
15
|
+
* passed, which is the answer, not an error.
|
|
16
|
+
*
|
|
17
|
+
* Per-file hashes are stored so a refusal can say *what* changed. "The package differs" sends an
|
|
18
|
+
* agent diffing blindly; "scan.py changed, scoring.py is new" is an answer.
|
|
19
|
+
*/
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
import { readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
23
|
+
import { recordProofVerification } from "../telemetry/metrics.js";
|
|
24
|
+
import { VERSION } from "../version.js";
|
|
25
|
+
import { VALIDATE_CODE } from "./codes.js";
|
|
26
|
+
/** Lives at the package root, beside the recipe. Excluded from its own fingerprint. */
|
|
27
|
+
export const PROOF_FILENAME = ".senpi-proof.json";
|
|
28
|
+
/**
|
|
29
|
+
* Not authored content. Interpreter caches and OS litter appear and vanish without anyone editing
|
|
30
|
+
* anything; hashing them would invalidate proofs for reasons no diff can show.
|
|
31
|
+
*/
|
|
32
|
+
const EXCLUDED = new Set([PROOF_FILENAME, ".DS_Store"]);
|
|
33
|
+
const EXCLUDED_DIRS = new Set(["__pycache__", ".git"]);
|
|
34
|
+
const EXCLUDED_SUFFIXES = [".pyc"];
|
|
35
|
+
function sha256(data) {
|
|
36
|
+
return createHash("sha256").update(data).digest("hex");
|
|
37
|
+
}
|
|
38
|
+
function excluded(name) {
|
|
39
|
+
return EXCLUDED.has(name) || EXCLUDED_SUFFIXES.some((s) => name.endsWith(s));
|
|
40
|
+
}
|
|
41
|
+
/** Every authored file under a root, relative to the package root. Deterministic order. */
|
|
42
|
+
function walk(root, packageRoot, into) {
|
|
43
|
+
let entries;
|
|
44
|
+
try {
|
|
45
|
+
entries = readdirSync(root);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return; // a root that vanished shows up as its files being absent, which is the honest answer
|
|
49
|
+
}
|
|
50
|
+
for (const name of entries.sort()) {
|
|
51
|
+
if (excluded(name))
|
|
52
|
+
continue;
|
|
53
|
+
const full = join(root, name);
|
|
54
|
+
let stat;
|
|
55
|
+
try {
|
|
56
|
+
stat = statSync(full);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (stat.isDirectory()) {
|
|
62
|
+
if (!EXCLUDED_DIRS.has(name))
|
|
63
|
+
walk(full, packageRoot, into);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
into.set(relative(packageRoot, full), sha256(readFileSync(full)));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Hash the authored content: the recipe as written, every scanner file verbatim. */
|
|
71
|
+
export function fingerprintPackage(recipePath, scannerRoots) {
|
|
72
|
+
const packageRoot = dirname(recipePath);
|
|
73
|
+
const map = new Map();
|
|
74
|
+
try {
|
|
75
|
+
map.set(relative(packageRoot, recipePath), sha256(readFileSync(recipePath)));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// An unreadable recipe simply is not in the map; comparison reports it as changed/removed.
|
|
79
|
+
}
|
|
80
|
+
for (const root of scannerRoots)
|
|
81
|
+
walk(resolve(packageRoot, root), packageRoot, map);
|
|
82
|
+
const sorted = [...map.entries()].sort(([a], [b]) => (a < b ? -1 : 1));
|
|
83
|
+
return {
|
|
84
|
+
fingerprint: sha256(JSON.stringify(sorted)),
|
|
85
|
+
files: Object.fromEntries(sorted),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Record a passing run. Atomic — a crash mid-write must not leave a half-proof that
|
|
90
|
+
* `verifyProof` would read as corrupt-and-therefore-absent *after* a genuine pass.
|
|
91
|
+
*
|
|
92
|
+
* Returns the finding to attach when the write fails: the run still passed — the package is no
|
|
93
|
+
* less runnable because a disk was full — but the deploy gate will not find a proof, and whoever
|
|
94
|
+
* operates the environment is the one who can do something about that. Post-verdict by design:
|
|
95
|
+
* it must not flip a pass, even under --strict.
|
|
96
|
+
*/
|
|
97
|
+
export function writeProof(inputs) {
|
|
98
|
+
const packageRoot = dirname(inputs.recipePath);
|
|
99
|
+
const { fingerprint, files } = fingerprintPackage(inputs.recipePath, inputs.scannerRoots);
|
|
100
|
+
const record = {
|
|
101
|
+
record_version: 1,
|
|
102
|
+
fingerprint,
|
|
103
|
+
files,
|
|
104
|
+
recipe: relative(packageRoot, inputs.recipePath),
|
|
105
|
+
scanner_roots: inputs.scannerRoots,
|
|
106
|
+
runtime_version: VERSION,
|
|
107
|
+
verdict: "PASS",
|
|
108
|
+
...(inputs.wallet
|
|
109
|
+
? { wallet: { address: inputs.wallet.address, source: inputs.wallet.source, book: inputs.wallet.book } }
|
|
110
|
+
: {}),
|
|
111
|
+
observations: inputs.observations,
|
|
112
|
+
created_at: new Date().toISOString(),
|
|
113
|
+
};
|
|
114
|
+
const path = join(packageRoot, PROOF_FILENAME);
|
|
115
|
+
try {
|
|
116
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
117
|
+
writeFileSync(tmp, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
118
|
+
renameSync(tmp, path);
|
|
119
|
+
return { recorded: true };
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
return {
|
|
123
|
+
recorded: false,
|
|
124
|
+
finding: {
|
|
125
|
+
code: VALIDATE_CODE.proofNotRecorded,
|
|
126
|
+
severity: "warning",
|
|
127
|
+
stage: "proof",
|
|
128
|
+
actor: "ops",
|
|
129
|
+
fix_kind: "command",
|
|
130
|
+
what: `The run passed but its proof could not be written to ${path}`,
|
|
131
|
+
why: "The package is no less runnable — but the deploy gate reads the proof, not this output, so without the record the pass cannot be acted on.",
|
|
132
|
+
fix: `Make ${packageRoot} writable and run the validation again.`,
|
|
133
|
+
evidence: { error: String(err) },
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function readRecord(packageRoot) {
|
|
139
|
+
try {
|
|
140
|
+
const parsed = JSON.parse(readFileSync(join(packageRoot, PROOF_FILENAME), "utf8"));
|
|
141
|
+
// A record this code cannot vouch for is treated as absent, not as an error: the remedy is
|
|
142
|
+
// identical (run validation), and failing loudly on version 2 would make old runtimes refuse
|
|
143
|
+
// packages that newer ones proved.
|
|
144
|
+
if (parsed.record_version !== 1 || typeof parsed.fingerprint !== "string" || typeof parsed.files !== "object") {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
return parsed;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return undefined; // absent and corrupt are the same fact: there is no proof here
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function refusal(reason, parts) {
|
|
154
|
+
const code = reason === "no_proof"
|
|
155
|
+
? VALIDATE_CODE.noProof
|
|
156
|
+
: reason === "content_changed"
|
|
157
|
+
? VALIDATE_CODE.contentChanged
|
|
158
|
+
: VALIDATE_CODE.runtimeVersionChanged;
|
|
159
|
+
return { code, severity: "error", stage: "proof", actor: "agent", fix_kind: "command", ...parts };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Is this package's content the content that passed?
|
|
163
|
+
*
|
|
164
|
+
* Pure observation for the deploy verb to act on. Every failure reason has the same remedy —
|
|
165
|
+
* `senpi validate <packageDir>` — stated on the finding, so the caller can surface it unmodified
|
|
166
|
+
* and an agent can close the loop without knowing anything about proofs.
|
|
167
|
+
*/
|
|
168
|
+
export function verifyProof(packageDir, options = {}) {
|
|
169
|
+
const instances = options.instances ?? [];
|
|
170
|
+
const result = instances.length === 0
|
|
171
|
+
? verifyProofInner(packageDir)
|
|
172
|
+
: verifyInstances(packageDir, instances);
|
|
173
|
+
recordProofVerification(result.ok ? "ok" : result.reason);
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Every named instance must be proven, and the first that is not decides the answer.
|
|
178
|
+
*
|
|
179
|
+
* Reported as one verdict rather than a list, because the caller's decision is binary and the
|
|
180
|
+
* remedy — revalidate the package — is the same whichever instance failed. The failing instance is
|
|
181
|
+
* named so the report still points somewhere.
|
|
182
|
+
*/
|
|
183
|
+
function verifyInstances(packageDir, instances) {
|
|
184
|
+
const packageRoot = resolve(packageDir);
|
|
185
|
+
for (const instance of instances) {
|
|
186
|
+
const result = verifyProofInner(join(packageRoot, instance));
|
|
187
|
+
if (!result.ok) {
|
|
188
|
+
const finding = result.finding;
|
|
189
|
+
return {
|
|
190
|
+
...result,
|
|
191
|
+
changedFiles: result.changedFiles?.map((f) => join(instance, f)),
|
|
192
|
+
finding: {
|
|
193
|
+
...finding,
|
|
194
|
+
what: `Instance '${instance}': ${finding.what}`,
|
|
195
|
+
fix: `Run: senpi validate ${join(packageRoot, instance)}`,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { ok: true };
|
|
201
|
+
}
|
|
202
|
+
function verifyProofInner(packageDir) {
|
|
203
|
+
const packageRoot = resolve(packageDir);
|
|
204
|
+
const record = readRecord(packageRoot);
|
|
205
|
+
if (record === undefined) {
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
reason: "no_proof",
|
|
209
|
+
finding: refusal("no_proof", {
|
|
210
|
+
what: `No proof of a passing validation exists for ${packageRoot}`,
|
|
211
|
+
why: "Funding requires evidence the strategy runs. Only a full, unscoped `senpi validate` that passes writes that evidence — a scoped or shallow run deliberately does not.",
|
|
212
|
+
fix: `Run: senpi validate ${packageRoot}`,
|
|
213
|
+
}),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const current = fingerprintPackage(join(packageRoot, record.recipe), record.scanner_roots);
|
|
217
|
+
if (current.fingerprint !== record.fingerprint) {
|
|
218
|
+
const changed = new Set();
|
|
219
|
+
for (const [file, hash] of Object.entries(record.files)) {
|
|
220
|
+
if (current.files[file] !== hash)
|
|
221
|
+
changed.add(file); // edited or removed
|
|
222
|
+
}
|
|
223
|
+
for (const file of Object.keys(current.files)) {
|
|
224
|
+
if (!(file in record.files))
|
|
225
|
+
changed.add(file); // added — new code is unproven code
|
|
226
|
+
}
|
|
227
|
+
const changedFiles = [...changed].sort();
|
|
228
|
+
return {
|
|
229
|
+
ok: false,
|
|
230
|
+
reason: "content_changed",
|
|
231
|
+
changedFiles,
|
|
232
|
+
record,
|
|
233
|
+
finding: refusal("content_changed", {
|
|
234
|
+
what: `The package differs from the content that passed: ${changedFiles.slice(0, 5).join(", ")}${changedFiles.length > 5 ? ` and ${changedFiles.length - 5} more` : ""}`,
|
|
235
|
+
why: "The proof is a claim about exact content. These files are not the bytes that were validated, so nothing is known about how they behave.",
|
|
236
|
+
fix: `Run: senpi validate ${packageRoot}`,
|
|
237
|
+
evidence: { changed_files: changedFiles, proved_at: record.created_at },
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (record.runtime_version !== VERSION) {
|
|
242
|
+
return {
|
|
243
|
+
ok: false,
|
|
244
|
+
reason: "runtime_version_changed",
|
|
245
|
+
record,
|
|
246
|
+
finding: refusal("runtime_version_changed", {
|
|
247
|
+
what: `The proof was recorded under runtime ${record.runtime_version}; this is ${VERSION}`,
|
|
248
|
+
why: "A pass is a claim about the package running under a specific runtime. The runtime changed, so the claim no longer covers what would actually execute.",
|
|
249
|
+
fix: `Run: senpi validate ${packageRoot}`,
|
|
250
|
+
evidence: { proved_under: record.runtime_version, current: VERSION },
|
|
251
|
+
}),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return { ok: true, record };
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=proof.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proof.js","sourceRoot":"","sources":["../../src/validate/proof.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic checks on a parsed recipe — the things a schema cannot express.
|
|
3
|
+
*
|
|
4
|
+
* Each is a pure function over the parsed config, exported individually so the same check can run
|
|
5
|
+
* from `senpi validate` and, later, from the install and boot paths. One implementation, several
|
|
6
|
+
* callers, rather than a validation copy that drifts from the enforcement copy.
|
|
7
|
+
*
|
|
8
|
+
* ## Every check here was measured against the fleet before it was written
|
|
9
|
+
*
|
|
10
|
+
* A check that refuses a working strategy is worse than no check: it trains people to bypass the
|
|
11
|
+
* gate. Each of these was run against all 125 production instances first, and each fires on **none**
|
|
12
|
+
* of them. Two were narrowed as a direct result — see `findUnsizedStrategy` and
|
|
13
|
+
* `findMarginPctFraction`, where the obvious version of the check would have been wrong.
|
|
14
|
+
*/
|
|
15
|
+
import type { RuntimeConfig } from "../runtime/runtime-schema.js";
|
|
16
|
+
import type { Finding } from "./types.js";
|
|
17
|
+
/**
|
|
18
|
+
* An entry action for which **no margin source can ever resolve**.
|
|
19
|
+
*
|
|
20
|
+
* The runtime resolves margin in priority order: a per-signal `marginPct`, then config
|
|
21
|
+
* `margin_pct`, then `margin_per_slot`, then budget ÷ slots. If none produces a value the signal is
|
|
22
|
+
* skipped as `no_margin_configured` — the failure class behind thousands of declined signals.
|
|
23
|
+
*
|
|
24
|
+
* ## Why this is narrower than "no configured margin"
|
|
25
|
+
*
|
|
26
|
+
* **49% of production instances (61 of 125) declare no config margin at all.** They size from a
|
|
27
|
+
* per-signal `marginPct` their scanner emits, with config margin as an optional fallback — that is
|
|
28
|
+
* the fleet norm, not an oversight. Refusing them would have made this command reject half of a
|
|
29
|
+
* working fleet on its first day.
|
|
30
|
+
*
|
|
31
|
+
* So this fires only when *nothing could supply a margin*: no config source, no external scanner
|
|
32
|
+
* that could emit one, and no model-mode decision that could override. That condition is provable
|
|
33
|
+
* from the recipe alone, and it matches none of the 125.
|
|
34
|
+
*
|
|
35
|
+
* The weaker case — config margin absent and the scanner simply never emits one — is not knowable
|
|
36
|
+
* statically. It is caught at the live stage, where the signals a tick actually emits can be read.
|
|
37
|
+
*/
|
|
38
|
+
export declare function findUnsizedStrategy(config: RuntimeConfig): Finding[];
|
|
39
|
+
/**
|
|
40
|
+
* An entry action with no risk guardrails.
|
|
41
|
+
*
|
|
42
|
+
* With no guardrail block the risk gate evaluates fully open: no daily loss limit, no drawdown
|
|
43
|
+
* ceiling, no trade cap. Every one of the 125 production instances declares one, so requiring it
|
|
44
|
+
* matches practice rather than imposing a new rule.
|
|
45
|
+
*/
|
|
46
|
+
export declare function findUngatedStrategy(config: RuntimeConfig): Finding[];
|
|
47
|
+
/**
|
|
48
|
+
* `strategy.margin_pct` given as a fraction where a percent is required.
|
|
49
|
+
*
|
|
50
|
+
* `margin_pct: 0.20` is accepted by the schema — it is positive and under 100 — and sizes **0.2%**
|
|
51
|
+
* of withdrawable rather than 20%. A hundred times too small, with no error at any point.
|
|
52
|
+
*
|
|
53
|
+
* ## Scope: only what the runtime reads as a percent
|
|
54
|
+
*
|
|
55
|
+
* Deliberately not a walk of the whole document. Scanner `inputs` are private tunables in whatever
|
|
56
|
+
* units their scanner defines, and at least one production strategy carries fractional tier values
|
|
57
|
+
* there on purpose, converting them before emitting. Flagging those would accuse a correct strategy
|
|
58
|
+
* of a bug — so this reads only the field the runtime itself interprets as a percent.
|
|
59
|
+
*
|
|
60
|
+
* An emitted per-signal value is checked separately at the live stage, where the real value is
|
|
61
|
+
* visible rather than inferred.
|
|
62
|
+
*/
|
|
63
|
+
export declare function findMarginPctFraction(config: RuntimeConfig): Finding[];
|
|
64
|
+
/** A model-mode decision action with no model configured — the decision step cannot run. */
|
|
65
|
+
export declare function findLlmActionsWithoutModel(config: RuntimeConfig): Finding[];
|
|
66
|
+
/**
|
|
67
|
+
* A declared entrypoint that is not on disk.
|
|
68
|
+
*
|
|
69
|
+
* Needs the resolved scanner root, so it takes what the target resolver worked out rather than
|
|
70
|
+
* re-deriving it. Without this the failure surfaces only when the scanner process starts and dies.
|
|
71
|
+
*/
|
|
72
|
+
export declare function findMissingEntrypoints(config: RuntimeConfig, resolveScannerRoot: (scannerPath: string) => {
|
|
73
|
+
ok: true;
|
|
74
|
+
root: string;
|
|
75
|
+
} | {
|
|
76
|
+
ok: false;
|
|
77
|
+
reason: string;
|
|
78
|
+
}, exists: (path: string) => boolean, join: (...parts: string[]) => string): Finding[];
|
|
79
|
+
//# sourceMappingURL=recipe-checks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipe-checks.d.ts","sourceRoot":"","sources":["../../src/validate/recipe-checks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAElE,OAAO,KAAK,EAAE,OAAO,EAAW,MAAM,YAAY,CAAC;AA0BnD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,EAAE,CAkCpE;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,EAAE,CAoBpE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,EAAE,CAqBtE;AAED,4FAA4F;AAC5F,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,EAAE,CAc3E;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,aAAa,EACrB,kBAAkB,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACvG,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,EACjC,IAAI,EAAE,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,KAAK,MAAM,GACnC,OAAO,EAAE,CAsCX"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic checks on a parsed recipe — the things a schema cannot express.
|
|
3
|
+
*
|
|
4
|
+
* Each is a pure function over the parsed config, exported individually so the same check can run
|
|
5
|
+
* from `senpi validate` and, later, from the install and boot paths. One implementation, several
|
|
6
|
+
* callers, rather than a validation copy that drifts from the enforcement copy.
|
|
7
|
+
*
|
|
8
|
+
* ## Every check here was measured against the fleet before it was written
|
|
9
|
+
*
|
|
10
|
+
* A check that refuses a working strategy is worse than no check: it trains people to bypass the
|
|
11
|
+
* gate. Each of these was run against all 125 production instances first, and each fires on **none**
|
|
12
|
+
* of them. Two were narrowed as a direct result — see `findUnsizedStrategy` and
|
|
13
|
+
* `findMarginPctFraction`, where the obvious version of the check would have been wrong.
|
|
14
|
+
*/
|
|
15
|
+
import { basename } from "node:path";
|
|
16
|
+
import { VALIDATE_CODE } from "./codes.js";
|
|
17
|
+
const OPEN_POSITION = "OPEN_POSITION";
|
|
18
|
+
function staticFinding(code, fixKind, parts) {
|
|
19
|
+
return { code, severity: "error", stage: "static", actor: "agent", fix_kind: fixKind, ...parts };
|
|
20
|
+
}
|
|
21
|
+
/** Actions that can open a position — the only ones for which sizing and guardrails matter. */
|
|
22
|
+
function entryActions(config) {
|
|
23
|
+
return (config.actions ?? [])
|
|
24
|
+
.map((action, index) => ({ action: action, index }))
|
|
25
|
+
.filter(({ action }) => action.action_type === OPEN_POSITION);
|
|
26
|
+
}
|
|
27
|
+
/** Read a possibly-absent numeric field without asserting it exists on the schema type. */
|
|
28
|
+
function numberField(source, key) {
|
|
29
|
+
if (source === null || typeof source !== "object")
|
|
30
|
+
return undefined;
|
|
31
|
+
const value = source[key];
|
|
32
|
+
return typeof value === "number" ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* An entry action for which **no margin source can ever resolve**.
|
|
36
|
+
*
|
|
37
|
+
* The runtime resolves margin in priority order: a per-signal `marginPct`, then config
|
|
38
|
+
* `margin_pct`, then `margin_per_slot`, then budget ÷ slots. If none produces a value the signal is
|
|
39
|
+
* skipped as `no_margin_configured` — the failure class behind thousands of declined signals.
|
|
40
|
+
*
|
|
41
|
+
* ## Why this is narrower than "no configured margin"
|
|
42
|
+
*
|
|
43
|
+
* **49% of production instances (61 of 125) declare no config margin at all.** They size from a
|
|
44
|
+
* per-signal `marginPct` their scanner emits, with config margin as an optional fallback — that is
|
|
45
|
+
* the fleet norm, not an oversight. Refusing them would have made this command reject half of a
|
|
46
|
+
* working fleet on its first day.
|
|
47
|
+
*
|
|
48
|
+
* So this fires only when *nothing could supply a margin*: no config source, no external scanner
|
|
49
|
+
* that could emit one, and no model-mode decision that could override. That condition is provable
|
|
50
|
+
* from the recipe alone, and it matches none of the 125.
|
|
51
|
+
*
|
|
52
|
+
* The weaker case — config margin absent and the scanner simply never emits one — is not knowable
|
|
53
|
+
* statically. It is caught at the live stage, where the signals a tick actually emits can be read.
|
|
54
|
+
*/
|
|
55
|
+
export function findUnsizedStrategy(config) {
|
|
56
|
+
const entries = entryActions(config);
|
|
57
|
+
if (entries.length === 0)
|
|
58
|
+
return [];
|
|
59
|
+
const strategy = config.strategy;
|
|
60
|
+
const hasConfigMargin = numberField(strategy, "margin_pct") !== undefined ||
|
|
61
|
+
numberField(strategy, "margin_per_slot") !== undefined ||
|
|
62
|
+
numberField(strategy, "budget") !== undefined;
|
|
63
|
+
if (hasConfigMargin)
|
|
64
|
+
return [];
|
|
65
|
+
const scanners = config.scanners ?? [];
|
|
66
|
+
const hasExternalScanner = scanners.some((s) => s.type === "external_scanner");
|
|
67
|
+
const hasModelDecision = (config.actions ?? []).some((a) => a.decision_mode === "llm");
|
|
68
|
+
if (hasExternalScanner || hasModelDecision)
|
|
69
|
+
return [];
|
|
70
|
+
const { action, index } = entries[0];
|
|
71
|
+
return [
|
|
72
|
+
staticFinding(VALIDATE_CODE.unsized, "edit", {
|
|
73
|
+
what: `Action '${action.name}' opens positions, but nothing in this recipe can determine how large they should be`,
|
|
74
|
+
why: "The runtime sizes from a per-signal margin, then config margin_pct, then margin_per_slot. " +
|
|
75
|
+
"This recipe declares none of them, and has no external scanner or model-mode decision that " +
|
|
76
|
+
"could supply one per signal — so every signal is skipped as no_margin_configured. The " +
|
|
77
|
+
"strategy runs, reports healthy, and never trades.",
|
|
78
|
+
fix: "Set one under `strategy:` — `margin_pct: <n>` (percent of withdrawable, 0-100) " +
|
|
79
|
+
"or `margin_per_slot: <usd>`.",
|
|
80
|
+
where: { yaml_path: "strategy" },
|
|
81
|
+
evidence: { action: action.name, action_index: index },
|
|
82
|
+
}),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* An entry action with no risk guardrails.
|
|
87
|
+
*
|
|
88
|
+
* With no guardrail block the risk gate evaluates fully open: no daily loss limit, no drawdown
|
|
89
|
+
* ceiling, no trade cap. Every one of the 125 production instances declares one, so requiring it
|
|
90
|
+
* matches practice rather than imposing a new rule.
|
|
91
|
+
*/
|
|
92
|
+
export function findUngatedStrategy(config) {
|
|
93
|
+
const entries = entryActions(config);
|
|
94
|
+
if (entries.length === 0)
|
|
95
|
+
return [];
|
|
96
|
+
const guardRails = config.risk?.guard_rails;
|
|
97
|
+
if (guardRails !== undefined && guardRails !== null)
|
|
98
|
+
return [];
|
|
99
|
+
const { action } = entries[0];
|
|
100
|
+
return [
|
|
101
|
+
staticFinding(VALIDATE_CODE.ungated, "edit", {
|
|
102
|
+
what: `Action '${action.name}' opens positions with no risk guardrails configured`,
|
|
103
|
+
why: "With no guardrail block the risk gate evaluates fully open — no daily loss limit, no " +
|
|
104
|
+
"drawdown ceiling, no trade cap. Nothing stops a losing run except the exit engine on each " +
|
|
105
|
+
"individual position.",
|
|
106
|
+
fix: "Add a `risk.guard_rails:` block setting the limits this strategy should stop at.",
|
|
107
|
+
where: { yaml_path: "risk" },
|
|
108
|
+
evidence: { action: action.name },
|
|
109
|
+
}),
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* `strategy.margin_pct` given as a fraction where a percent is required.
|
|
114
|
+
*
|
|
115
|
+
* `margin_pct: 0.20` is accepted by the schema — it is positive and under 100 — and sizes **0.2%**
|
|
116
|
+
* of withdrawable rather than 20%. A hundred times too small, with no error at any point.
|
|
117
|
+
*
|
|
118
|
+
* ## Scope: only what the runtime reads as a percent
|
|
119
|
+
*
|
|
120
|
+
* Deliberately not a walk of the whole document. Scanner `inputs` are private tunables in whatever
|
|
121
|
+
* units their scanner defines, and at least one production strategy carries fractional tier values
|
|
122
|
+
* there on purpose, converting them before emitting. Flagging those would accuse a correct strategy
|
|
123
|
+
* of a bug — so this reads only the field the runtime itself interprets as a percent.
|
|
124
|
+
*
|
|
125
|
+
* An emitted per-signal value is checked separately at the live stage, where the real value is
|
|
126
|
+
* visible rather than inferred.
|
|
127
|
+
*/
|
|
128
|
+
export function findMarginPctFraction(config) {
|
|
129
|
+
const marginPct = numberField(config.strategy, "margin_pct");
|
|
130
|
+
// Strictly below 1. Exactly 1 is ambiguous — it reads as either a fraction meaning 100% or a
|
|
131
|
+
// plain 1%, and 1% is an ordinary size for a many-slot strategy. Refusing it would block a
|
|
132
|
+
// working recipe to catch a mistake whose worst outcome is positions that open too small and are
|
|
133
|
+
// immediately visible. The asymmetry favours letting it through.
|
|
134
|
+
if (marginPct === undefined || marginPct >= 1)
|
|
135
|
+
return [];
|
|
136
|
+
const asPercent = marginPct * 100;
|
|
137
|
+
return [
|
|
138
|
+
staticFinding(VALIDATE_CODE.marginPctFraction, "edit", {
|
|
139
|
+
what: `strategy.margin_pct is ${marginPct}, which the runtime reads as ${marginPct}% of withdrawable`,
|
|
140
|
+
why: `This field is a PERCENT in (0,100], not a fraction — the runtime sizes ` +
|
|
141
|
+
`(margin_pct / 100) × withdrawable. A value of ${marginPct} sizes every position a hundred ` +
|
|
142
|
+
`times smaller than ${asPercent}%, and nothing errors: the positions simply open tiny.`,
|
|
143
|
+
fix: `Write ${asPercent} if you meant ${asPercent}%. Keep ${marginPct} only if you genuinely want ${marginPct}% of withdrawable.`,
|
|
144
|
+
where: { yaml_path: "strategy.margin_pct" },
|
|
145
|
+
evidence: { declared: marginPct, interpreted_as_percent: marginPct, likely_intended: asPercent },
|
|
146
|
+
}),
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
/** A model-mode decision action with no model configured — the decision step cannot run. */
|
|
150
|
+
export function findLlmActionsWithoutModel(config) {
|
|
151
|
+
return (config.actions ?? [])
|
|
152
|
+
.map((action, index) => ({ action: action, index }))
|
|
153
|
+
.filter(({ action }) => action.decision_mode === "llm")
|
|
154
|
+
.filter(({ action }) => !action.decision_model || action.decision_model.trim() === "")
|
|
155
|
+
.map(({ action, index }) => staticFinding(VALIDATE_CODE.llmNoModel, "edit", {
|
|
156
|
+
what: `Action '${action.name}' is in model decision mode but names no model`,
|
|
157
|
+
why: "The decision step has nothing to call, so the action cannot evaluate a signal.",
|
|
158
|
+
fix: `Set \`decision_model:\` on action '${action.name}', or change \`decision_mode\` to \`rule\` if the scanner already applies every filter.`,
|
|
159
|
+
where: { yaml_path: `actions.${index}` },
|
|
160
|
+
evidence: { action: action.name, action_index: index },
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* A declared entrypoint that is not on disk.
|
|
165
|
+
*
|
|
166
|
+
* Needs the resolved scanner root, so it takes what the target resolver worked out rather than
|
|
167
|
+
* re-deriving it. Without this the failure surfaces only when the scanner process starts and dies.
|
|
168
|
+
*/
|
|
169
|
+
export function findMissingEntrypoints(config, resolveScannerRoot, exists, join) {
|
|
170
|
+
const findings = [];
|
|
171
|
+
(config.scanners ?? []).forEach((scanner, index) => {
|
|
172
|
+
if (scanner.type !== "external_scanner")
|
|
173
|
+
return;
|
|
174
|
+
const declaredPath = scanner.path;
|
|
175
|
+
const entrypoint = scanner.entrypoint;
|
|
176
|
+
if (!declaredPath || !entrypoint)
|
|
177
|
+
return; // absence is a schema concern, not this check's
|
|
178
|
+
const resolved = resolveScannerRoot(declaredPath);
|
|
179
|
+
if (!resolved.ok) {
|
|
180
|
+
findings.push(staticFinding(VALIDATE_CODE.unresolvableScannerPath, "edit", {
|
|
181
|
+
what: `Scanner '${scanner.name}' declares path '${declaredPath}', which cannot be resolved`,
|
|
182
|
+
why: `${resolved.reason} An unresolved relative path is interpreted against whatever directory the runtime happens to be started from, which is rarely the one you meant.`,
|
|
183
|
+
fix: "Make the path absolute, or validate from a target that has a directory to resolve against.",
|
|
184
|
+
where: { yaml_path: `scanners.${index}.path` },
|
|
185
|
+
evidence: { declared_path: declaredPath, scanner: scanner.name },
|
|
186
|
+
}));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const expected = join(resolved.root, entrypoint);
|
|
190
|
+
if (exists(expected))
|
|
191
|
+
return;
|
|
192
|
+
findings.push(staticFinding(VALIDATE_CODE.entrypointMissing, "edit", {
|
|
193
|
+
what: `Scanner '${scanner.name}' declares entrypoint '${entrypoint}', which is not at ${expected}`,
|
|
194
|
+
why: "The runtime imports this file by name when the scanner starts. If it is missing the scanner dies on launch and never produces a signal.",
|
|
195
|
+
fix: `Create ${basename(expected)} in ${resolved.root}, or correct \`entrypoint\` to name the file that is actually there.`,
|
|
196
|
+
where: { yaml_path: `scanners.${index}.entrypoint` },
|
|
197
|
+
evidence: { expected_path: expected, scanner: scanner.name, declared_entrypoint: entrypoint },
|
|
198
|
+
}));
|
|
199
|
+
});
|
|
200
|
+
return findings;
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=recipe-checks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipe-checks.js","sourceRoot":"","sources":["../../src/validate/recipe-checks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The static depth: read a recipe and report what is wrong with it.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the loading path the runtime itself uses — same parser, same schema, same hint table, same
|
|
5
|
+
* semantic asserts — but collects findings instead of throwing at the first one. The throwing loader
|
|
6
|
+
* is untouched and remains the runtime's entry point; this is a second reading of the same rules for
|
|
7
|
+
* a different audience.
|
|
8
|
+
*
|
|
9
|
+
* ## Ordering: fail fast at dependency boundaries, report together within one
|
|
10
|
+
*
|
|
11
|
+
* A config that will not parse cannot be schema-checked, and one that fails the schema cannot be
|
|
12
|
+
* semantically checked — running those anyway produces findings derived from a shape that does not
|
|
13
|
+
* exist. So each boundary stops the pass.
|
|
14
|
+
*
|
|
15
|
+
* Within a boundary the opposite applies: schema issues are independent of one another, as are the
|
|
16
|
+
* semantic checks, so all of them are reported at once. An agent fixing one thing per round trip is
|
|
17
|
+
* an agent making N round trips for information we already had.
|
|
18
|
+
*/
|
|
19
|
+
import { type RuntimeConfig } from "../runtime/runtime-schema.js";
|
|
20
|
+
import type { Finding } from "./types.js";
|
|
21
|
+
export interface RecipeSource {
|
|
22
|
+
/** Absolute path to read, when the recipe is on disk. */
|
|
23
|
+
path?: string;
|
|
24
|
+
/** Recipe text, when supplied directly. */
|
|
25
|
+
content?: string;
|
|
26
|
+
/** Absolute directory relative scanner paths resolve against, when there is one. */
|
|
27
|
+
baseDir?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface RecipeValidation {
|
|
30
|
+
findings: Finding[];
|
|
31
|
+
/** Present only when the recipe parsed and satisfied the schema. */
|
|
32
|
+
config?: RuntimeConfig;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Read a recipe and report every problem the static depth can see.
|
|
36
|
+
*
|
|
37
|
+
* Returns the parsed config alongside the findings when it got that far, so a caller running deeper
|
|
38
|
+
* depths does not parse a second time.
|
|
39
|
+
*/
|
|
40
|
+
export declare function collectRecipeFindings(source: RecipeSource): Promise<RecipeValidation>;
|
|
41
|
+
//# sourceMappingURL=recipe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipe.d.ts","sourceRoot":"","sources":["../../src/validate/recipe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAUH,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAUvF,OAAO,KAAK,EAAE,OAAO,EAA4B,MAAM,YAAY,CAAC;AAEpE,MAAM,WAAW,YAAY;IAC3B,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,oEAAoE;IACpE,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAkID;;;;;GAKG;AACH,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA2F3F"}
|