sigrank 0.0.178 → 0.0.180

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/MCP_SERVER_DESCRIPTION.md +141 -0
  3. package/README.md +8 -0
  4. package/adapters/index.mjs +865 -0
  5. package/adapters/tokenpull.mjs +576 -0
  6. package/adapters.mjs +1 -865
  7. package/analytics/cascade.mjs +274 -0
  8. package/cascade.mjs +1 -274
  9. package/cli.mjs +1 -1853
  10. package/connect.mjs +1 -13
  11. package/identity/connect.mjs +13 -0
  12. package/identity/keystore.mjs +242 -0
  13. package/identity/sign.mjs +106 -0
  14. package/index.mjs +43 -159
  15. package/keystore.mjs +1 -242
  16. package/narrate.mjs +1 -77
  17. package/package.json +32 -15
  18. package/preflight.mjs +1 -191
  19. package/presentation/cli.mjs +1874 -0
  20. package/presentation/narrate.mjs +77 -0
  21. package/presentation/tui.mjs +2668 -0
  22. package/prompts/check-my-efficiency.md +1 -0
  23. package/prompts/compare-with-leader.md +1 -0
  24. package/prompts/simulate-improvement.md +1 -0
  25. package/resources/class-tiers.md +23 -0
  26. package/resources/data-policy.md +27 -0
  27. package/resources/install-guide.md +59 -0
  28. package/resources/privacy-model.md +30 -0
  29. package/resources/scoring-formula.md +28 -0
  30. package/review.mjs +263 -0
  31. package/sign.mjs +1 -106
  32. package/submit/index.mjs +309 -0
  33. package/submit/preflight.mjs +191 -0
  34. package/submit.mjs +1 -303
  35. package/tokenpull.mjs +1 -576
  36. package/tokscale_analytics.mjs +875 -0
  37. package/tools/_framing.mjs +139 -0
  38. package/tools/_helpers.mjs +258 -0
  39. package/tools/_report.mjs +262 -0
  40. package/tools/_schemas.mjs +394 -0
  41. package/tools/_verifiers.mjs +148 -0
  42. package/tools/compare-operators.mjs +79 -0
  43. package/tools/compare-self.mjs +122 -0
  44. package/tools/describe-power-user.mjs +68 -0
  45. package/tools/diagnose-cascade.mjs +192 -0
  46. package/tools/enroll.mjs +121 -0
  47. package/tools/get-best-operator.mjs +58 -0
  48. package/tools/get-leaderboard.mjs +26 -0
  49. package/tools/get-operator.mjs +33 -0
  50. package/tools/index.mjs +246 -0
  51. package/tools/optimize-efficiency.mjs +160 -0
  52. package/tools/rank-paste.mjs +51 -0
  53. package/tools/rank-windows.mjs +120 -0
  54. package/tools/self-improve.mjs +423 -0
  55. package/tools/simulate-change.mjs +207 -0
  56. package/tools/submit-paste.mjs +118 -0
  57. package/tools/submit-verified.mjs +153 -0
  58. package/tools/suggest-improvements.mjs +168 -0
  59. package/tools/tokenpull-compare.mjs +123 -0
  60. package/tools/tokenpull-submit.mjs +234 -0
  61. package/tools/tokenpull.mjs +113 -0
  62. package/tools/tokscale-breakdown.mjs +159 -0
  63. package/tools/tokscale-competitive-intel.mjs +45 -0
  64. package/tools/tokscale-cost-analysis.mjs +31 -0
  65. package/tools/tokscale-developer-profile.mjs +36 -0
  66. package/tools/tokscale-device-profile.mjs +35 -0
  67. package/tools/tokscale-market-share.mjs +38 -0
  68. package/tools/tokscale-mcp-usage.mjs +33 -0
  69. package/tools/tokscale-model-trends.mjs +32 -0
  70. package/tools/watch-tokenpull.mjs +134 -0
  71. package/tools.mjs +1 -3897
  72. package/tui.mjs +4 -2650
  73. package/banner.svg +0 -7
