@manyoars/oars 0.2.0 → 0.3.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.
Files changed (241) hide show
  1. package/README.md +132 -28
  2. package/dist/api.js +1 -1
  3. package/dist/bin.js +6 -29
  4. package/dist/cli.d.ts +6 -3
  5. package/dist/cli.js +367 -88
  6. package/dist/commands/keys.d.ts +31 -0
  7. package/dist/commands/keys.js +65 -0
  8. package/dist/commands/login.js +2 -2
  9. package/dist/errors.d.ts +13 -13
  10. package/dist/errors.js +28 -11
  11. package/dist/gate/checks.d.ts +68 -0
  12. package/dist/gate/checks.js +584 -0
  13. package/dist/gate/command.d.ts +14 -0
  14. package/dist/gate/command.js +61 -0
  15. package/dist/gate/hook-fill-main.d.ts +1 -0
  16. package/dist/gate/hook-fill-main.js +16 -0
  17. package/dist/gate/hook-fill.d.ts +38 -0
  18. package/dist/gate/hook-fill.js +94 -0
  19. package/dist/gate/hook-main.d.ts +1 -0
  20. package/dist/gate/hook-main.js +35 -0
  21. package/dist/gate/hook-process.d.ts +14 -0
  22. package/dist/gate/hook-process.js +64 -0
  23. package/dist/gate/hook.d.ts +46 -0
  24. package/dist/gate/hook.js +256 -0
  25. package/dist/gate/install.d.ts +25 -0
  26. package/dist/gate/install.js +126 -0
  27. package/dist/gate/local-bin.d.ts +8 -0
  28. package/dist/gate/local-bin.js +36 -0
  29. package/dist/gate/notes.d.ts +19 -0
  30. package/dist/gate/notes.js +27 -0
  31. package/dist/gate/novelty.d.ts +35 -0
  32. package/dist/gate/novelty.js +99 -0
  33. package/dist/gate/policy.d.ts +25 -0
  34. package/dist/gate/policy.js +73 -0
  35. package/dist/gate/record.d.ts +120 -0
  36. package/dist/gate/record.js +125 -0
  37. package/dist/gate/registry-state.d.ts +10 -0
  38. package/dist/gate/registry-state.js +30 -0
  39. package/dist/gate/registry.d.ts +56 -0
  40. package/dist/gate/registry.js +94 -0
  41. package/dist/gate/replay.d.ts +20 -0
  42. package/dist/gate/replay.js +257 -0
  43. package/dist/gate/report.d.ts +50 -0
  44. package/dist/gate/report.js +89 -0
  45. package/dist/gate/scope.d.ts +7 -0
  46. package/dist/gate/scope.js +27 -0
  47. package/dist/gate/transcripts.d.ts +68 -0
  48. package/dist/gate/transcripts.js +232 -0
  49. package/dist/hook-bin.d.ts +1 -0
  50. package/dist/hook-bin.js +25 -0
  51. package/dist/issuer.d.ts +7 -1
  52. package/dist/issuer.js +18 -1
  53. package/dist/main.d.ts +1 -0
  54. package/dist/main.js +64 -0
  55. package/dist/oidc.js +1 -1
  56. package/dist/sessions/actions.d.ts +35 -0
  57. package/dist/sessions/actions.js +351 -0
  58. package/dist/sessions/allow.d.ts +14 -0
  59. package/dist/sessions/allow.js +51 -0
  60. package/dist/sessions/archive.d.ts +11 -0
  61. package/dist/sessions/archive.js +38 -0
  62. package/dist/sessions/audit.d.ts +42 -0
  63. package/dist/sessions/audit.js +104 -0
  64. package/dist/sessions/baseline.d.ts +62 -0
  65. package/dist/sessions/baseline.js +188 -0
  66. package/dist/sessions/capture-runtime.d.ts +10 -0
  67. package/dist/sessions/capture-runtime.js +12 -0
  68. package/dist/sessions/cell-increments.d.ts +43 -0
  69. package/dist/sessions/cell-increments.js +209 -0
  70. package/dist/sessions/cells.d.ts +79 -0
  71. package/dist/sessions/cells.js +181 -0
  72. package/dist/sessions/codex.d.ts +53 -0
  73. package/dist/sessions/codex.js +679 -0
  74. package/dist/sessions/command.d.ts +117 -0
  75. package/dist/sessions/command.js +373 -0
  76. package/dist/sessions/copilot.d.ts +40 -0
  77. package/dist/sessions/copilot.js +432 -0
  78. package/dist/sessions/cursor.d.ts +24 -0
  79. package/dist/sessions/cursor.js +324 -0
  80. package/dist/sessions/derive.d.ts +57 -0
  81. package/dist/sessions/derive.js +320 -0
  82. package/dist/sessions/exclusion.d.ts +81 -0
  83. package/dist/sessions/exclusion.js +353 -0
  84. package/dist/sessions/extract.d.ts +179 -0
  85. package/dist/sessions/extract.js +1774 -0
  86. package/dist/sessions/fold.d.ts +17 -0
  87. package/dist/sessions/fold.js +71 -0
  88. package/dist/sessions/frame.d.ts +24 -0
  89. package/dist/sessions/frame.js +95 -0
  90. package/dist/sessions/gemini.d.ts +37 -0
  91. package/dist/sessions/gemini.js +613 -0
  92. package/dist/sessions/gold-arms.d.ts +67 -0
  93. package/dist/sessions/gold-arms.js +172 -0
  94. package/dist/sessions/gold.d.ts +197 -0
  95. package/dist/sessions/gold.js +463 -0
  96. package/dist/sessions/graph.d.ts +12 -0
  97. package/dist/sessions/graph.js +34 -0
  98. package/dist/sessions/grok.d.ts +36 -0
  99. package/dist/sessions/grok.js +421 -0
  100. package/dist/sessions/hook-calls.d.ts +3 -0
  101. package/dist/sessions/hook-calls.js +47 -0
  102. package/dist/sessions/hook-index.d.ts +53 -0
  103. package/dist/sessions/hook-index.js +157 -0
  104. package/dist/sessions/hooks.d.ts +13 -0
  105. package/dist/sessions/hooks.js +170 -0
  106. package/dist/sessions/identity.d.ts +22 -0
  107. package/dist/sessions/identity.js +101 -0
  108. package/dist/sessions/install.d.ts +54 -0
  109. package/dist/sessions/install.js +201 -0
  110. package/dist/sessions/lineage.d.ts +19 -0
  111. package/dist/sessions/lineage.js +118 -0
  112. package/dist/sessions/mcp.d.ts +6 -0
  113. package/dist/sessions/mcp.js +153 -0
  114. package/dist/sessions/merkle.d.ts +7 -0
  115. package/dist/sessions/merkle.js +28 -0
  116. package/dist/sessions/motif.d.ts +43 -0
  117. package/dist/sessions/motif.js +59 -0
  118. package/dist/sessions/package-resolve.d.ts +30 -0
  119. package/dist/sessions/package-resolve.js +183 -0
  120. package/dist/sessions/pending.d.ts +34 -0
  121. package/dist/sessions/pending.js +142 -0
  122. package/dist/sessions/pool-command.d.ts +44 -0
  123. package/dist/sessions/pool-command.js +542 -0
  124. package/dist/sessions/pool-delta.d.ts +54 -0
  125. package/dist/sessions/pool-delta.js +190 -0
  126. package/dist/sessions/pool-outbox.d.ts +63 -0
  127. package/dist/sessions/pool-outbox.js +260 -0
  128. package/dist/sessions/pool-print.d.ts +26 -0
  129. package/dist/sessions/pool-print.js +68 -0
  130. package/dist/sessions/pool-wire.d.ts +225 -0
  131. package/dist/sessions/pool-wire.js +620 -0
  132. package/dist/sessions/pool.d.ts +32 -0
  133. package/dist/sessions/pool.js +164 -0
  134. package/dist/sessions/projection.d.ts +75 -0
  135. package/dist/sessions/projection.js +165 -0
  136. package/dist/sessions/public-vocabulary.d.ts +73 -0
  137. package/dist/sessions/public-vocabulary.js +302 -0
  138. package/dist/sessions/query.d.ts +111 -0
  139. package/dist/sessions/query.js +246 -0
  140. package/dist/sessions/read-adoption.d.ts +2 -0
  141. package/dist/sessions/read-adoption.js +27 -0
  142. package/dist/sessions/read-call-shape.d.ts +2 -0
  143. package/dist/sessions/read-call-shape.js +25 -0
  144. package/dist/sessions/read-change.d.ts +2 -0
  145. package/dist/sessions/read-change.js +30 -0
  146. package/dist/sessions/read-cost.d.ts +4 -0
  147. package/dist/sessions/read-cost.js +37 -0
  148. package/dist/sessions/read-day-move.d.ts +3 -0
  149. package/dist/sessions/read-day-move.js +65 -0
  150. package/dist/sessions/read-fit.d.ts +7 -0
  151. package/dist/sessions/read-fit.js +41 -0
  152. package/dist/sessions/read-interaction.d.ts +5 -0
  153. package/dist/sessions/read-interaction.js +62 -0
  154. package/dist/sessions/read-lead-lag.d.ts +15 -0
  155. package/dist/sessions/read-lead-lag.js +81 -0
  156. package/dist/sessions/read-live.d.ts +6 -0
  157. package/dist/sessions/read-live.js +38 -0
  158. package/dist/sessions/read-method.d.ts +2 -0
  159. package/dist/sessions/read-method.js +29 -0
  160. package/dist/sessions/read-next-step.d.ts +2 -0
  161. package/dist/sessions/read-next-step.js +35 -0
  162. package/dist/sessions/read-order.d.ts +54 -0
  163. package/dist/sessions/read-order.js +62 -0
  164. package/dist/sessions/read-readable.d.ts +8 -0
  165. package/dist/sessions/read-readable.js +24 -0
  166. package/dist/sessions/read-recovery.d.ts +20 -0
  167. package/dist/sessions/read-recovery.js +97 -0
  168. package/dist/sessions/read-rows.d.ts +13 -0
  169. package/dist/sessions/read-rows.js +47 -0
  170. package/dist/sessions/read-substitution.d.ts +5 -0
  171. package/dist/sessions/read-substitution.js +51 -0
  172. package/dist/sessions/read-support.d.ts +48 -0
  173. package/dist/sessions/read-support.js +73 -0
  174. package/dist/sessions/read-tool-day.d.ts +2 -0
  175. package/dist/sessions/read-tool-day.js +26 -0
  176. package/dist/sessions/read-version-boundary.d.ts +31 -0
  177. package/dist/sessions/read-version-boundary.js +95 -0
  178. package/dist/sessions/read.d.ts +79 -0
  179. package/dist/sessions/read.js +84 -0
  180. package/dist/sessions/recall-consequence.d.ts +45 -0
  181. package/dist/sessions/recall-consequence.js +179 -0
  182. package/dist/sessions/recall-object.d.ts +137 -0
  183. package/dist/sessions/recall-object.js +77 -0
  184. package/dist/sessions/recall-pool.d.ts +19 -0
  185. package/dist/sessions/recall-pool.js +96 -0
  186. package/dist/sessions/recall-push.d.ts +26 -0
  187. package/dist/sessions/recall-push.js +95 -0
  188. package/dist/sessions/recall-reads.d.ts +117 -0
  189. package/dist/sessions/recall-reads.js +410 -0
  190. package/dist/sessions/recall-render.d.ts +19 -0
  191. package/dist/sessions/recall-render.js +168 -0
  192. package/dist/sessions/redact.d.ts +6 -0
  193. package/dist/sessions/redact.js +54 -0
  194. package/dist/sessions/repo-root.d.ts +10 -0
  195. package/dist/sessions/repo-root.js +101 -0
  196. package/dist/sessions/schema.d.ts +644 -0
  197. package/dist/sessions/schema.js +148 -0
  198. package/dist/sessions/session-shape.d.ts +73 -0
  199. package/dist/sessions/session-shape.js +342 -0
  200. package/dist/sessions/sources.d.ts +50 -0
  201. package/dist/sessions/sources.js +159 -0
  202. package/dist/sessions/steps.d.ts +105 -0
  203. package/dist/sessions/steps.js +250 -0
  204. package/dist/sessions/store.d.ts +297 -0
  205. package/dist/sessions/store.js +1478 -0
  206. package/dist/sessions/strata.d.ts +26 -0
  207. package/dist/sessions/strata.js +77 -0
  208. package/dist/sessions/survival.d.ts +25 -0
  209. package/dist/sessions/survival.js +230 -0
  210. package/dist/sessions/verify.d.ts +53 -0
  211. package/dist/sessions/verify.js +286 -0
  212. package/dist/sessions/vocab-load.d.ts +26 -0
  213. package/dist/sessions/vocab-load.js +61 -0
  214. package/dist/sessions/vscode-copilot.d.ts +38 -0
  215. package/dist/sessions/vscode-copilot.js +477 -0
  216. package/package.json +4 -5
  217. package/vocab/crates.json +1 -0
  218. package/vocab/go.json +1 -0
  219. package/vocab/harness.json +1 -0
  220. package/vocab/homebrew.json +1 -0
  221. package/vocab/manifest.json +1 -0
  222. package/vocab/maven.json +1 -0
  223. package/vocab/models.json +1 -0
  224. package/vocab/npm.json +1 -0
  225. package/vocab/programs.json +1 -0
  226. package/vocab/pypi.json +1 -0
  227. package/vocab/rubygems.json +1 -0
  228. package/dist/commands/deploy-input.d.ts +0 -38
  229. package/dist/commands/deploy-input.js +0 -150
  230. package/dist/commands/deploy.d.ts +0 -22
  231. package/dist/commands/deploy.js +0 -124
  232. package/dist/commands/status.d.ts +0 -7
  233. package/dist/commands/status.js +0 -18
  234. package/dist/commands/templates.d.ts +0 -11
  235. package/dist/commands/templates.js +0 -32
  236. package/dist/commands/test-charge.d.ts +0 -11
  237. package/dist/commands/test-charge.js +0 -34
  238. package/dist/journal.d.ts +0 -26
  239. package/dist/journal.js +0 -58
  240. package/parity/deploy-plans/paid-api.json +0 -34
  241. package/parity/deploy-plans/per-task-agent.json +0 -34
