sigrank 0.15.4 → 0.16.0

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/submit.mjs CHANGED
@@ -10,6 +10,7 @@
10
10
  // the display signa_rate reflects true session shape (the ranked Υ is already exact).
11
11
 
12
12
  import { snapshotHash, signPayload } from './sign.mjs'
13
+ import { preflight } from './preflight.mjs'
13
14
 
14
15
  const WINDOW_TYPE = { '7d': '7d', '30d': '30d', '90d': '90d', all: 'all_time' }
15
16
  const WINDOW_SPAN_DAYS = { '7d': 7, '30d': 30, '90d': 90, all_time: 3650 }
@@ -99,6 +100,14 @@ export function buildPayload(windowKey, pillars, messages, identity, platform, o
99
100
  public_key: identity.public_key,
100
101
  },
101
102
  }
103
+
104
+ // Cascade Report block (Phase 1 — mode detection + badges + health).
105
+ // Computed locally (pure math), submitted alongside the 4 token pillars.
106
+ // The server stores it as-is — does NOT recompute modes.
107
+ // Weekly granularity is the privacy boundary — daily modes never leave the machine.
108
+ if (opts.report && typeof opts.report === 'object') {
109
+ payload.report = opts.report
110
+ }
102
111
  payload.agent.snapshot_hash = snapshotHash(payload)
103
112
  return payload
104
113
  }
@@ -128,6 +137,33 @@ export async function submitSignedWindow(windowKey, pillars, messages, identity,
128
137
  return fetch(url, { ...init, signal: init.signal || ctrl.signal }).finally(() => clearTimeout(timer))
129
138
  })
130
139
  const payload = buildPayload(windowKey, pillars, messages, identity, opts.platform || 'claude', opts)
140
+
141
+ // Preflight: run the same anti-gaming checks the server will run, locally.
142
+ // If the payload would be rejected or downgraded, warn the operator BEFORE
143
+ // submitting. The server always runs its own checks — this is a preview.
144
+ const pre = preflight(payload)
145
+ if (!pre.pass && !opts.skipPreflight) {
146
+ if (pre.wouldReject) {
147
+ return {
148
+ status: 'preflight_rejected',
149
+ window: WINDOW_TYPE[windowKey] || windowKey,
150
+ preflight: pre,
151
+ detail: `Submission would be REJECTED by the server: ${pre.summary}. Fix the issue or re-run with skipPreflight.`,
152
+ }
153
+ }
154
+ // Flags downgrade verified → flagged (not ranked). Warn but don't block
155
+ // unless the caller explicitly asked for strict mode.
156
+ if (opts.strictPreflight) {
157
+ return {
158
+ status: 'preflight_flagged',
159
+ window: WINDOW_TYPE[windowKey] || windowKey,
160
+ preflight: pre,
161
+ detail: `Submission would be DOWNGRADED (not ranked): ${pre.summary}. Fix the issue or re-run with skipPreflight.`,
162
+ }
163
+ }
164
+ // Non-strict: attach the preflight warning to the result but proceed.
165
+ }
166
+
131
167
  const signature = signPayload(payload, identity.private_key_pkcs8_b64)
132
168
 
133
169
  // Dry run: build + sign exactly as a real publish, then STOP before the POST.
@@ -140,6 +176,7 @@ export async function submitSignedWindow(windowKey, pillars, messages, identity,
140
176
  would_post: `${apiBase}/api/v1/snapshots`,
141
177
  payload,
142
178
  signature,
179
+ preflight: pre,
143
180
  detail: 'Nothing sent. Re-run without dry_run to publish.',
144
181
  }
145
182
  }
@@ -177,5 +214,6 @@ export async function submitSignedWindow(windowKey, pillars, messages, identity,
177
214
  snapshot_hash: payload.agent.snapshot_hash,
178
215
  reason: res.ok ? null : ack.reason || ack.status || `http_${res.status}`,
179
216
  detail: ack.detail ?? null,
217
+ preflight: pre,
180
218
  }
181
219
  }
package/tokenpull.mjs CHANGED
@@ -164,6 +164,14 @@ export async function tokenpull({ adapter = claudeAdapter, root, now } = {}) {
164
164
 
165
165
  const windows = WINDOWS.map((w) => {
166
166
  const cutoff = w.days === Infinity ? -Infinity : nowMs - w.days * DAY_MS
167
+ // NULL-TIMESTAMP WINDOW ASYMMETRY: the `all` window returns true unconditionally,
168
+ // so records whose adapter cannot supply a timestamp (ts: null or unparseable)
169
+ // appear in all-time but disappear from 7d/30d/90d. This is intentional — we do
170
+ // not fabricate timestamps. Session-granularity adapters (goose/droid/hermes)
171
+ // typically have created_at/updated_at, but if those columns are absent the row
172
+ // is all-time-only. A file-mtime fallback was considered but rejected: mtime
173
+ // reflects the log file, not the session, and would misattribute old sessions
174
+ // to recent file-touch times.
167
175
  const inWin = msgs.filter((m) => {
168
176
  if (w.days === Infinity) return true
169
177
  const t = m.ts ? Date.parse(m.ts) : NaN
@@ -250,6 +258,8 @@ export async function tokenpullCodex({ adapter = codexAdapter, root, now, ioRati
250
258
 
251
259
  const windows = WINDOWS.map((w) => {
252
260
  const cutoff = w.days === Infinity ? -Infinity : nowMs - w.days * DAY_MS
261
+ // NULL-TIMESTAMP WINDOW ASYMMETRY: see tokenpull() above — same behavior.
262
+ // Records without a parseable ts appear in `all` only, not in 7d/30d/90d.
253
263
  const inWin = recs.filter((m) => {
254
264
  if (w.days === Infinity) return true
255
265
  const t = m.ts ? Date.parse(m.ts) : NaN
@@ -284,7 +294,9 @@ export async function tokenpullAny(platform, opts = {}) {
284
294
  let ioRatio = opts.ioRatio || 2.0
285
295
  if (!opts.ioRatio) {
286
296
  try {
287
- const c = await tokenpull({ adapter: claudeAdapter })
297
+ // Forward root/now from opts so the Beta-ratio derivation is testable
298
+ // (without this, it always reads the real ~/.claude and can't be injected).
299
+ const c = await tokenpull({ adapter: claudeAdapter, root: opts.root, now: opts.now })
288
300
  const all = c.windows.find((w) => w.window === 'all')
289
301
  if (all && all.pillars.output > 0) ioRatio = all.pillars.input / all.pillars.output
290
302
  } catch { /* no Claude data → Alpha 2.0 */ }