package/keystore.mjs CHANGED
@@ -1,242 +1 @@
1
- // keystore.mjs — local ed25519 device identity for SigRank signed submit (D7 §4.5).
2
- //
3
- // Persists a per-device keypair at ~/.sigrank-mcp/identity.json (dir 0700, file 0600).
4
- // The PRIVATE key never leaves this machine; only the raw 32-byte public key (as
5
- // "ed25519:<base64>") is sent to the server at enroll time. The device_id (uuid) is
6
- // generated once and is the immutable binding target server-side.
7
- //
8
- // Public-key export MUST match the server's publicKeyFromAgent: generateKeyPairSync
9
- // exports SPKI DER with a 12-byte prefix; we strip it to the raw 32 bytes, base64 it,
10
- // and prefix "ed25519:". The server re-adds the SPKI prefix to verify. Round-trip is
11
- // locked by sign.test.mjs + the canon-parity fixture.
12
-
13
- import { homedir } from "node:os";
14
- import { join } from "node:path";
15
- import {
16
- mkdirSync,
17
- readFileSync,
18
- writeFileSync,
19
- existsSync,
20
- chmodSync,
21
- unlinkSync,
22
- readdirSync,
23
- copyFileSync,
24
- } from "node:fs";
25
- import { generateKeyPairSync, randomUUID } from "node:crypto";
26
-
27
- const DIR = join(homedir(), ".sigrank-mcp");
28
- const PATH = join(DIR, "identity.json");
29
- const BACKUP_PREFIX = "identity.json.bak-";
30
- const MAX_BACKUPS = 5;
31
-
32
- /** SPKI DER prefix length for an ed25519 public key (12 bytes before the raw 32). */
33
- const SPKI_PREFIX_LEN = 12;
34
-
35
- /** Resolve this package's version for the agent_version stamp (best-effort). */
36
- function agentVersion() {
37
- try {
38
- const pkg = JSON.parse(
39
- readFileSync(new URL("./package.json", import.meta.url), "utf-8"),
40
- );
41
- return `sigrank-mcp/${pkg.version}`;
42
- } catch {
43
- return "sigrank-mcp";
44
- }
45
- }
46
-
47
- export function keystorePath() {
48
- return PATH;
49
- }
50
-
51
- /** Read the stored identity, or null if absent/corrupt. */
52
- export function loadIdentity() {
53
- if (!existsSync(PATH)) return null;
54
- try {
55
- return JSON.parse(readFileSync(PATH, "utf-8"));
56
- } catch {
57
- return null;
58
- }
59
- }
60
-
61
- /** Write the identity with strict perms (dir 0700, file 0600). Backs up the existing
62
- * file before overwriting so a corrupt write or accidental reset can be recovered. */
63
- export function persistIdentity(identity) {
64
- mkdirSync(DIR, { recursive: true });
65
- try {
66
- chmodSync(DIR, 0o700);
67
- } catch {
68
- /* best-effort on platforms without chmod */
69
- }
70
- // Back up the existing identity before overwriting (resilience: if the write
71
- // fails or the file is later lost, the backup carries the binding + keys).
72
- if (existsSync(PATH)) {
73
- try {
74
- const stamp = Date.now();
75
- copyFileSync(PATH, join(DIR, `${BACKUP_PREFIX}${stamp}`));
76
- pruneBackups();
77
- } catch {
78
- /* best-effort — don't block the write */
79
- }
80
- }
81
- writeFileSync(PATH, `${JSON.stringify(identity, null, 2)}\n`, {
82
- mode: 0o600,
83
- });
84
- try {
85
- chmodSync(PATH, 0o600);
86
- } catch {
87
- /* best-effort */
88
- }
89
- return identity;
90
- }
91
-
92
- /** Build a fresh keypair record (pure — no fs). Exposed for tests. */
93
- export function generateIdentity({ device_id } = {}) {
94
- const { publicKey, privateKey } = generateKeyPairSync("ed25519");
95
- const spki = publicKey.export({ type: "spki", format: "der" });
96
- const rawPub = Buffer.from(spki).subarray(SPKI_PREFIX_LEN); // the raw 32 bytes
97
- const pkcs8 = privateKey.export({ type: "pkcs8", format: "der" });
98
- return {
99
- device_id: device_id || randomUUID(),
100
- codename: null,
101
- operator_id: null,
102
- public_key: `ed25519:${rawPub.toString("base64")}`,
103
- private_key_pkcs8_b64: Buffer.from(pkcs8).toString("base64"),
104
- agent_version: agentVersion(),
105
- enrolled_at: null,
106
- };
107
- }
108
-
109
- /**
110
- * Decide the binding (codename/operator_id/enrolled_at) to carry onto a freshly
111
- * generated identity. The binding is tied to device_id: if device_id is CHANGING
112
- * (a new device, e.g. after a reset/re-enroll where the old file was deleted), the
113
- * old binding is INVALIDATED — never preserve a stale codename/operator onto a
114
- * different device. That mismatch is the "Frankenstein identity" root cause: a new
115
- * device_id + an old codename → the server sees a mismatch → tags submissions
116
- * `unverified` → never ranks, yet `isSignedIn` reads the local codename as present.
117
- * Only reuse the binding when the SAME device_id is being kept. Pure — exposed for
118
- * tests (no fs, so the owner's live identity is never at risk during a test run).
119
- */
120
- export function bindingForFreshIdentity(existing, fresh) {
121
- if (!existing)
122
- return { codename: null, operator_id: null, enrolled_at: null };
123
- if (existing.device_id && fresh.device_id === existing.device_id) {
124
- return {
125
- codename: existing.codename ?? null,
126
- operator_id: existing.operator_id ?? null,
127
- enrolled_at: existing.enrolled_at ?? null,
128
- };
129
- }
130
- // device_id changed → the old binding belongs to a different device; drop it.
131
- return { codename: null, operator_id: null, enrolled_at: null };
132
- }
133
-
134
- /**
135
- * Load the existing identity, or generate + persist a fresh one. Idempotent: a
136
- * complete existing identity is returned untouched (never rotates a live key).
137
- * Self-healing: if the identity has keys + device_id but no codename (binding
138
- * lost via partial write / version transition), scans backups for a matching
139
- * device_id and restores the binding before returning.
140
- */
141
- export function ensureIdentity() {
142
- const existing = loadIdentity();
143
- if (
144
- existing?.private_key_pkcs8_b64 &&
145
- existing?.public_key &&
146
- existing?.device_id
147
- ) {
148
- // Self-healing: keys present but binding lost → try to restore from backups.
149
- if (!existing.codename || !existing.operator_id) {
150
- const restored = restoreBindingFromBackups(existing.device_id);
151
- if (restored) {
152
- existing.codename = restored.codename;
153
- existing.operator_id = restored.operator_id;
154
- existing.enrolled_at = restored.enrolled_at;
155
- return persistIdentity(existing);
156
- }
157
- }
158
- return existing;
159
- }
160
- // Regeneration path: a partial/corrupt record. The binding is tied to device_id —
161
- // if a new device_id is generated, the old codename/operator MUST NOT carry over
162
- // (bindingForFreshIdentity drops them). Only a reused device_id keeps its binding.
163
- const fresh = generateIdentity({ device_id: existing?.device_id });
164
- const binding = bindingForFreshIdentity(existing, fresh);
165
- fresh.codename = binding.codename;
166
- fresh.operator_id = binding.operator_id;
167
- fresh.enrolled_at = binding.enrolled_at;
168
- return persistIdentity(fresh);
169
- }
170
-
171
- /** Record a successful enrollment (codename/operator_id + enrolled_at) into the keystore. */
172
- export function recordEnrollment({ codename, operator_id }) {
173
- const id = ensureIdentity();
174
- // OVERWRITE from the server's enroll response — a new enroll is a new binding, full
175
- // stop. Never `??`-preserve a stale codename/operator onto this device (the other
176
- // half of the Frankenstein bug: a re-enroll used to keep the old codename when the
177
- // new one was absent). A 201 enrolled always carries both.
178
- id.codename = codename ?? null;
179
- id.operator_id = operator_id ?? null;
180
- id.enrolled_at = new Date().toISOString();
181
- return persistIdentity(id);
182
- }
183
-
184
- /** Clear the local identity (sign out). Next enroll provisions a fresh device_id. */
185
- export function clearIdentity() {
186
- try {
187
- if (existsSync(PATH)) unlinkSync(PATH);
188
- } catch {
189
- /* best-effort */
190
- }
191
- }
192
-
193
- /** Keep at most MAX_BACKUPS backup files (oldest deleted first). */
194
- function pruneBackups() {
195
- try {
196
- const files = readdirSync(DIR)
197
- .filter((f) => f.startsWith(BACKUP_PREFIX))
198
- .sort(); // timestamp-suffixed → lexicographic = chronological
199
- while (files.length > MAX_BACKUPS) {
200
- unlinkSync(join(DIR, files.shift()));
201
- }
202
- } catch {
203
- /* best-effort */
204
- }
205
- }
206
-
207
- /** Scan backup files for one with the same device_id that carries a codename +
208
- * operator_id binding. Returns { codename, operator_id, enrolled_at } or null.
209
- * Used by ensureIdentity() to self-heal when the live file lost its binding
210
- * (e.g. a partial write or version transition that preserved keys but dropped
211
- * the binding metadata). Only restores from a backup with the SAME device_id —
212
- * never crosses device boundaries (Frankenstein-identity guard). */
213
- export function restoreBindingFromBackups(device_id) {
214
- if (!device_id) return null;
215
- try {
216
- const files = readdirSync(DIR)
217
- .filter((f) => f.startsWith(BACKUP_PREFIX))
218
- .sort()
219
- .reverse(); // newest first — prefer the most recent binding
220
- for (const f of files) {
221
- try {
222
- const bak = JSON.parse(readFileSync(join(DIR, f), "utf-8"));
223
- if (
224
- bak.device_id === device_id &&
225
- bak.codename &&
226
- bak.operator_id
227
- ) {
228
- return {
229
- codename: bak.codename,
230
- operator_id: bak.operator_id,
231
- enrolled_at: bak.enrolled_at ?? null,
232
- };
233
- }
234
- } catch {
235
- /* skip corrupt backups */
236
- }
237
- }
238
- } catch {
239
- /* best-effort */
240
- }
241
- return null;
242
- }
1
+ export * from './identity/keystore.mjs';
package/narrate.mjs CHANGED
@@ -1,77 +1 @@
1
- /**
2
- * narrate.mjs — deterministic prose "card" for a cascade result.
3
- *
4
- * Port of _template() from ~/Desktop/moses-sigrank/narrate.py. The model path
5
- * (MiniCPM4-0.5B) is intentionally SKIPPED: the template is the trustworthy,
6
- * instant, auditable fallback — same numbers in → same card out, and it can never
7
- * emit a metric the cascade didn't produce. A model hook can layer behind this same
8
- * narrate() interface later without a rewrite.
9
- *
10
- * Token-only. No network, no randomness.
11
- */
12
-
13
- // Safe formatters: never emit NaN/Infinity/undefined into the card.
14
- const safeNum = (n) => (Number.isFinite(Number(n)) ? Number(n) : null);
15
- const comma = (n, dec) => {
16
- const v = safeNum(n);
17
- return v !== null
18
- ? v.toLocaleString("en-US", {
19
- minimumFractionDigits: dec,
20
- maximumFractionDigits: dec,
21
- })
22
- : "—";
23
- };
24
- const plain = (n, dec) => {
25
- const v = safeNum(n);
26
- return v !== null ? v.toFixed(dec) : "—";
27
- };
28
-
29
- /**
30
- * Given a cascade result ({ velocity, leverage, dev10x, pillars, class }) and an
31
- * optional subject name, return "**CLASS.** <one or two sentences>". Deterministic.
32
- */
33
- export function narrate(cascade, name = "This operator") {
34
- const klass = cascade.class || cascade.klass || "UNCLASSED";
35
- const v = safeNum(cascade.velocity);
36
- const l = safeNum(cascade.leverage);
37
-
38
- // "non-compounding" = a stateless pipe: no cache commits, so the cascade can't
39
- // form. cascade.mjs leaves dev10x null when cacheCreate is 0 (the cw/o term
40
- // collapses), which is exactly metrics.py's non_compounding flag.
41
- // Also catches zero-input sessions where velocity/leverage are null.
42
- const cw = cascade.pillars ? Number(cascade.pillars.cacheCreate) : NaN;
43
- const nonCompounding =
44
- cascade.dev10x == null || !(cw > 0) || v === null || l === null;
45
-
46
- let body;
47
- if (nonCompounding) {
48
- const leverageStr =
49
- l !== null
50
- ? `Leverage ${comma(l, 1)}x comes from reuse alone.`
51
- : "Leverage is undefined (no fresh input recorded).";
52
- const dev10xNote =
53
- cascade.dev10x == null
54
- ? " 10xDEV is undefined — the compounding loop has not formed yet."
55
- : "";
56
- body =
57
- `${name} runs a stateless pipe — no cache commits, so the cascade can't form. ` +
58
- `High read volume, but nothing is being built forward. ${leverageStr}${dev10xNote}`;
59
- } else if (v >= 1 && l >= 100) {
60
- body =
61
- `${name} holds both axes at once: ${plain(v, 1)}x generation AND ${comma(l, 0)}x memory leverage. ` +
62
- `A closed kinetic loop — the rare operator the leverage/generation tradeoff says shouldn't exist.`;
63
- } else if (l >= 10 && v < 1) {
64
- body =
65
- `${name} is an archival sponge — ${comma(l, 0)}x reuse but only ${plain(v, 2)}x generation. ` +
66
- `Holds context beautifully, executes little with it. The reuse number is inflated by a weak commitment stage.`;
67
- } else if (v >= 0.8 && l < 2) {
68
- body =
69
- `${name} is a volatile ingestor — ${plain(v, 2)}x generation but ${plain(l, 1)}x leverage. ` +
70
- `Fast on single shots, resets between turns. Memory doesn't persist into a compounding loop.`;
71
- } else {
72
- body =
73
- `${name} sits low on both axes: ${plain(v, 2)}x generation, ${plain(l, 1)}x leverage. ` +
74
- `A transient profile — neither building state nor converting input to output efficiently.`;
75
- }
76
- return `**${klass}.** ${body}`;
77
- }
1
+ export * from './presentation/narrate.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigrank",
3
- "version": "0.0.178",
3
+ "version": "0.0.180",
4
4
  "author": "SunrisesIllNeverSee (https://github.com/SunrisesIllNeverSee)",