@@ -0,0 +1,246 @@
1
+ // The read side: recall, books, and measure. All three are pure functions of a loaded store (and, for measure, the MCP log).
2
+ import { firstCopies } from './derive.js';
3
+ import { errorKey } from './extract.js';
4
+ import { makeRedactor } from './redact.js';
5
+ import { SOURCES } from './sources.js';
6
+ import { harnessOf } from './schema.js';
7
+ import { signatureRecord } from './store.js';
8
+ const DAY_MS = 86_400_000;
9
+ const sinceCutoff = (now, days) => (days === undefined ? undefined : new Date(now - days * DAY_MS).toISOString());
10
+ /** `Bash | text` pasted whole selects the tool too. */
11
+ const splitTool = (query) => {
12
+ const found = /^([^\s|]+) \| ([\s\S]*)$/.exec(query.trim());
13
+ return found === null ? { tool: undefined, text: query } : { tool: found[1], text: found[2] ?? '' };
14
+ };
15
+ const touched = (session, path) => session.files.some((file) => file === path || file.endsWith(`/${path}`) || file.endsWith(`\\${path}`));
16
+ /** Which error rows a query selects, and the normalized text it matched them on. A count stated for a narrowed query reads through this, so it counts what the query narrowed to. */
17
+ export const recallRows = (store, query, now, redactDomains = []) => {
18
+ const redact = makeRedactor(redactDomains);
19
+ const cutoff = sinceCutoff(now, query.sinceDays);
20
+ let tool;
21
+ let normalized = null;
22
+ if (query.error !== undefined) {
23
+ const split = splitTool(query.error);
24
+ tool = split.tool;
25
+ normalized = errorKey(split.text, redact);
26
+ }
27
+ const path = query.path === undefined ? undefined : redact(query.path);
28
+ const sessions = path === undefined ? undefined : new Set(store.sessions.filter((session) => touched(session, path)).map((session) => session.session));
29
+ const keep = (error) => {
30
+ if (cutoff !== undefined && (error.ts === null || error.ts < cutoff))
31
+ return false;
32
+ if (query.cls !== undefined && error.cls !== query.cls)
33
+ return false;
34
+ if (sessions !== undefined && !sessions.has(error.session))
35
+ return false;
36
+ if (tool !== undefined && error.tool !== tool)
37
+ return false;
38
+ if (normalized === null)
39
+ return true;
40
+ return normalized !== '' && error.sig.slice(error.sig.indexOf(' | ') + 3).includes(normalized);
41
+ };
42
+ return { normalized, keep };
43
+ };
44
+ export const recall = (store, query, now, limit = 10, redactDomains = []) => {
45
+ const { normalized, keep } = recallRows(store, query, now, redactDomains);
46
+ const bySig = new Map();
47
+ for (const error of store.errors) {
48
+ if (keep(error))
49
+ bySig.set(error.sig, [...(bySig.get(error.sig) ?? []), error]);
50
+ }
51
+ const matches = [...bySig.entries()]
52
+ .map(([sig, list]) => signatureRecord(sig, list))
53
+ .sort((a, b) => b.sessions - a.sessions || b.count - a.count || a.sig.localeCompare(b.sig))
54
+ .slice(0, limit)
55
+ .map((record) => ({ ...record, followedBy: Object.entries(record.following).map(([followed, count]) => ({ tool: followed, count })).sort((a, b) => b.count - a.count || a.tool.localeCompare(b.tool)) }));
56
+ return { query, normalized, matches };
57
+ };
58
+ export const formatRecall = (result) => {
59
+ if (result.matches.length === 0)
60
+ return 'No matching signature in the store.';
61
+ return result.matches
62
+ .map((match) => {
63
+ const followed = match.followedBy.slice(0, 3).map((entry) => `${entry.tool} x${String(entry.count)}`).join(', ');
64
+ return [
65
+ match.sig,
66
+ ` ${String(match.sessions)} sessions, ${String(match.count)} errors, first ${match.firstSeen ?? 'unknown'}, last ${match.lastSeen ?? 'unknown'}`,
67
+ ` harness ${match.harnessVersions.join(', ') || 'unknown'}; followed by ${followed || 'nothing'}; an edit followed ${String(match.editFollowed)} times`,
68
+ ].join('\n');
69
+ })
70
+ .join('\n');
71
+ };
72
+ /** Below this many in its denominator a share is noise, so a cell has none and prints as too few. The counts stay in `--json`. */
73
+ export const BOOKS_MIN_PROMPTS = 30;
74
+ /** Each subagent's tokens, keyed by the parent prompt that spawned it (`<session>#<index>`). */
75
+ const delegatedByPrompt = (store) => {
76
+ const promptOf = new Map();
77
+ for (const tool of store.tools) {
78
+ if (tool.toolUseId !== null && tool.prompt !== null)
79
+ promptOf.set(`${tool.session} ${tool.toolUseId}`, `${tool.session}#${String(tool.prompt)}`);
80
+ }
81
+ const totals = new Map();
82
+ for (const agent of store.subagents) {
83
+ const prompt = promptOf.get(`${String(agent.parent)} ${String(agent.spawnedBy)}`);
84
+ if (prompt === undefined)
85
+ continue;
86
+ const { input, cacheWrite, cacheRead, output } = agent.usage;
87
+ totals.set(prompt, (totals.get(prompt) ?? 0) + input + cacheWrite + cacheRead + output);
88
+ }
89
+ return totals;
90
+ };
91
+ const promptKey = (session, index) => `${session}#${String(index)}`;
92
+ const shareOf = (count, of) => ({ count, of, share: of < BOOKS_MIN_PROMPTS ? null : Math.round((count / of) * 1000) / 1000 });
93
+ /** Each prompt's shas and pulls, by the rule `commits` is derived under: a session's first copy of a call, off the sidechain. */
94
+ const callsByPrompt = (store) => {
95
+ const bySession = new Map();
96
+ for (const tool of store.tools) {
97
+ const list = bySession.get(tool.session);
98
+ if (list === undefined)
99
+ bySession.set(tool.session, [tool]);
100
+ else
101
+ list.push(tool);
102
+ }
103
+ const calls = new Map();
104
+ for (const tools of bySession.values()) {
105
+ for (const tool of firstCopies(tools)) {
106
+ if (tool.prompt === null || (tool.commits === undefined && tool.pulls === undefined))
107
+ continue;
108
+ const key = promptKey(tool.session, tool.prompt);
109
+ const entry = calls.get(key) ?? { shas: new Set(), pulls: new Set() };
110
+ for (const sha of tool.commits ?? [])
111
+ if (sha !== null)
112
+ entry.shas.add(sha);
113
+ for (const pull of tool.pulls ?? [])
114
+ entry.pulls.add(`${pull.ecosystem}:${pull.name}`);
115
+ calls.set(key, entry);
116
+ }
117
+ }
118
+ return calls;
119
+ };
120
+ export const books = (store, by, now, sinceDays, survival = new Map()) => {
121
+ const cutoff = sinceCutoff(now, sinceDays);
122
+ const groups = new Map();
123
+ const harnesses = new Map(store.sessions.map((session) => [session.session, harnessOf(session)]));
124
+ const calls = callsByPrompt(store);
125
+ const keysOf = (prompt) => {
126
+ if (by === 'pull')
127
+ return calls.get(promptKey(prompt.session, prompt.index))?.pulls ?? [];
128
+ return [by === 'class' ? prompt.cls : by === 'model' ? prompt.model : (harnesses.get(prompt.session) ?? 'claude-code')];
129
+ };
130
+ for (const prompt of store.prompts) {
131
+ if (cutoff !== undefined && (prompt.ts === null || prompt.ts < cutoff))
132
+ continue;
133
+ for (const key of keysOf(prompt)) {
134
+ const group = groups.get(key);
135
+ if (group === undefined)
136
+ groups.set(key, [prompt]);
137
+ else
138
+ group.push(prompt);
139
+ }
140
+ }
141
+ const sum = (list, pick) => list.reduce((total, prompt) => total + pick(prompt), 0);
142
+ const delegated = delegatedByPrompt(store);
143
+ return [...groups.entries()]
144
+ .map(([key, list]) => {
145
+ const verified = list.filter((prompt) => prompt.cls === 'edit-verify').length;
146
+ const edited = verified + list.filter((prompt) => prompt.cls === 'edit-only').length;
147
+ const readable = list.filter((prompt) => prompt.verify !== undefined && prompt.verify !== 'na' && prompt.verify !== 'masked');
148
+ const carryingStop = list.filter((prompt) => prompt.stopped !== undefined);
149
+ const carryingRedo = list.filter((prompt) => prompt.redo !== undefined);
150
+ const states = [...new Set(list.flatMap((prompt) => [...(calls.get(promptKey(prompt.session, prompt.index))?.shas ?? [])]))].map((sha) => survival.get(sha));
151
+ const settled = states.filter((state) => state !== undefined && state !== 'unknown').length;
152
+ return {
153
+ key,
154
+ prompts: list.length,
155
+ cacheRead: sum(list, (prompt) => prompt.usage.cacheRead),
156
+ cacheWrite: sum(list, (prompt) => prompt.usage.cacheWrite),
157
+ output: sum(list, (prompt) => prompt.usage.output),
158
+ thinking: sum(list, (prompt) => prompt.usage.thinking),
159
+ wallSec: Math.round(sum(list, (prompt) => prompt.wallSec ?? 0)),
160
+ turns: sum(list, (prompt) => prompt.turns),
161
+ errors: sum(list, (prompt) => prompt.errors),
162
+ verifyAfterEdit: edited === 0 ? null : Math.round((verified / edited) * 1000) / 1000,
163
+ delegated: sum(list, (prompt) => delegated.get(promptKey(prompt.session, prompt.index)) ?? 0),
164
+ verifyPassed: shareOf(readable.filter((prompt) => prompt.verify === 'passed').length, readable.length),
165
+ stopped: shareOf(carryingStop.filter((prompt) => prompt.stopped !== null).length, carryingStop.length),
166
+ redone: shareOf(carryingRedo.filter((prompt) => prompt.redo === true).length, carryingRedo.length),
167
+ commits: sum(list, (prompt) => prompt.commits ?? 0),
168
+ settled,
169
+ survived: shareOf(states.filter((state) => state === 'survived').length, settled),
170
+ };
171
+ })
172
+ .sort((a, b) => b.prompts - a.prompts || a.key.localeCompare(b.key));
173
+ };
174
+ /** The closing line of `books` text: what the columns can and cannot be read as. */
175
+ export const BOOKS_CAVEAT = "These are associations in one operator's logs, not measured lift.";
176
+ const shareCell = (cell) => (cell.share === null ? 'too few' : `${String(Math.round(cell.share * 100))}%`);
177
+ export const formatBooks = (rows, by) => {
178
+ if (rows.length === 0)
179
+ return by === 'pull' ? 'No pulls in the store.' : 'No prompts in the store.';
180
+ // A pull name, unlike a class, has no length it stays under.
181
+ const width = Math.max(14, ...rows.map((row) => row.key.length));
182
+ const header = `${by.padEnd(width)} ${'prompts'.padStart(8)} ${'cache read'.padStart(14)} ${'cache write'.padStart(13)} ${'output'.padStart(11)} ${'thinking'.padStart(10)} ${'wall s'.padStart(9)} ${'turns'.padStart(7)} ${'errors'.padStart(7)} ${'verify'.padStart(7)} ${'delegated'.padStart(13)} ${'passed'.padStart(8)} ${'stop'.padStart(8)} ${'redo'.padStart(8)} ${'commits'.padStart(8)} ${'settled'.padStart(8)} ${'survived'.padStart(8)}`;
183
+ const lines = rows.map((row) => `${row.key.padEnd(width)} ${String(row.prompts).padStart(8)} ${String(row.cacheRead).padStart(14)} ${String(row.cacheWrite).padStart(13)} ${String(row.output).padStart(11)} ${String(row.thinking).padStart(10)} ${String(row.wallSec).padStart(9)} ${String(row.turns).padStart(7)} ${String(row.errors).padStart(7)} ${(row.verifyAfterEdit === null ? '-' : `${String(Math.round(row.verifyAfterEdit * 100))}%`).padStart(7)} ${String(row.delegated).padStart(13)} ${shareCell(row.verifyPassed).padStart(8)} ${shareCell(row.stopped).padStart(8)} ${shareCell(row.redone).padStart(8)} ${String(row.commits).padStart(8)} ${String(row.settled).padStart(8)} ${shareCell(row.survived).padStart(8)}`);
184
+ return [header, ...lines].join('\n');
185
+ };
186
+ const order = (a, b) => (a.tsFirst ?? '').localeCompare(b.tsFirst ?? '') || a.session.localeCompare(b.session);
187
+ /**
188
+ * Without `since` the current window is the last `window` sessions, so it moves with every deposit. With it, the current window is the
189
+ * first `window` sessions that started at or after the cutoff and the prior window is the `window` before it, so a reading pinned to an
190
+ * install time keeps its start however many sessions come later.
191
+ */
192
+ export const measure = (store, calls, hookCalls, window, since) => {
193
+ const sessions = [...store.sessions].sort(order);
194
+ const index = new Map(sessions.map((session, position) => [session.session, position]));
195
+ const firstSeen = new Map();
196
+ for (const error of store.errors) {
197
+ const position = index.get(error.session);
198
+ if (position !== undefined)
199
+ firstSeen.set(error.sig, Math.min(firstSeen.get(error.sig) ?? position, position));
200
+ }
201
+ const signal = (slice) => {
202
+ if (slice.length === 0)
203
+ return { sessions: 0, cursorSessions: 0, recurringPer100: null, recurringSignatures: null, cacheWritePerSession: null, recallPerSession: null, hookRecallPerSession: null, hookHitsPerSession: null, hookDepositsPerSession: null };
204
+ const names = new Set(slice.map((session) => session.session));
205
+ const recurringErrors = store.errors.filter((error) => names.has(error.session) && (index.get(error.session) ?? 0) > (firstSeen.get(error.sig) ?? 0));
206
+ const recurring = recurringErrors.length;
207
+ const from = slice.map((session) => session.tsFirst).filter((ts) => ts !== null).sort()[0];
208
+ const to = slice.map((session) => session.tsLast).filter((ts) => ts !== null).sort().reverse()[0];
209
+ const inWindow = from === undefined || to === undefined ? 0 : calls.filter((call) => call.at >= from && call.at <= to).length;
210
+ const round = (value) => Math.round(value * 100) / 100;
211
+ const lookups = hookCalls.filter((call) => call.session !== null && names.has(call.session) && (call.event === 'session-start' || call.event === 'post-tool'));
212
+ return {
213
+ sessions: slice.length,
214
+ cursorSessions: slice.filter((session) => harnessOf(session) === 'cursor').length,
215
+ otherSessions: Object.fromEntries(SOURCES.filter((source) => source.harness !== 'cursor').map((source) => [source.harness, slice.filter((session) => harnessOf(session) === source.harness).length])),
216
+ recurringPer100: round((recurring * 100) / slice.length),
217
+ recurringSignatures: new Set(recurringErrors.map((error) => error.sig)).size,
218
+ cacheWritePerSession: Math.round(slice.reduce((total, session) => total + session.usage.cacheWrite, 0) / slice.length),
219
+ recallPerSession: round(inWindow / slice.length),
220
+ hookRecallPerSession: round(lookups.length / slice.length),
221
+ hookHitsPerSession: round(lookups.filter((call) => call.hit).length / slice.length),
222
+ hookDepositsPerSession: round(hookCalls.filter((call) => call.session !== null && names.has(call.session) && call.event === 'stop' && call.hit).length / slice.length),
223
+ };
224
+ };
225
+ const start = since === undefined ? Math.max(sessions.length - window, 0) : sessions.findIndex((session) => (session.tsFirst ?? '') >= since);
226
+ if (start < 0)
227
+ return { window, since: since ?? null, last: signal([]), before: signal(sessions.slice(Math.max(sessions.length - window, 0))) };
228
+ return { window, since: since ?? null, last: signal(sessions.slice(start, start + window)), before: signal(sessions.slice(Math.max(start - window, 0), start)) };
229
+ };
230
+ export const formatMeasure = (result) => {
231
+ const cell = (value) => (value === null ? '-' : String(value));
232
+ const row = (label, pick) => `${label.padEnd(36)} ${cell(pick(result.last)).padStart(12)} ${cell(pick(result.before)).padStart(12)}`;
233
+ return [
234
+ `${(result.since === null ? `G1 signals, last ${String(result.window)} sessions and the ${String(result.window)} before` : `G1 signals, ${String(result.window)} sessions from ${result.since} and the ${String(result.window)} before`).padEnd(36)} ${(result.since === null ? 'last' : 'from').padStart(12)} ${'before'.padStart(12)}`,
235
+ row('sessions in the window', (signal) => signal.sessions),
236
+ row(' of which Cursor', (signal) => signal.cursorSessions),
237
+ ...SOURCES.filter((source) => source.harness !== 'cursor' && ((result.last.otherSessions?.[source.harness] ?? 0) > 0 || (result.before.otherSessions?.[source.harness] ?? 0) > 0)).map((source) => row(` of which ${source.label}`, (signal) => signal.otherSessions?.[source.harness] ?? 0)),
238
+ row('recurring errors per 100 sessions', (signal) => signal.recurringPer100),
239
+ row('distinct recurring signatures', (signal) => signal.recurringSignatures),
240
+ row('cache-write tokens per session', (signal) => signal.cacheWritePerSession),
241
+ row('recall calls per session', (signal) => signal.recallPerSession),
242
+ row('hook recall calls per session', (signal) => signal.hookRecallPerSession),
243
+ row('hook hits per session', (signal) => signal.hookHitsPerSession),
244
+ row('hook deposits per session', (signal) => signal.hookDepositsPerSession),
245
+ ].join('\n');
246
+ };
@@ -0,0 +1,2 @@
1
+ import { type DeriveFamily } from './read-support.js';
2
+ export declare const deriveAdoption: DeriveFamily;
@@ -0,0 +1,27 @@
1
+ // Adoption (rank 6): when a tool was first and last seen (as the Monday of its UTC week, the grain of an index cell, rule 6), in how many weeks, and its share of all steps. Confounders: the operator's own rules move these, and
2
+ // a program name can be noise.
3
+ import { UNKNOWN, weekStartOf } from './cells.js';
4
+ import { motifName } from './motif.js';
5
+ import { body, firstDayOf, groupBy, lastDayOf, rate, settled, shapeUnits, total, unitCells, wilson } from './read-support.js';
6
+ const WEEK_MS = 7 * 86_400_000;
7
+ const weeksBetween = (from, to) => (from === null || to === null ? null : Math.floor((Date.parse(weekStartOf(to)) - Date.parse(from)) / WEEK_MS));
8
+ export const deriveAdoption = ({ pool }) => {
9
+ const of = total(shapeUnits(pool, 'week'), settled);
10
+ return [...groupBy(unitCells(pool, 'week'), ({ cell }) => cell.motifId).values()].flatMap((group) => {
11
+ const [first] = group;
12
+ const cells = group.map(({ cell }) => cell);
13
+ const n = total(cells, settled);
14
+ if (first === undefined || n === 0)
15
+ return [];
16
+ const weeks = cells.map((cell) => cell.day).filter((week) => week !== UNKNOWN);
17
+ const lastWeek = lastDayOf(weeks);
18
+ return [
19
+ body('adoption', cells, {
20
+ slice: { motif: motifName(first.motif), level: first.motif.level },
21
+ value: { share: rate(n, of), of, firstWeek: firstDayOf(weeks), lastWeek, weeks: new Set(weeks).size, weeksSinceLast: weeksBetween(lastWeek, pool.lastDay) },
22
+ n,
23
+ interval: wilson(n, of),
24
+ }),
25
+ ];
26
+ });
27
+ };
@@ -0,0 +1,2 @@
1
+ import { type DeriveFamily } from './read-support.js';
2
+ export declare const deriveCallShape: DeriveFamily;
@@ -0,0 +1,25 @@
1
+ // Call shape (the fit family's other method): one tool and its input key names, over the floors a shape needs before a hook may push it. A shape is what a pre-tool hook can
2
+ // compute from the call it is about to run, so this read is what a push cites. A failure is not in a cell by shape, so its evidence is the failed one-step cells of the
3
+ // tool's action on the days the shape failed. Same confounders as `read-fit.ts`: a failure is not a misfit, and prompt class is not adjusted for.
4
+ import { shapeMotif } from './motif.js';
5
+ import { CALL_SHAPE_METHOD } from './read.js';
6
+ import { actionOfShape, fitReads, toolShapeId } from './recall-reads.js';
7
+ import { body, atGrain, rate, wilson } from './read-support.js';
8
+ export const deriveCallShape = ({ pool, store }) => fitReads(store).flatMap((read) => {
9
+ const action = actionOfShape(read.tool, read.harnesses);
10
+ const motifId = shapeMotif([{ action, identity: null }]).motifId;
11
+ const within = (day) => read.firstDay !== null && read.lastDay !== null && day >= read.firstDay && day <= read.lastDay;
12
+ const cells = atGrain(pool, 'day').filter((cell) => cell.motifId === motifId && cell.failed > 0 && within(cell.day));
13
+ // A shape whose failures are in no cell has nothing to cite, so it is not a read: its answers keep their stand-in.
14
+ if (cells.length === 0)
15
+ return [];
16
+ const calls = read.calls ?? read.count;
17
+ return [
18
+ body('fit', cells, {
19
+ slice: { shape: toolShapeId(read.tool, read.keys), action },
20
+ value: { failureRate: rate(read.count, calls), failed: read.count, calls },
21
+ n: read.count,
22
+ interval: wilson(read.count, calls),
23
+ }, CALL_SHAPE_METHOD),
24
+ ];
25
+ });
@@ -0,0 +1,2 @@
1
+ import { type DeriveFamily } from './read-support.js';
2
+ export declare const deriveChange: DeriveFamily;
@@ -0,0 +1,30 @@
1
+ // Change (rank 4), at order 1: each tool's share and failure rate inside one harness version bucket. What moved between two buckets is a relation between
2
+ // these reads, so it is order 2 (spec Section 6) and composes on them. Confounders: effort, role by brief, rule commits, class drift, and date.
3
+ import { bucketFields, UNKNOWN } from './cells.js';
4
+ import { motifName } from './motif.js';
5
+ import { body, firstDayOf, groupBy, lastDayOf, rate, settled, shapeUnits, total, unitCells, wilson } from './read-support.js';
6
+ const scopeOf = (cell) => {
7
+ const { harness, harnessBucket } = bucketFields(cell.frameBucket);
8
+ return `${harness}/${harnessBucket}`;
9
+ };
10
+ export const deriveChange = ({ pool }) => {
11
+ const steps = new Map([...groupBy(shapeUnits(pool, 'week'), scopeOf)].map(([scope, cells]) => [scope, total(cells, settled)]));
12
+ return [...groupBy(unitCells(pool, 'week'), ({ cell }) => `${cell.motifId}/${scopeOf(cell)}`).values()].flatMap((group) => {
13
+ const [first] = group;
14
+ const cells = group.map(({ cell }) => cell);
15
+ const n = total(cells, settled);
16
+ if (first === undefined || n === 0)
17
+ return [];
18
+ const of = steps.get(scopeOf(first.cell)) ?? 0;
19
+ const { harness, harnessBucket } = bucketFields(first.cell.frameBucket);
20
+ const weeks = cells.map((cell) => cell.day).filter((week) => week !== UNKNOWN);
21
+ return [
22
+ body('change', cells, {
23
+ slice: { motif: motifName(first.motif), level: first.motif.level, harness, harnessBucket },
24
+ value: { share: rate(n, of), of, failureRate: rate(total(cells, (cell) => cell.failed), n), firstWeek: firstDayOf(weeks), lastWeek: lastDayOf(weeks) },
25
+ n,
26
+ interval: wilson(n, of),
27
+ }),
28
+ ];
29
+ });
30
+ };
@@ -0,0 +1,4 @@
1
+ import { type DeriveFamily } from './read-support.js';
2
+ /** The ISO 8601 week of a UTC day: the week belongs to the year of its Thursday. */
3
+ export declare const isoWeek: (day: string) => string;
4
+ export declare const deriveCost: DeriveFamily;
@@ -0,0 +1,37 @@
1
+ // Cost (rank 3): tokens and wall time per prompt, by prompt class, model, and ISO week. Confounders: class drift, subagent work outside the prompt's own usage, and
2
+ // task size, which nothing here can see. Tokens are plain counts, not priced. A prompt with no wall time adds no seconds and stays in the denominator, so seconds per prompt understates.
3
+ import { bucketFields, EFFORT_CLASS, UNKNOWN } from './cells.js';
4
+ import { motifName } from './motif.js';
5
+ import { body, atGrain, groupBy, rate, settled, total } from './read-support.js';
6
+ const DAY_MS = 86_400_000;
7
+ /** The ISO 8601 week of a UTC day: the week belongs to the year of its Thursday. */
8
+ export const isoWeek = (day) => {
9
+ const millis = Date.parse(day);
10
+ const weekday = (new Date(millis).getUTCDay() + 6) % 7;
11
+ const thursday = new Date(millis - weekday * DAY_MS + 3 * DAY_MS);
12
+ const year = thursday.getUTCFullYear();
13
+ const week = Math.floor((thursday.getTime() - Date.UTC(year, 0, 1)) / (7 * DAY_MS)) + 1;
14
+ return `${String(year)}-W${String(week).padStart(2, '0')}`;
15
+ };
16
+ const weekOf = (cell) => (cell.day === UNKNOWN ? UNKNOWN : isoWeek(cell.day));
17
+ export const deriveCost = ({ pool }) => {
18
+ const classed = atGrain(pool, 'week').flatMap((cell) => {
19
+ const motif = pool.motifs.get(cell.motifId);
20
+ return motif?.level === 'class' && !motif.cls?.startsWith(EFFORT_CLASS) ? [{ cell, motif, model: bucketFields(cell.frameBucket).model }] : [];
21
+ });
22
+ return [...groupBy(classed, ({ cell, model }) => `${cell.motifId}/${model}/${weekOf(cell)}`).values()].flatMap((group) => {
23
+ const [first] = group;
24
+ if (first === undefined)
25
+ return [];
26
+ const cells = group.map(({ cell }) => cell);
27
+ const prompts = total(cells, settled);
28
+ return [
29
+ body('cost', cells, {
30
+ slice: { class: motifName(first.motif), model: first.model, week: weekOf(first.cell) },
31
+ value: { tokensPerPrompt: Math.round(total(cells, (cell) => cell.tokens) / prompts), secondsPerPrompt: Math.round(total(cells, (cell) => cell.durationMs) / prompts / 1000), errorShare: rate(total(cells, (cell) => cell.failed), prompts) },
32
+ n: prompts,
33
+ interval: null,
34
+ }),
35
+ ];
36
+ });
37
+ };
@@ -0,0 +1,3 @@
1
+ import { type DeriveRelation } from './read-order.js';
2
+ export declare const nextDay: (day: string) => string;
3
+ export declare const deriveDayMove: DeriveRelation;
@@ -0,0 +1,65 @@
1
+ // Day move (spec Section 6, order 2): a tool's failure rate from one calendar day to the next, for a tool above the floor of calls on both, and its share, for a tool on either
2
+ // of two days above the floor of steps. Unlike the other order 2 families this is not screened: it is the input lead-lag stands on, and keeping only the large moves, or only the
3
+ // tools still called after a fall, would select that read on its outcome. So a row here is not a finding.
4
+ import { UNKNOWN } from './cells.js';
5
+ import { differenceOf, estimateOf, relate } from './read-order.js';
6
+ import { groupBy, wilson } from './read-support.js';
7
+ const DAY_MS = 86_400_000;
8
+ const ISO_DAY_LENGTH = 10;
9
+ export const nextDay = (day) => new Date(Date.parse(`${day}T00:00:00Z`) + DAY_MS).toISOString().slice(0, ISO_DAY_LENGTH);
10
+ const stepsOf = (read) => {
11
+ const { steps } = read.value;
12
+ return typeof steps === 'number' ? steps : 0;
13
+ };
14
+ /** A tool's share on a day, measured against that day's steps. With no read of the tool there, it is zero of them, and that is a measurement, not a gap. The read's interval is on its failure rate, so a share's comes from its counts. */
15
+ const shareOn = (tool, witness) => {
16
+ const interval = wilson(tool?.n ?? 0, stepsOf(witness));
17
+ const value = tool === undefined ? 0 : tool.value.share;
18
+ return typeof value !== 'number' || interval === null ? undefined : { value, interval };
19
+ };
20
+ const sliceOf = (tool, metric, from, to) => ({ motif: tool.slice.motif ?? UNKNOWN, level: tool.slice.level ?? UNKNOWN, metric, from, to });
21
+ const moveOver = (before, after) => {
22
+ const move = differenceOf(after, before);
23
+ return { value: { before: before.value, after: after.value, move: move.value }, interval: move.interval };
24
+ };
25
+ /** Held to the tool's calls on both days: a failure rate is a rate of those calls. */
26
+ const failureMove = (earlier, later, from, to) => {
27
+ const before = estimateOf(earlier, 'failureRate');
28
+ const after = estimateOf(later, 'failureRate');
29
+ if (before === undefined || after === undefined)
30
+ return [];
31
+ const read = relate('day-move', [earlier, later], { slice: sliceOf(earlier, 'failureRate', from, to), ...moveOver(before, after) });
32
+ return read === undefined ? [] : [read];
33
+ };
34
+ /** Held to each day's steps and not the tool's calls: a floor on the calls would drop a tool for falling, which is the outcome a read over these moves asks about. */
35
+ const shareMove = (tool, sides, from, to) => {
36
+ const [earlier, later] = sides;
37
+ const before = shareOn(earlier.tool, earlier.witness);
38
+ const after = shareOn(later.tool, later.witness);
39
+ if (before === undefined || after === undefined)
40
+ return [];
41
+ const read = relate('day-move', [earlier.tool ?? earlier.witness, later.tool ?? later.witness], {
42
+ slice: sliceOf(tool, 'share', from, to),
43
+ ...moveOver(before, after),
44
+ trials: stepsOf,
45
+ });
46
+ return read === undefined ? [] : [read];
47
+ };
48
+ export const deriveDayMove = (reads) => {
49
+ const toolDays = reads.filter((read) => read.family === 'tool-day');
50
+ const witnesses = new Map([...groupBy(toolDays, (read) => read.slice.day ?? UNKNOWN)].flatMap(([day, [witness]]) => (witness === undefined ? [] : [[day, witness]])));
51
+ return [...groupBy(toolDays, (read) => read.slice.motif)].flatMap(([, group]) => {
52
+ const byDay = new Map(group.map((read) => [read.slice.day, read]));
53
+ return [...witnesses].flatMap(([from, earlierWitness]) => {
54
+ const to = nextDay(from);
55
+ const laterWitness = witnesses.get(to);
56
+ const earlier = byDay.get(from);
57
+ const later = byDay.get(to);
58
+ const tool = earlier ?? later;
59
+ if (laterWitness === undefined || tool === undefined)
60
+ return [];
61
+ const failure = earlier === undefined || later === undefined ? [] : failureMove(earlier, later, from, to);
62
+ return [...failure, ...shareMove(tool, [{ tool: earlier, witness: earlierWitness }, { tool: later, witness: laterWitness }], from, to)];
63
+ });
64
+ });
65
+ };
@@ -0,0 +1,7 @@
1
+ import { type DeriveFamily } from './read-support.js';
2
+ export declare const deriveFit: DeriveFamily;
3
+ /**
4
+ * Fit by environment: how often a name fails on a platform and shell, from the version-free cells beside the versioned ones. The two sets count the same calls and are never summed, so
5
+ * this read is its own, and a read of the version cells does not see these cells (`atGrain`). A shell the frame does not name reads as `unknown`, as a missing model does.
6
+ */
7
+ export declare const deriveFitEnvironment: DeriveFamily;
@@ -0,0 +1,41 @@
1
+ // Fit (rank 2): how often a tool fails on this frame, by harness version bucket and model. Effort is not in a cell (rule 3), so it is not a slice. Confounders it does not remove: a failure is not a
2
+ // misfit, red tests can be the method, effort, and prompt class is not adjusted for.
3
+ import { bucketFields, environmentFieldsOf } from './cells.js';
4
+ import { motifName } from './motif.js';
5
+ import { body, environmentCells, groupBy, rate, settled, total, unitCells, wilson } from './read-support.js';
6
+ export const deriveFit = ({ pool }) => [...groupBy(unitCells(pool, 'week'), ({ cell }) => `${cell.motifId}/${cell.frameBucket}`).values()].flatMap((group) => {
7
+ const [first] = group;
8
+ const cells = group.map(({ cell }) => cell);
9
+ const n = total(cells, settled);
10
+ if (first === undefined || n === 0)
11
+ return [];
12
+ const failed = total(cells, (cell) => cell.failed);
13
+ return [
14
+ body('fit', cells, {
15
+ slice: { motif: motifName(first.motif), level: first.motif.level, ...bucketFields(first.cell.frameBucket) },
16
+ value: { failureRate: rate(failed, n), failed, ok: n - failed },
17
+ n,
18
+ interval: wilson(failed, n),
19
+ }),
20
+ ];
21
+ });
22
+ /**
23
+ * Fit by environment: how often a name fails on a platform and shell, from the version-free cells beside the versioned ones. The two sets count the same calls and are never summed, so
24
+ * this read is its own, and a read of the version cells does not see these cells (`atGrain`). A shell the frame does not name reads as `unknown`, as a missing model does.
25
+ */
26
+ export const deriveFitEnvironment = ({ pool }) => [...groupBy(environmentCells(pool), (cell) => `${cell.motifId}/${cell.frameBucket}`).values()].flatMap((group) => {
27
+ const [first] = group;
28
+ const motif = first === undefined ? undefined : pool.motifs.get(first.motifId);
29
+ const n = total(group, settled);
30
+ if (first === undefined || motif === undefined || n === 0)
31
+ return [];
32
+ const failed = total(group, (cell) => cell.failed);
33
+ return [
34
+ body('fit', group, {
35
+ slice: { motif: motifName(motif), level: motif.level, ...environmentFieldsOf(first.frameBucket) },
36
+ value: { failureRate: rate(failed, n), failed, ok: n - failed },
37
+ n,
38
+ interval: wilson(failed, n),
39
+ }),
40
+ ];
41
+ });
@@ -0,0 +1,5 @@
1
+ import { type Candidate, type DeriveRelation } from './read-order.js';
2
+ import type { Read } from './read.js';
3
+ /** The comparisons a store's fit reads allow, above the floor, whether or not any of them moved. */
4
+ export declare const interactionCandidates: (reads: readonly Read[]) => Candidate[];
5
+ export declare const deriveInteraction: DeriveRelation;
@@ -0,0 +1,62 @@
1
+ // Interaction (spec Section 6, order 2): a tool fails more on one tier than another, beyond how those two tiers differ on every other tool. The difference of differences is
2
+ // what makes it an interaction: a tier that fails more on everything has a main effect, usually the harder work it is given. The axis is the tier assigned, not a model version
3
+ // (ruling mrd5591-08: one version per model on this store), and comparisons hold the version bucket constant. Effort is not in a cell (rule 3), so nothing here holds it. Confounders it does not remove: task assignment by
4
+ // tier, which is the operator's own choice and the largest of them, effort, prompt class, and a failure not being a misfit.
5
+ import { UNKNOWN } from './cells.js';
6
+ import { candidateOf, differenceOf, estimateOf, screen, TRIALS_FLOOR } from './read-order.js';
7
+ import { groupBy, rate, wilson } from './read-support.js';
8
+ import { toolKey } from './read-version-boundary.js';
9
+ const countOf = (read, metric) => {
10
+ const value = read.value[metric];
11
+ return typeof value === 'number' ? value : 0;
12
+ };
13
+ const tierOf = (model, reads) => ({ model, tools: new Map(reads.map((read) => [toolKey(read), read])), shapes: reads.filter((read) => read.slice.level === 'shape') });
14
+ /** A tier's failure rate on every step but this tool's. A named tool's steps are in its action's shape read, so subtracting it works at either level. */
15
+ const restOf = (tier, read) => {
16
+ const failed = tier.shapes.reduce((sum, shape) => sum + countOf(shape, 'failed'), 0) - countOf(read, 'failed');
17
+ const trials = tier.shapes.reduce((sum, shape) => sum + shape.n, 0) - read.n;
18
+ const value = rate(failed, trials);
19
+ const interval = wilson(failed, trials);
20
+ return trials < TRIALS_FLOOR || value === null || interval === null ? undefined : { value, interval, cites: tier.shapes.filter((shape) => shape.readId !== read.readId) };
21
+ };
22
+ const candidateFor = (a, b, key, scope) => {
23
+ const readA = a.tools.get(key);
24
+ const readB = b.tools.get(key);
25
+ if (readA === undefined || readB === undefined)
26
+ return undefined;
27
+ const failureA = estimateOf(readA, 'failureRate');
28
+ const failureB = estimateOf(readB, 'failureRate');
29
+ const restA = restOf(a, readA);
30
+ const restB = restOf(b, readB);
31
+ if (failureA === undefined || failureB === undefined || restA === undefined || restB === undefined)
32
+ return undefined;
33
+ const gap = differenceOf(failureA, failureB);
34
+ const restGap = differenceOf(restA, restB);
35
+ const interaction = differenceOf(gap, restGap);
36
+ const [level = UNKNOWN, motif = UNKNOWN] = key.split('\n');
37
+ return candidateOf('interaction', [readA, readB], [gap, interaction], {
38
+ slice: { motif, level, ...scope, tierA: a.model, tierB: b.model },
39
+ value: { failureA: failureA.value, failureB: failureB.value, gap: gap.value, restGap: restGap.value, interaction: interaction.value },
40
+ interval: interaction.interval,
41
+ cites: [...restA.cites, ...restB.cites],
42
+ });
43
+ };
44
+ /** The comparisons a store's fit reads allow, above the floor, whether or not any of them moved. */
45
+ export const interactionCandidates = (reads) => {
46
+ const known = reads.filter((read) => read.family === 'fit' && read.slice.model !== UNKNOWN && read.slice.harnessBucket !== UNKNOWN);
47
+ const frames = groupBy(known, (read) => `${read.slice.harness ?? UNKNOWN}\n${read.slice.harnessBucket ?? UNKNOWN}`);
48
+ return [...frames].flatMap(([frame, group]) => {
49
+ const [harness = UNKNOWN, harnessBucket = UNKNOWN] = frame.split('\n');
50
+ const tiers = [...groupBy(group, (read) => read.slice.model ?? UNKNOWN)].map(([model, inTier]) => tierOf(model, inTier)).sort((a, b) => (a.model < b.model ? -1 : 1));
51
+ return tiers.flatMap((a, index) => tiers.slice(index + 1).flatMap((b) => [...a.tools.keys()].flatMap((key) => {
52
+ const candidate = candidateFor(a, b, key, { harness, harnessBucket });
53
+ return candidate === undefined ? [] : [candidate];
54
+ })));
55
+ });
56
+ };
57
+ /**
58
+ * The gap and the interaction point the same way, so the tool moves toward that tier whatever the other tools do: `moved` asks only that each gate is large, not its sign. A gap and an interaction
59
+ * of opposite signs mean the tool resists the main effect (it still fails more on that tier, only by less than the other tools do), which is not a tool that works on one tier and not the other.
60
+ */
61
+ const sameSign = ({ gates }) => gates.every((gate) => gate.value > 0) || gates.every((gate) => gate.value < 0);
62
+ export const deriveInteraction = (reads) => screen(interactionCandidates(reads), sameSign);
@@ -0,0 +1,15 @@
1
+ import { type DeriveRelation, type Estimate } from './read-order.js';
2
+ import type { Read } from './read.js';
3
+ /** The fewest pairs on each side of the cut. */
4
+ export declare const PAIRS_FLOOR = 30;
5
+ export interface LeadLagPair {
6
+ /** The failure move. */
7
+ x: Estimate;
8
+ /** The share move that shares no day with it. */
9
+ y: Estimate;
10
+ /** The day the outcome is known: the last day of the share move. */
11
+ day: string;
12
+ reads: readonly [Read, Read];
13
+ }
14
+ export declare const leadLagPairs: (reads: readonly Read[]) => LeadLagPair[];
15
+ export declare const deriveLeadLag: DeriveRelation;