sigrank 1.0.11 → 1.0.14

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/README.md CHANGED
@@ -1,16 +1,17 @@
1
- # Upsilon Measurement Engine — `sigrank` CLI/MCP
1
+ # SigRank SignalAF — `sigrank` CLI/MCP
2
2
 
3
- > **Upsilon** is SignalAF's measurement engine. **SigRank** is the public leaderboard
4
- > and proof surface. The existing `sigrank` package name and `sigrank/0.1-draft`
5
- > record remain stable for compatibility. _Four token counts only. Never prompts or code._
3
+ > **SignalAF** is the public brand. **SigRank** is the leaderboard and proof surface.
4
+ > The `sigrank` package reads local AI session logs, derives your token cascade,
5
+ > and publishes to the board at [signalaf.com](https://signalaf.com).
6
+ > _Four token counts only. Never prompts or code._
6
7
 
7
8
  <div align="center">
8
9
 
9
10
  <p><img src="./.github/assets/og-card.png" alt="SigRank SignalAF — AI operator evaluation and token cascade efficiency leaderboard" width="800"></p>
10
11
 
11
- **Upsilon measures. SigRank proves. MO§ES™ governs. SignalAF is the brand.**
12
+ **SignalAF is the brand. SigRank proves. MO§ES™ governs.**
12
13
 
13
- Upsilon describes observable AI operator token-processing patterns—not AI model quality, cognition, work quality, employee productivity, or business value.
14
+ SigRank evaluates observable AI operator token-processing patterns—not AI model quality, cognition, work quality, employee productivity, or business value.
14
15
 
15
16
  [![npm version](https://img.shields.io/npm/v/sigrank.svg?style=flat-square&color=gold)](https://www.npmjs.com/package/sigrank)
16
17
  [![CI](https://github.com/SunrisesIllNeverSee/sigrank-mcp/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/SunrisesIllNeverSee/sigrank-mcp/actions/workflows/ci.yml)
@@ -59,13 +60,13 @@ Upsilon describes observable AI operator token-processing patterns—not AI mode
59
60
  ## SignalAF product architecture
60
61
 
61
62
  ```text
62
- MO§ES™ governance → Upsilon measurement engine → SigRank public proof
63
+ MO§ES™ governance → SignalAF brand → SigRank leaderboard → Upsilon engine
63
64
  ```
64
65
 
65
66
  | Repo | What it is | Install |
66
67
  |------|-----------|---------|
67
- | **[sigrank-mcp](https://github.com/SunrisesIllNeverSee/sigrank-mcp)** (this repo) | Upsilon's compatibility instrument—extracts four token pillars, computes locally, and optionally submits to SigRank. | `npx sigrank` |
68
- | **[sigrank-app](https://github.com/SunrisesIllNeverSee/sigrank-app)** | Upsilon web implementation plus the SigRank leaderboard/proof surface. | [signalaf.com](https://signalaf.com) |
68
+ | **[sigrank-mcp](https://github.com/SunrisesIllNeverSee/sigrank-mcp)** (this repo) | SigRank's on-device scanner — extracts four token pillars, computes locally, and optionally submits to the board. | `npx sigrank` |
69
+ | **[sigrank-app](https://github.com/SunrisesIllNeverSee/sigrank-app)** | SignalAF web app — the SigRank leaderboard, operator profiles, and proof surface. | [signalaf.com](https://signalaf.com) |
69
70
  | **[bestuser-router-mcp](https://github.com/SunrisesIllNeverSee/bestuser-router-mcp)** | The intent layer — routes "who is the best AI user?" queries to SigRank SignalAF's leaderboard. MCP server for AI assistants. | `npx bestuser-router-mcp` |
70
71
  | **[sigarena](https://github.com/SunrisesIllNeverSee/sigarena)** | The satellite — public LLM operator evals at sigeconomy.com. Read-only leaderboard, SEO/AEO surface. | [sigeconomy.com](https://sigeconomy.com) |
71
72
  | **[signaf](https://github.com/SunrisesIllNeverSee/signa)** | The coach — reads your session logs, builds a taste profile, measures ASI, coaches you on token efficiency. | `npx @burnmydays/signaf` |
@@ -77,7 +78,7 @@ MO§ES™ governance → Upsilon measurement engine → SigRank public proof
77
78
  | Site | What it is |
78
79
  | ---- | ---------- |
79
80
  | **[SIGNOMY](https://signomy.xyz)** | Governed AI agent marketplace where ranked agents form teams, fill slots, run missions, and earn revenue under constitutional protocol. Agents are free. Operators pay. |
80
- | **[MO§ES](https://mos2es.com)** | Constitutional governance and methodology—the law governing Upsilon and other governed systems. |
81
+ | **[MO§ES](https://mos2es.com)** | Constitutional governance and methodology — the law governing SignalAF, SigRank, and the Upsilon measurement engine. |
81
82
 
82
83
  ## Quickstart — 3 steps to the board
83
84
 
package/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://raw.githubusercontent.com/anthropics/mcpb/main/schemas/mcpb-manifest-v0.4.schema.json",
3
3
  "manifest_version": "0.4",
4
4
  "name": "sigrank",
5
- "version": "1.0.10",
5
+ "version": "1.0.13",
6
6
  "description": "Ranks AI coding operators by token cascade efficiency (Yield = cache_read \u00d7 output / input\u00b2). 25 tools including SigRank Standard record, yield calculation, local token usage pulls, signed submission, and tokscale-powered analytics (market share, developer profiles, model trends, cost analysis, device profiles, MCP usage, competitive intelligence). Privacy-first: runs locally, only four token counts leave the machine.",
7
7
  "author": {
8
8
  "name": "SunrisesIllNeverSee",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigrank",
3
- "version": "1.0.11",
3
+ "version": "1.0.14",
4
4
  "author": "SunrisesIllNeverSee (https://github.com/SunrisesIllNeverSee)",
5
5
  "description": "Upsilon measurement engine distributed through the sigrank CLI/MCP compatibility package, with SigRank public leaderboard tools.",
6
6
  "mcpName": "io.github.SunrisesIllNeverSee/sigrank-mcp",
@@ -2335,6 +2335,45 @@ export async function runTui({
2335
2335
  return;
2336
2336
  }
2337
2337
 
2338
+ // ── Terminal size guard ────────────────────────────────────────────────────
2339
+ // The TUI renders a tab bar + content + footer in a locked alternate-screen
2340
+ // frame. If the terminal is too small, the footer smears off the bottom and
2341
+ // content rows get clamped — confusing, not useful. Refuse to render until
2342
+ // there's enough room, matching the PostHog wizard's pattern: show a clear
2343
+ // message with current vs required dimensions, and re-check on resize.
2344
+ const MIN_COLS = 80;
2345
+ const MIN_ROWS = 24;
2346
+ const tooSmall = () =>
2347
+ (process.stdout.columns || 80) < MIN_COLS ||
2348
+ (process.stdout.rows || 24) < MIN_ROWS;
2349
+ if (tooSmall() && process.stdout.isTTY) {
2350
+ const renderSizeMsg = () => {
2351
+ const cols = process.stdout.columns || 80;
2352
+ const rows = process.stdout.rows || 24;
2353
+ process.stdout.write(`${ESC}H${ESC}2J`);
2354
+ process.stdout.write(
2355
+ `\n SigRank TUI needs more room to display required content.\n` +
2356
+ ` Please make this terminal bigger.\n\n` +
2357
+ ` Currently ${cols}×${rows}, needs at least ${MIN_COLS}×${MIN_ROWS}.\n\n` +
2358
+ ` Waiting for resize… (Ctrl+C to cancel)\n`,
2359
+ );
2360
+ };
2361
+ renderSizeMsg();
2362
+ await new Promise((resolve) => {
2363
+ const onResize = () => {
2364
+ if (!tooSmall()) {
2365
+ process.stdout.off("resize", onResize);
2366
+ resolve();
2367
+ } else {
2368
+ renderSizeMsg();
2369
+ }
2370
+ };
2371
+ process.stdout.on("resize", onResize);
2372
+ });
2373
+ // Clear the size-warning message before entering the alt screen
2374
+ process.stdout.write(`${ESC}H${ESC}2J`);
2375
+ }
2376
+
2338
2377
  write(ENTER_ALT); // switch to alternate screen — original terminal state preserved on exit
2339
2378
  write(CLEAR_SB); // FIXED-WINDOW (2026-06-27): clear scrollback so nothing is above the TUI
2340
2379
  write(HIDE);
package/proxy.mjs CHANGED
@@ -360,7 +360,16 @@ export async function startProxy({
360
360
  return;
361
361
  }
362
362
 
363
- const upstreamUrl = new URL(req.url, upstreamBase);
363
+ const upstreamUrl = new URL(parsedUrl.pathname + parsedUrl.search, upstreamBase);
364
+ // Defense-in-depth: verify the constructed URL origin matches the intended
365
+ // upstream. This prevents an absolute-form request target (e.g.
366
+ // "http://attacker.example/v1/messages") from overriding upstreamBase via
367
+ // new URL(req.url, base) — which would forward credentials to an attacker.
368
+ if (upstreamUrl.origin !== new URL(upstreamBase).origin) {
369
+ res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
370
+ res.end("Refused to proxy to unexpected origin\n");
371
+ return;
372
+ }
364
373
  const transport = upstreamUrl.protocol === "http:" ? http : https;
365
374
  const tracker = createUsageTracker({
366
375
  backend: route.backend,
@@ -10,7 +10,7 @@ import { MAX_INPUT, withParseWarnings } from "./_helpers.mjs";
10
10
  export const TOOL_DEF = {
11
11
  name: "rank_windows",
12
12
  description:
13
- "Rank all four time windows (7d/30d/90d/all-time) in one call from a dashboard paste — paste the full table from ccusage, tokscale, or the Claude Max usage dashboard and get the cascade (Υ, SNR, Leverage, Velocity, 10xDEV, class, card) for each window. Each window is parsed and scored independently. Named keys required (input/output/cacheCreate/cacheRead); positional order is NOT safe here (dashboards list cache_read before cache_create — see WINDOWED_PROFILES gotcha). Omit windows you don't have — partial input is allowed (1–4 windows). Does NOT submit to the board; use tokenpull_submit for zero-paste publishing.",
13
+ "Rank all four time windows (7d/30d/90d/all-time) in one call from a dashboard paste — paste the full table from ccusage, tokscale, or the Claude Max usage dashboard and get the cascade (Υ, SNR, Leverage, Velocity, 10xDEV, class, card) for each window. Each window is parsed and scored independently. Named keys required (input/output/cacheCreate/cacheRead); positional order is NOT safe here (dashboards list cache_read before cache_create — see WINDOWED_PROFILES gotcha). Omit windows you don't have — partial input is allowed (1–4 windows). Does NOT submit to the board; use tokenpull_submit for a local zero-paste preview, or submit_verified to publish via the enrolled-device path.",
14
14
  annotations: {
15
15
  title: "Rank all time windows",
16
16
  ...ANNOTATIONS.readOnlyHint,
@@ -115,6 +115,6 @@ export async function handleRankWindows(args) {
115
115
  return {
116
116
  windows,
117
117
  source_tool: sourceTool,
118
- note: "Local preview only — use tokenpull_submit to publish to the board.",
118
+ note: "Local preview only — use submit_verified to publish to the board.",
119
119
  };
120
120
  }
@@ -7,17 +7,15 @@ import { narrate } from "../presentation/narrate.mjs";
7
7
  import { SUBMIT_OUTPUT, ANNOTATIONS } from "./_schemas.mjs";
8
8
  import {
9
9
  MAX_INPUT,
10
- uploadStamp,
11
10
  withParseWarnings,
12
11
  } from "./_helpers.mjs";
13
- import { TERMS_VERSION, PRIVACY_VERSION, isRankedAck } from "../lib/constants.mjs";
14
12
 
15
13
  export const TOOL_DEF = {
16
14
  name: "submit_paste",
17
15
  description:
18
- "Ranks a paste of token counts AND publishes it to the live SigRank board at signalaf.com in one call. First computes the cascade locally for an instant preview (yield, leverage, velocity, class, card), then submits the raw paste to the board's web-paste endpoint, which re-parses and re-scores it server-side. The server score is authoritative — it may differ from the local preview if the board applies additional validation. Returns both the local preview and the server response (including the operator's new rank if accepted). A codename is required to publish — omit it for a local preview only (no board submission). Token-only, no auth required. Use this when you have token counts from ccusage or a dashboard and want to both see your score and publish it. Do NOT use this if you want to pull your local usage automatically — use tokenpull_submit for the zero-paste flow. Do NOT use this for multi-window dashboard pastes — use rank_windows to rank them first, then submit each window. After calling this, use get_operator with your codename to verify your submission appeared on the board.",
16
+ "Ranks a paste of token counts locally and shows the cascade result (yield, leverage, velocity, class, card). This is a PREVIEW-ONLY tool — it does not publish to the board. The board's /api/v1/ingest-paste endpoint now requires an authenticated Supabase session, which MCP tools do not carry. To publish to the leaderboard, use submit_verified (which signs and posts to /api/v1/snapshots via the enrolled-device path) or submit directly through the signalaf.com web UI. Use this when you have token counts from ccusage or a dashboard and want to see your score instantly. Do NOT use this if you want to pull your local usage automatically — use tokenpull_submit for the zero-paste flow. Do NOT use this for multi-window dashboard pastes — use rank_windows to rank them first.",
19
17
  annotations: {
20
- title: "Submit paste to board",
18
+ title: "Preview paste ranking",
21
19
  ...ANNOTATIONS.destructiveHint,
22
20
  ...ANNOTATIONS.idempotentHint,
23
21
  ...ANNOTATIONS.openWorldHint,
@@ -28,17 +26,17 @@ export const TOOL_DEF = {
28
26
  text: {
29
27
  type: "string",
30
28
  description:
31
- 'Token counts to rank and submit. Two formats: (1) JSON {"input":N,"output":N,"cacheCreate":N,"cacheRead":N} from ccusage (preferred — the board parses this reliably), or (2) four whitespace-separated numbers: input output cacheCreate cacheRead. The 4-number form ranks locally but the board may reject it. Example: {"input":1000000,"output":500000,"cacheCreate":50000,"cacheRead":800000}',
29
+ 'Token counts to rank. Two formats: (1) JSON {"input":N,"output":N,"cacheCreate":N,"cacheRead":N} from ccusage (preferred), or (2) four whitespace-separated numbers: input output cacheCreate cacheRead. Example: {"input":1000000,"output":500000,"cacheCreate":50000,"cacheRead":800000}',
32
30
  },
33
31
  codename: {
34
32
  type: "string",
35
33
  description:
36
- 'Operator codename to publish under on the leaderboard (e.g. "Ghost Falcon"). Required to submit — omit for local preview only (no board submission, just returns the local cascade result). Must be a non-empty string.',
34
+ 'Operator codename for the ranking card display (e.g. "Ghost Falcon"). Optional — used only for the local preview card, not for board submission.',
37
35
  },
38
36
  },
39
37
  required: ["text"],
40
38
  description:
41
- "Requires token counts (text). Codename is optional but required for board submission — omit it for preview-only mode.",
39
+ "Requires token counts (text). Returns a local preview only — does not publish to the board.",
42
40
  },
43
41
  outputSchema: SUBMIT_OUTPUT,
44
42
  };
@@ -53,69 +51,22 @@ export async function handleSubmitPaste(args, ctx) {
53
51
  detail: `text exceeds ${MAX_INPUT} chars (${args.text.length}). Paste only the token-count table, not full output.`,
54
52
  };
55
53
  }
56
- // Local preview first — also validates the paste is parseable before any POST.
54
+ // Local preview — this tool is preview-only. The board's /api/v1/ingest-paste
55
+ // endpoint now requires an authenticated Supabase session, which MCP tools
56
+ // do not carry. Use submit_verified for authenticated board submission.
57
57
  const pillars = parsePillars(args.text);
58
58
  const c = withParseWarnings(pillars, cascade(pillars));
59
59
  const codename = String(args?.codename || "").trim();
60
60
  const card = narrate(c, codename || "This operator");
61
61
 
62
- // No codename → cannot publish (the board endpoint requires it). Fail fast at the
63
- // tool boundary with a clear message instead of an opaque server 400.
64
- if (!codename) {
65
- return {
66
- ...c,
67
- card,
68
- submission: {
69
- status: "not_submitted",
70
- reason: "codename_required",
71
- detail:
72
- "Pass a codename to publish to the board. Showing local preview only.",
73
- },
74
- };
75
- }
76
-
77
- // Privacy: send ONLY the 4 parsed token numbers (canonical "input output
78
- // cacheCreate cacheRead" form), never the raw user text. parsePillars above
79
- // already validated the input — we reconstruct from the parsed pillars so
80
- // no conversation content or prose can leak to the server. The server's
81
- // ingestMeta() re-parses these 4 numbers authoritatively (Strategy 3:
82
- // four bare numbers). This makes every submit path — signed and paste —
83
- // send only token counts.
84
- const canonicalPaste = `${pillars.input} ${pillars.output} ${pillars.cacheCreate} ${pillars.cacheRead}`;
85
- const stamp = uploadStamp({
86
- codename,
87
- pillars: c.pillars,
88
- source: "web_paste",
89
- });
90
- const res = await ctx.doFetch(`${ctx.apiBase}/api/v1/ingest-paste`, {
91
- method: "POST",
92
- headers: {
93
- "content-type": "application/json",
94
- accept: "application/json",
95
- },
96
- body: JSON.stringify({
97
- codename,
98
- raw_paste: canonicalPaste,
99
- consent_acknowledged: true,
100
- terms_version: TERMS_VERSION,
101
- privacy_version: PRIVACY_VERSION,
102
- ...stamp,
103
- }),
104
- });
105
- let ack;
106
- try {
107
- ack = await res.json();
108
- } catch {
109
- ack = {
110
- status: "error",
111
- detail: `HTTP ${res.status} (non-JSON response)`,
112
- };
113
- }
114
- const ranked = isRankedAck(res, ack);
115
62
  return {
116
63
  ...c,
117
64
  card,
118
- ranked,
119
- submission: { ...stamp, httpStatus: res.status, ranked, ...ack },
65
+ submission: {
66
+ status: "not_submitted",
67
+ reason: "preview_only",
68
+ detail:
69
+ "submit_paste is preview-only. The board requires an authenticated session. Use submit_verified (enrolled device) or the signalaf.com web UI to publish.",
70
+ },
120
71
  };
121
72
  }
@@ -7,19 +7,15 @@ import { narrate } from "../presentation/narrate.mjs";
7
7
  import { ALL_PLATFORMS } from "../adapters/index.mjs";
8
8
  import { SUBMIT_OUTPUT, ANNOTATIONS } from "./_schemas.mjs";
9
9
  import {
10
- MAX_INPUT,
11
10
  pullByPlatform,
12
- uploadStamp,
13
- WINDOW_TYPE,
14
11
  } from "./_helpers.mjs";
15
- import { TERMS_VERSION, PRIVACY_VERSION, isRankedAck } from "../lib/constants.mjs";
16
12
 
17
13
  export const TOOL_DEF = {
18
14
  name: "tokenpull_submit",
19
15
  description:
20
- "Pull your LOCAL token usage from session logs AND publish it to the SigRank board in one call — the zero-paste flow. Reads the four canonical pillars (input, output, cacheCreate, cacheRead) per window from your local logs, computes the cascade, and submits each window to the board where it is re-scored server-side and tagged with the source platform. Requires a codename to publish; omit for a local preview only. Token-only — no prompt content is read or transmitted.",
16
+ "Pull your LOCAL token usage from session logs and compute the cascade per window — the zero-paste preview flow. Reads the four canonical pillars (input, output, cacheCreate, cacheRead) per window from your local logs and computes yield, leverage, velocity, class, and card. This is a PREVIEW-ONLY tool — it does not publish to the board. The board's /api/v1/ingest-paste endpoint now requires an authenticated Supabase session, which MCP tools do not carry. To publish to the leaderboard, use submit_verified (which signs and posts to /api/v1/snapshots via the enrolled-device path) or submit directly through the signalaf.com web UI. Token-only — no prompt content is read or transmitted.",
21
17
  annotations: {
22
- title: "Pull and submit tokens",
18
+ title: "Pull and preview tokens",
23
19
  ...ANNOTATIONS.destructiveHint,
24
20
  ...ANNOTATIONS.idempotentHint,
25
21
  ...ANNOTATIONS.openWorldHint,
@@ -30,13 +26,13 @@ export const TOOL_DEF = {
30
26
  codename: {
31
27
  type: "string",
32
28
  description:
33
- 'Operator codename to publish under on the leaderboard (e.g. "Iron Lotus"). Required to submit — omit for local preview only.',
29
+ 'Operator codename for the ranking card display (e.g. "Iron Lotus"). Optional — used only for the local preview card, not for board submission.',
34
30
  },
35
31
  window: {
36
32
  type: "string",
37
33
  enum: ["7d", "30d", "90d", "all"],
38
34
  description:
39
- 'Submit only this time window (default: all 4 windows). Use "7d" for recent activity or "all" for all-time ranking.',
35
+ 'Preview only this time window (default: all 4 windows). Use "7d" for recent activity or "all" for all-time ranking.',
40
36
  },
41
37
  platform: {
42
38
  type: "string",
@@ -49,15 +45,16 @@ export const TOOL_DEF = {
49
45
  };
50
46
 
51
47
  export async function handleTokenpullSubmit(args, ctx) {
52
- // Pull local usage, then publish each window's CANONICAL pillars to the board
53
- // (server re-scores). The board stays platform-agnostic via the 4 pillars; the
54
- // source platform rides along as a tag. Conversion already happened in the adapter.
48
+ // Pull local usage and compute the cascade per window. This tool is
49
+ // preview-only — the board's /api/v1/ingest-paste endpoint now requires
50
+ // an authenticated Supabase session, which MCP tools do not carry. Use
51
+ // submit_verified for authenticated board submission.
55
52
  const codename = String(args?.codename || "").trim();
56
53
  const platform = args?.platform || "claude";
57
54
 
58
55
  // MULTI: same combined cross-platform cascade as submit_verified. Includes Devin
59
56
  // (cloud, via tokscale). Aggregate every locally-detected platform's pillars per
60
- // window and publish as platform='multi'.
57
+ // window.
61
58
  if (platform === "multi") {
62
59
  const detected = [];
63
60
  for (const p of ALL_PLATFORMS) {
@@ -102,58 +99,17 @@ export async function handleTokenpullSubmit(args, ctx) {
102
99
  continue;
103
100
  const c = cascade(sum);
104
101
  const card = narrate(c, `${wk} multi`);
105
- if (!codename) {
106
- out.push({
107
- window: wk,
108
- pillars: sum,
109
- cascade: c,
110
- card,
111
- submission: { status: "not_submitted", reason: "codename_required" },
112
- });
113
- continue;
114
- }
115
- const rawPaste = `${sum.input} ${sum.output} ${sum.cacheCreate} ${sum.cacheRead}`;
116
- const windowType = WINDOW_TYPE[wk] || wk;
117
- const stamp = uploadStamp({
118
- codename,
119
- window: windowType,
120
- pillars: sum,
121
- platform: "multi",
122
- });
123
- const res = await ctx.doFetch(`${ctx.apiBase}/api/v1/ingest-paste`, {
124
- method: "POST",
125
- headers: {
126
- "content-type": "application/json",
127
- accept: "application/json",
128
- },
129
- body: JSON.stringify({
130
- codename,
131
- raw_paste: rawPaste,
132
- window_type: windowType,
133
- telemetry: { platform: { primary: "multi" } },
134
- consent_acknowledged: true,
135
- terms_version: TERMS_VERSION,
136
- privacy_version: PRIVACY_VERSION,
137
- ...stamp,
138
- }),
139
- });
140
- let ack;
141
- try {
142
- ack = await res.json();
143
- } catch {
144
- ack = { status: "parse_error", httpStatus: res.status };
145
- }
146
- // Fix 4: was `ack?.ranked ?? ack?.accepted ?? false` — a looser predicate
147
- // than the single-platform branch below, which would mark a non-persisted
148
- // 202 as ranked. Now both branches use the shared isRankedAck() so every
149
- // submit path agrees on what "ranked" means.
150
- const ranked = isRankedAck(res, ack);
151
102
  out.push({
152
103
  window: wk,
153
104
  pillars: sum,
154
105
  cascade: c,
155
106
  card,
156
- submission: { ...stamp, httpStatus: res.status, ranked, ...ack },
107
+ submission: {
108
+ status: "not_submitted",
109
+ reason: "preview_only",
110
+ detail:
111
+ "tokenpull_submit is preview-only. Use submit_verified (enrolled device) or the signalaf.com web UI to publish.",
112
+ },
157
113
  });
158
114
  }
159
115
  return {
@@ -172,58 +128,17 @@ export async function handleTokenpullSubmit(args, ctx) {
172
128
  for (const w of targets) {
173
129
  const c = cascade(w.pillars);
174
130
  const card = narrate(c, `${w.window} window`);
175
- if (!codename) {
176
- out.push({
177
- window: w.window,
178
- pillars: w.pillars,
179
- cascade: c,
180
- card,
181
- submission: { status: "not_submitted", reason: "codename_required" },
182
- });
183
- continue;
184
- }
185
- // canonical pillars → "input output cacheCreate cacheRead" (the parser's 4-bare-number form)
186
- const rawPaste = `${w.pillars.input} ${w.pillars.output} ${w.pillars.cacheCreate} ${w.pillars.cacheRead}`;
187
- const windowType = WINDOW_TYPE[w.window] || w.window;
188
- const stamp = uploadStamp({
189
- codename,
190
- window: windowType,
191
- pillars: w.pillars,
192
- platform: pulled.platform,
193
- });
194
- const res = await ctx.doFetch(`${ctx.apiBase}/api/v1/ingest-paste`, {
195
- method: "POST",
196
- headers: {
197
- "content-type": "application/json",
198
- accept: "application/json",
199
- },
200
- body: JSON.stringify({
201
- codename,
202
- raw_paste: rawPaste,
203
- window_type: windowType,
204
- telemetry: { platform: { primary: pulled.platform } },
205
- consent_acknowledged: true,
206
- terms_version: TERMS_VERSION,
207
- privacy_version: PRIVACY_VERSION,
208
- ...stamp,
209
- }),
210
- });
211
- let ack;
212
- try {
213
- ack = await res.json();
214
- } catch {
215
- ack = { status: "error", detail: `HTTP ${res.status} (non-JSON)` };
216
- }
217
- // ranked = actually on the board (verified + persisted), not just received — parity
218
- // with submit_verified (submit/index.mjs). An unenrolled/revoked device gets 202 but is NEVER ranked.
219
- const ranked = isRankedAck(res, ack);
220
131
  out.push({
221
132
  window: w.window,
222
133
  pillars: w.pillars,
223
134
  cascade: c,
224
135
  card,
225
- ranked,
226
- submission: { ...stamp, httpStatus: res.status, ranked, ...ack },
136
+ submission: {
137
+ status: "not_submitted",
138
+ reason: "preview_only",
139
+ detail:
140
+ "tokenpull_submit is preview-only. Use submit_verified (enrolled device) or the signalaf.com web UI to publish.",
141
+ },
227
142
  });
228
143
  }
229
144
  return {