5
5
  "description": "SigRank MCP server — the yield cascade + live leaderboard as MCP tools any agent can call",
6
6
  "mcpName": "io.github.SunrisesIllNeverSee/sigrank-mcp",
@@ -24,26 +24,40 @@
24
24
  },
25
25
  "scripts": {
26
26
  "start": "node index.mjs",
27
- "test": "node test.mjs && node sign.test.mjs && node --test __tests__/cascade.test.mjs __tests__/badges.test.mjs",
28
- "sync:platforms": "node scripts/sync-platforms.mjs"
27
+ "test": "node test.mjs && node __tests__/sign.test.mjs && node --test __tests__/cascade.test.mjs __tests__/badges.test.mjs __tests__/tui-input.test.mjs",
28
+ "sync:platforms": "node scripts/sync-platforms.mjs",
29
+ "sync:spine": "node scripts/sync-spine.mjs",
30
+ "spine:check": "node scripts/sync-spine.mjs --check",
31
+ "spine:diff": "node scripts/sync-spine.mjs --dry-run"
29
32
  },
30
33
  "files": [
31
34
  "index.mjs",
32
- "cli.mjs",
33
- "tui.mjs",
34
- "tools.mjs",
35
+ "tools/",
36
+ "analytics/",
37
+ "identity/",
38
+ "submit/",
39
+ "adapters/",
40
+ "presentation/",
41
+ "resources/",
42
+ "prompts/",
43
+ "badges.mjs",
44
+ "review.mjs",
35
45
  "cascade.mjs",
36
- "narrate.mjs",
37
- "tokenpull.mjs",
38
- "adapters.mjs",
39
- "sign.mjs",
40
46
  "keystore.mjs",
41
- "submit.mjs",
47
+ "sign.mjs",
42
48
  "connect.mjs",
43
- "badges.mjs",
49
+ "submit.mjs",
44
50
  "preflight.mjs",
45
- "banner.svg",
51
+ "adapters.mjs",
52
+ "tokenpull.mjs",
53
+ "tui.mjs",
54
+ "cli.mjs",
55
+ "narrate.mjs",
56
+ "tools.mjs",
57
+ "tokscale_analytics.mjs",
46
58
  "README.md",
59
+ "CHANGELOG.md",
60
+ "MCP_SERVER_DESCRIPTION.md",
47
61
  "LICENSE"
48
62
  ],
49
63
  "keywords": [
@@ -65,7 +79,10 @@
65
79
  ],
66
80
  "dependencies": {
67
81
  "@modelcontextprotocol/sdk": "^1.0.0",
68
- "ccusage": "^20.0.17",
69
- "tokscale": "^4.5.2"
82
+ "ccusage": "^20.0.19",
83
+ "tokscale": "^4.7.0"
84
+ },
85
+ "overrides": {
86
+ "@hono/node-server": "^2.0.5"
70
87
  }
71
88
  }
package/preflight.mjs CHANGED
@@ -1,191 +1 @@
1
- /**
2
- * preflight.mjs — local plausibility pre-checks for the MCP submit path.
3
- *
4
- * Mirrors ONLY the server's public plausibility gate (gates.ts:86-137) —
5
- * totals consistency, turns/sessions ratios, output rate, cache ratio,
6
- * cadence. These are integrity guards labeled "NOT the proprietary RS.xx"
7
- * and are safe to replicate in an open agent.
8
- *
9
- * The proprietary battery (Benford / cadence / contamination) is SERVER-ONLY
10
- * and must NOT be shipped in the agent. The server always runs its own
11
- * battery after the POST — this preflight is a client-side preview of the
12
- * public plausibility checks only, not a replacement for the full gate chain.
13
- *
14
- * If preflight flags, the operator sees WHY their submission would be
15
- * rejected or flagged, and can fix the issue or submit anyway.
16
- *
17
- * Source of truth: lib/ingest/gates.ts (plausibilityGate). This port must
18
- * stay in sync with the plausibility gate ONLY.
19
- */
20
-
21
- // ── plausibility checks (from gates.ts:86-137) ──────────────────────────────
22
-
23
- const GATE_LIMITS = {
24
- TOTALS_TOLERANCE_FRAC: 0.01,
25
- MAX_OUTPUT_TOKENS_PER_MIN: 20_000,
26
- // Tightened range-plausibility bounds (deviewreview3)
27
- MAX_CACHE_REUSE_RATIO: 35,
28
- MIN_CACHE_WRITE_RATIO: 0.5,
29
- MIN_INPUT_SHARE_FRAC: 0.0003,
30
- MAX_CADENCE_PER_MIN: 15,
31
- };
32
-
33
- /**
34
- * Run plausibility checks against a payload's raw_telemetry.
35
- * Returns array of issues: { severity, code, detail }
36
- */
37
- export function plausibilityCheck(rt, window) {
38
- const out = [];
39
- const pillars =
40
- rt.tokens_input_fresh +
41
- rt.tokens_output +
42
- rt.tokens_cache_read +
43
- rt.tokens_cache_creation;
44
-
45
- if (rt.tokens_total > 0) {
46
- const tol = Math.max(
47
- 1,
48
- rt.tokens_total * GATE_LIMITS.TOTALS_TOLERANCE_FRAC,
49
- );
50
- if (Math.abs(pillars - rt.tokens_total) > tol) {
51
- out.push({
52
- severity: "reject",
53
- code: "totals_inconsistent",
54
- detail: `Σ4 pillars (${pillars}) ≠ tokens_total (${rt.tokens_total})`,
55
- });
56
- }
57
- if (rt.sessions_count === 0) {
58
- out.push({
59
- severity: "reject",
60
- code: "tokens_without_sessions",
61
- detail: `tokens_total ${rt.tokens_total} with sessions_count 0`,
62
- });
63
- }
64
- }
65
- if (rt.turns_total < rt.sessions_count) {
66
- out.push({
67
- severity: "reject",
68
- code: "turns_lt_sessions",
69
- detail: `turns_total ${rt.turns_total} < sessions_count ${rt.sessions_count}`,
70
- });
71
- }
72
- if (rt.tokens_output > 0 && rt.turns_total === 0) {
73
- out.push({
74
- severity: "reject",
75
- code: "output_without_turns",
76
- detail: `tokens_output ${rt.tokens_output} with turns_total 0`,
77
- });
78
- }
79
-
80
- if (window) {
81
- const spanMin =
82
- (Date.parse(window.end) - Date.parse(window.start)) / 60_000;
83
- if (Number.isFinite(spanMin) && rt.active_minutes_est > spanMin + 1) {
84
- out.push({
85
- severity: "flag",
86
- code: "active_exceeds_window",
87
- detail: `active_minutes_est ${rt.active_minutes_est} > window span ${Math.round(spanMin)}m`,
88
- });
89
- }
90
- }
91
-
92
- const outPerMin = rt.tokens_output / Math.max(rt.active_minutes_est, 1);
93
- if (outPerMin > GATE_LIMITS.MAX_OUTPUT_TOKENS_PER_MIN) {
94
- out.push({
95
- severity: "flag",
96
- code: "implausible_output_rate",
97
- detail: `${Math.round(outPerMin)} output tok/min > ${GATE_LIMITS.MAX_OUTPUT_TOKENS_PER_MIN}`,
98
- });
99
- }
100
-
101
- // Cross-field ratio checks (defense-in-depth — also in the plausibility gate)
102
- // Bounds tightened (deviewreview3): original 100:1 + 50/min were too loose.
103
- if (rt.tokens_cache_read > 1_000 && rt.tokens_cache_creation === 0) {
104
- out.push({
105
- severity: "flag",
106
- code: "cache_without_creation",
107
- detail: `${rt.tokens_cache_read} cache_read with 0 cache_creation (impossible cascade)`,
108
- });
109
- }
110
- if (
111
- rt.tokens_cache_creation > 0 &&
112
- rt.tokens_cache_read / rt.tokens_cache_creation >
113
- GATE_LIMITS.MAX_CACHE_REUSE_RATIO
114
- ) {
115
- out.push({
116
- severity: "flag",
117
- code: "extreme_cache_ratio",
118
- detail: `cache_read/cache_creation = ${(rt.tokens_cache_read / rt.tokens_cache_creation).toFixed(1)}:1 (real max ~30:1)`,
119
- });
120
- }
121
- if (
122
- rt.tokens_output > 1_000 &&
123
- rt.tokens_cache_creation / rt.tokens_output <
124
- GATE_LIMITS.MIN_CACHE_WRITE_RATIO
125
- ) {
126
- out.push({
127
- severity: "flag",
128
- code: "low_cache_write_ratio",
129
- detail: `cache_creation/output = ${(rt.tokens_cache_creation / rt.tokens_output).toFixed(2)}:1 (real min ~1.5:1)`,
130
- });
131
- }
132
- if (
133
- pillars > 10_000 &&
134
- rt.tokens_input_fresh / pillars < GATE_LIMITS.MIN_INPUT_SHARE_FRAC
135
- ) {
136
- out.push({
137
- severity: "flag",
138
- code: "implausible_input_share",
139
- detail: `input is ${((rt.tokens_input_fresh / pillars) * 100).toFixed(3)}% of total (real min ~0.3%)`,
140
- });
141
- }
142
- if (
143
- rt.active_minutes_est > 0 &&
144
- rt.turns_total / rt.active_minutes_est > GATE_LIMITS.MAX_CADENCE_PER_MIN
145
- ) {
146
- out.push({
147
- severity: "flag",
148
- code: "implausible_cadence",
149
- detail: `${(rt.turns_total / rt.active_minutes_est).toFixed(1)} turns/min (real: 0.5-10)`,
150
- });
151
- }
152
-
153
- return out;
154
- }
155
-
156
- // ── full preflight (plausibility only — NO battery) ─────────────────────────
157
-
158
- /**
159
- * Run preflight plausibility checks against a payload. Returns:
160
- * { pass: true, issues: [] } — clean submission
161
- * { pass: false, issues: [...], wouldDowngrade } — would be flagged/rejected
162
- *
163
- * NOTE: this only mirrors the PUBLIC plausibility gate. The server runs
164
- * additional proprietary checks (the battery) that this preflight does NOT
165
- * replicate. A "pass" here means "passes the plausibility gate," not
166
- * "guaranteed to pass all server gates."
167
- */
168
- export function preflight(payload) {
169
- const issues = plausibilityCheck(payload.raw_telemetry, payload.window);
170
-
171
- const hasReject = issues.some((i) => i.severity === "reject");
172
- const hasFlag = issues.some((i) => i.severity === "flag");
173
-
174
- return {
175
- pass: issues.length === 0,
176
- wouldReject: hasReject,
177
- wouldDowngrade: hasFlag,
178
- issues,
179
- summary: hasReject
180
- ? `REJECT: ${issues
181
- .filter((i) => i.severity === "reject")
182
- .map((i) => i.code)
183
- .join(", ")}`
184
- : hasFlag
185
- ? `FLAG (verified → flagged, not ranked): ${issues
186
- .filter((i) => i.severity === "flag")
187
- .map((i) => i.code)
188
- .join(", ")}`
189
- : "clean — passes plausibility gate (server runs additional proprietary checks)",
190
- };
191
- }
1
+ export * from './submit/preflight.mjs';