@manyoars/oars 0.4.0 → 0.5.1

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 (185) hide show
  1. package/README.md +82 -32
  2. package/dist/bin.js +3 -1
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +206 -20
  5. package/dist/club.d.ts +19 -0
  6. package/dist/club.js +132 -0
  7. package/dist/gate/hook-fill-main.js +1 -1
  8. package/dist/gate/hook-fill.d.ts +1 -1
  9. package/dist/gate/hook-fill.js +1 -1
  10. package/dist/gate/hook-main.js +7 -5
  11. package/dist/gate/hook.d.ts +12 -1
  12. package/dist/gate/hook.js +38 -7
  13. package/dist/hook-bin.js +23 -4
  14. package/dist/issuer.d.ts +1 -1
  15. package/dist/issuer.js +2 -2
  16. package/dist/main.js +5 -0
  17. package/dist/node-floor.d.ts +10 -0
  18. package/dist/node-floor.js +23 -0
  19. package/dist/oars-hook.d.ts +2 -0
  20. package/dist/oars-hook.js +7 -0
  21. package/dist/sessions/allow.d.ts +1 -0
  22. package/dist/sessions/allow.js +1 -1
  23. package/dist/sessions/call-shape.d.ts +111 -0
  24. package/dist/sessions/call-shape.js +464 -0
  25. package/dist/sessions/candidates.d.ts +65 -0
  26. package/dist/sessions/candidates.js +236 -0
  27. package/dist/sessions/card-command.d.ts +24 -0
  28. package/dist/sessions/card-command.js +71 -0
  29. package/dist/sessions/card-font.d.ts +6 -0
  30. package/dist/sessions/card-font.js +36 -0
  31. package/dist/sessions/card-line.d.ts +30 -0
  32. package/dist/sessions/card-line.js +105 -0
  33. package/dist/sessions/card-render.d.ts +15 -0
  34. package/dist/sessions/card-render.js +146 -0
  35. package/dist/sessions/card.d.ts +59 -0
  36. package/dist/sessions/card.js +107 -0
  37. package/dist/sessions/cell-increments.d.ts +14 -3
  38. package/dist/sessions/cell-increments.js +79 -12
  39. package/dist/sessions/cells.d.ts +42 -4
  40. package/dist/sessions/cells.js +72 -18
  41. package/dist/sessions/codex-hook-bin.d.ts +1 -0
  42. package/dist/sessions/codex-hook-bin.js +24 -0
  43. package/dist/sessions/codex-hook-install.d.ts +21 -0
  44. package/dist/sessions/codex-hook-install.js +47 -0
  45. package/dist/sessions/codex-hook-view.d.ts +20 -0
  46. package/dist/sessions/codex-hook-view.js +20 -0
  47. package/dist/sessions/codex-hook.d.ts +77 -0
  48. package/dist/sessions/codex-hook.js +255 -0
  49. package/dist/sessions/codex.d.ts +9 -0
  50. package/dist/sessions/codex.js +104 -18
  51. package/dist/sessions/command.d.ts +17 -2
  52. package/dist/sessions/command.js +145 -33
  53. package/dist/sessions/copilot.d.ts +3 -0
  54. package/dist/sessions/copilot.js +12 -4
  55. package/dist/sessions/derive.js +2 -1
  56. package/dist/sessions/empty-state.d.ts +17 -0
  57. package/dist/sessions/empty-state.js +38 -0
  58. package/dist/sessions/exclusion.js +48 -3
  59. package/dist/sessions/extract.d.ts +16 -3
  60. package/dist/sessions/extract.js +281 -24
  61. package/dist/sessions/gemini.d.ts +2 -0
  62. package/dist/sessions/gemini.js +29 -2
  63. package/dist/sessions/harness-files.d.ts +27 -0
  64. package/dist/sessions/harness-files.js +109 -0
  65. package/dist/sessions/head-only.d.ts +7 -0
  66. package/dist/sessions/head-only.js +21 -0
  67. package/dist/sessions/hook-calls.js +4 -3
  68. package/dist/sessions/hooks.d.ts +1 -1
  69. package/dist/sessions/hooks.js +6 -2
  70. package/dist/sessions/identity.d.ts +6 -0
  71. package/dist/sessions/injected.d.ts +13 -0
  72. package/dist/sessions/injected.js +212 -0
  73. package/dist/sessions/install.d.ts +31 -20
  74. package/dist/sessions/install.js +80 -82
  75. package/dist/sessions/load-status.d.ts +41 -0
  76. package/dist/sessions/load-status.js +156 -0
  77. package/dist/sessions/machine-child-deps.d.ts +5 -0
  78. package/dist/sessions/machine-child-deps.js +83 -0
  79. package/dist/sessions/machine-child.d.ts +43 -0
  80. package/dist/sessions/machine-child.js +120 -0
  81. package/dist/sessions/machine-credential.d.ts +10 -0
  82. package/dist/sessions/machine-credential.js +20 -0
  83. package/dist/sessions/machine-enroll.d.ts +47 -0
  84. package/dist/sessions/machine-enroll.js +230 -0
  85. package/dist/sessions/machine-hold.d.ts +49 -0
  86. package/dist/sessions/machine-hold.js +88 -0
  87. package/dist/sessions/machine-ids.d.ts +6 -0
  88. package/dist/sessions/machine-ids.js +9 -0
  89. package/dist/sessions/machine-notice.d.ts +21 -0
  90. package/dist/sessions/machine-notice.js +89 -0
  91. package/dist/sessions/machine-pull.d.ts +40 -0
  92. package/dist/sessions/machine-pull.js +220 -0
  93. package/dist/sessions/machine-repo.d.ts +7 -0
  94. package/dist/sessions/machine-repo.js +30 -0
  95. package/dist/sessions/machine-scan-cache.d.ts +12 -0
  96. package/dist/sessions/machine-scan-cache.js +47 -0
  97. package/dist/sessions/machine-send.d.ts +12 -0
  98. package/dist/sessions/machine-send.js +156 -0
  99. package/dist/sessions/machine-stop.d.ts +22 -0
  100. package/dist/sessions/machine-stop.js +53 -0
  101. package/dist/sessions/machine.d.ts +41 -0
  102. package/dist/sessions/machine.js +89 -0
  103. package/dist/sessions/opencode.d.ts +2 -0
  104. package/dist/sessions/opencode.js +6 -0
  105. package/dist/sessions/otel.d.ts +21 -0
  106. package/dist/sessions/otel.js +109 -38
  107. package/dist/sessions/owner-only.d.ts +7 -0
  108. package/dist/sessions/owner-only.js +34 -0
  109. package/dist/sessions/pi.d.ts +13 -1
  110. package/dist/sessions/pi.js +27 -13
  111. package/dist/sessions/pool-books.d.ts +1 -1
  112. package/dist/sessions/pool-books.js +1 -1
  113. package/dist/sessions/pool-choices.d.ts +4 -2
  114. package/dist/sessions/pool-choices.js +4 -2
  115. package/dist/sessions/pool-command.d.ts +46 -6
  116. package/dist/sessions/pool-command.js +577 -133
  117. package/dist/sessions/pool-delta.d.ts +7 -0
  118. package/dist/sessions/pool-delta.js +28 -5
  119. package/dist/sessions/pool-due.d.ts +43 -0
  120. package/dist/sessions/pool-due.js +113 -0
  121. package/dist/sessions/pool-nightly.d.ts +9 -1
  122. package/dist/sessions/pool-nightly.js +65 -21
  123. package/dist/sessions/pool-outbox.d.ts +51 -5
  124. package/dist/sessions/pool-outbox.js +217 -24
  125. package/dist/sessions/pool-print.d.ts +1 -1
  126. package/dist/sessions/pool-raw.js +4 -3
  127. package/dist/sessions/pool-replica.d.ts +4 -4
  128. package/dist/sessions/pool-replica.js +26 -7
  129. package/dist/sessions/pool-send-lock.js +1 -1
  130. package/dist/sessions/pool-sent.d.ts +2 -0
  131. package/dist/sessions/pool-sent.js +21 -4
  132. package/dist/sessions/pool-wire.d.ts +172 -12
  133. package/dist/sessions/pool-wire.js +284 -30
  134. package/dist/sessions/pool.js +4 -2
  135. package/dist/sessions/public-vocabulary.d.ts +5 -0
  136. package/dist/sessions/public-vocabulary.js +16 -0
  137. package/dist/sessions/read-call-shape.js +12 -6
  138. package/dist/sessions/read-fit.d.ts +5 -0
  139. package/dist/sessions/read-fit.js +23 -2
  140. package/dist/sessions/read.d.ts +1 -1
  141. package/dist/sessions/recall-consequence.d.ts +2 -2
  142. package/dist/sessions/recall-consequence.js +14 -9
  143. package/dist/sessions/recall-object.d.ts +9 -2
  144. package/dist/sessions/receipt.d.ts +8 -0
  145. package/dist/sessions/receipt.js +78 -0
  146. package/dist/sessions/recovery-calls.d.ts +3 -2
  147. package/dist/sessions/recovery-calls.js +9 -3
  148. package/dist/sessions/redact.d.ts +2 -2
  149. package/dist/sessions/redact.js +116 -14
  150. package/dist/sessions/reversion.d.ts +8 -0
  151. package/dist/sessions/reversion.js +114 -0
  152. package/dist/sessions/schema.d.ts +78 -4
  153. package/dist/sessions/schema.js +31 -1
  154. package/dist/sessions/session-shape.d.ts +2 -1
  155. package/dist/sessions/session-shape.js +15 -0
  156. package/dist/sessions/sources.d.ts +9 -0
  157. package/dist/sessions/sources.js +16 -5
  158. package/dist/sessions/steps.js +3 -2
  159. package/dist/sessions/store.d.ts +41 -2
  160. package/dist/sessions/store.js +68 -16
  161. package/dist/sessions/try-demo.d.ts +8 -0
  162. package/dist/sessions/try-demo.js +90 -0
  163. package/dist/sessions/try.d.ts +13 -0
  164. package/dist/sessions/try.js +50 -0
  165. package/dist/sessions/vscode-copilot.d.ts +4 -0
  166. package/dist/sessions/vscode-copilot.js +25 -18
  167. package/dist/telemetry/command.js +12 -6
  168. package/dist/telemetry/consent.d.ts +9 -3
  169. package/dist/telemetry/consent.js +25 -9
  170. package/dist/telemetry/fields.d.ts +11 -0
  171. package/dist/telemetry/fields.js +56 -0
  172. package/dist/telemetry/measures.d.ts +13 -0
  173. package/dist/telemetry/measures.js +30 -0
  174. package/dist/telemetry/observe.d.ts +15 -2
  175. package/dist/telemetry/observe.js +34 -7
  176. package/dist/telemetry/region.d.ts +6 -0
  177. package/dist/telemetry/region.js +28 -0
  178. package/dist/telemetry/report.js +1 -1
  179. package/dist/telemetry/schema.d.ts +3 -0
  180. package/dist/telemetry/schema.js +11 -1
  181. package/dist/telemetry/send.d.ts +5 -2
  182. package/dist/telemetry/send.js +15 -7
  183. package/dist/telemetry/words.d.ts +6 -0
  184. package/dist/telemetry/words.js +6 -0
  185. package/package.json +23 -4
@@ -0,0 +1,146 @@
1
+ // The crew card as a picture. `cardSvg` writes the SVG; `rasterize` reads back that SVG (the rects and monospace text this file writes, nothing else) into pixels, and
2
+ // `encodePng` writes them as a PNG. No renderer dependency: the one on offer (resvg) is MPL-2.0, which `check-licenses` refuses, and the card's picture is only
3
+ // rects and text. Both outputs are byte-stable for one card. The names on it are plain text as the session recorded them, with no vendor logo.
4
+ import { deflateSync } from 'node:zlib';
5
+ import { CARD_COMMAND, CARD_SITE, cardRows, cardTitle } from './card.js';
6
+ import { GLYPH_HEIGHT, GLYPH_WIDTH, lit } from './card-font.js';
7
+ const WIDTH = 1200;
8
+ const MARGIN = 48;
9
+ const TOP = 140;
10
+ export const PITCH = 36;
11
+ const FONT = 20;
12
+ const TITLE_FONT = 30;
13
+ const VALUE_X = 308;
14
+ /** The widest value line, in characters, that fits between VALUE_X and the right margin. */
15
+ const WRAP = 68;
16
+ const BACKGROUND = '0b1b2b';
17
+ const INK = 'e8eef4';
18
+ const MUTED = '8fa6ba';
19
+ const ACCENT = 'f2b134';
20
+ /** A glyph cell is one column wider than the glyph, so a monospace font of size N has this advance at N / 10 pixels per font pixel: 0.6 em. */
21
+ const advance = (font) => ((GLYPH_WIDTH + 1) * font) / 10;
22
+ const escape = (text) => text.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
23
+ const unescape = (text) => text.replaceAll('&quot;', '"').replaceAll('&gt;', '>').replaceAll('&lt;', '<').replaceAll('&amp;', '&');
24
+ /** A value split at its commas into lines of at most `WRAP` characters; a part longer than that stays whole. */
25
+ const wrap = (value) => {
26
+ const lines = [];
27
+ for (const part of value.split(', ')) {
28
+ const last = lines.length - 1;
29
+ if (last >= 0 && `${lines[last] ?? ''}, ${part}`.length < WRAP)
30
+ lines[last] = `${lines[last] ?? ''}, ${part}`;
31
+ else
32
+ lines.push(part);
33
+ }
34
+ return lines.map((line, index) => (index < lines.length - 1 ? `${line},` : line));
35
+ };
36
+ const text = (x, y, font, fill, body) => `<text x="${String(x)}" y="${String(y)}" font-family="monospace" font-size="${String(font)}" fill="#${fill}">${escape(body)}</text>`;
37
+ const rect = (x, y, width, height, fill) => `<rect x="${String(x)}" y="${String(y)}" width="${String(width)}" height="${String(height)}" fill="#${fill}"/>`;
38
+ export const cardSvg = (card, depositor) => {
39
+ const body = [];
40
+ let y = TOP;
41
+ let previous = '';
42
+ for (const row of cardRows(card, depositor)) {
43
+ wrap(row.value).forEach((line, index) => {
44
+ // A repeated label (one `tokens` row for each model) prints once.
45
+ if (index === 0 && row.label !== previous)
46
+ body.push(text(MARGIN, y, FONT, MUTED, row.label));
47
+ body.push(text(VALUE_X, y, FONT, INK, line));
48
+ y += PITCH;
49
+ });
50
+ previous = row.label;
51
+ }
52
+ const height = y + 20 + 60;
53
+ const footerY = height - 30;
54
+ return [
55
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${String(WIDTH)}" height="${String(height)}" viewBox="0 0 ${String(WIDTH)} ${String(height)}">`,
56
+ rect(0, 0, WIDTH, height, BACKGROUND),
57
+ text(MARGIN, 72, TITLE_FONT, INK, cardTitle(card)),
58
+ rect(MARGIN, 96, WIDTH - 2 * MARGIN, 2, ACCENT),
59
+ ...body,
60
+ rect(MARGIN, height - 70, WIDTH - 2 * MARGIN, 1, MUTED),
61
+ text(MARGIN, footerY, FONT, ACCENT, CARD_SITE),
62
+ text(WIDTH - MARGIN - Math.round(advance(FONT) * CARD_COMMAND.length), footerY, FONT, ACCENT, CARD_COMMAND),
63
+ '</svg>',
64
+ '',
65
+ ].join('\n');
66
+ };
67
+ const colorOf = (hex) => [Number.parseInt(hex.slice(0, 2), 16), Number.parseInt(hex.slice(2, 4), 16), Number.parseInt(hex.slice(4, 6), 16)];
68
+ const SHAPE = /<rect x="(\d+)" y="(\d+)" width="(\d+)" height="(\d+)" fill="#([0-9a-f]{6})"\/>|<text x="(\d+)" y="(\d+)" font-family="monospace" font-size="(\d+)" fill="#([0-9a-f]{6})">([^<]*)<\/text>/g;
69
+ /** Pixels for the SVG `cardSvg` writes: rects and monospace text in document order, a text's font size a whole multiple of ten. */
70
+ export const rasterize = (svg) => {
71
+ const size = /<svg [^>]*width="(\d+)" height="(\d+)"/.exec(svg);
72
+ if (size === null)
73
+ throw new Error('The card SVG has no size.');
74
+ const width = Number(size[1]);
75
+ const height = Number(size[2]);
76
+ const rgb = Buffer.alloc(width * height * 3);
77
+ const paint = (x, y, color) => {
78
+ if (x < 0 || y < 0 || x >= width || y >= height)
79
+ return;
80
+ rgb.set(color, (y * width + x) * 3);
81
+ };
82
+ for (const match of svg.matchAll(SHAPE)) {
83
+ if (match[1] !== undefined) {
84
+ const [x, y, w, h] = [Number(match[1]), Number(match[2]), Number(match[3]), Number(match[4])];
85
+ const color = colorOf(match[5] ?? '');
86
+ for (let row = y; row < y + h; row += 1)
87
+ for (let column = x; column < x + w; column += 1)
88
+ paint(column, row, color);
89
+ continue;
90
+ }
91
+ const [x, baseline, font] = [Number(match[6]), Number(match[7]), Number(match[8])];
92
+ if (font % 10 !== 0)
93
+ throw new Error(`The card SVG has a font size of ${String(font)}; the picture takes a multiple of 10.`);
94
+ const scale = font / 10;
95
+ const color = colorOf(match[9] ?? '');
96
+ Array.from(unescape(match[10] ?? '')).forEach((char, index) => {
97
+ for (let column = 0; column < GLYPH_WIDTH; column += 1) {
98
+ for (let row = 0; row < GLYPH_HEIGHT; row += 1) {
99
+ if (!lit(char, column, row))
100
+ continue;
101
+ for (let dy = 0; dy < scale; dy += 1)
102
+ for (let dx = 0; dx < scale; dx += 1)
103
+ paint(x + (index * (GLYPH_WIDTH + 1) + column) * scale + dx, baseline - (GLYPH_HEIGHT - 1) * scale + row * scale + dy, color);
104
+ }
105
+ }
106
+ });
107
+ }
108
+ return { width, height, rgb };
109
+ };
110
+ const CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
111
+ let c = n;
112
+ for (let bit = 0; bit < 8; bit += 1)
113
+ c = (c & 1) === 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
114
+ return c >>> 0;
115
+ });
116
+ const crc32 = (bytes) => {
117
+ let c = 0xffffffff;
118
+ for (const byte of bytes)
119
+ c = (CRC_TABLE[(c ^ byte) & 0xff] ?? 0) ^ (c >>> 8);
120
+ return (c ^ 0xffffffff) >>> 0;
121
+ };
122
+ const chunk = (type, data) => {
123
+ const head = Buffer.alloc(8);
124
+ head.writeUInt32BE(data.length, 0);
125
+ head.write(type, 4, 'latin1');
126
+ const crc = Buffer.alloc(4);
127
+ crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])), 0);
128
+ return Buffer.concat([head, data, crc]);
129
+ };
130
+ /** An 8-bit RGB PNG, unfiltered rows, zlib at level 9 (deterministic for one input). */
131
+ export const encodePng = (width, height, rgb) => {
132
+ const header = Buffer.alloc(13);
133
+ header.writeUInt32BE(width, 0);
134
+ header.writeUInt32BE(height, 4);
135
+ header.set([8, 2, 0, 0, 0], 8);
136
+ const stride = width * 3;
137
+ const raw = Buffer.alloc((stride + 1) * height);
138
+ for (let row = 0; row < height; row += 1)
139
+ rgb.copy(raw, row * (stride + 1) + 1, row * stride, (row + 1) * stride);
140
+ return Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), chunk('IHDR', header), chunk('IDAT', deflateSync(raw, { level: 9 })), chunk('IEND', Buffer.alloc(0))]);
141
+ };
142
+ /** The PNG for an SVG `cardSvg` wrote. */
143
+ export const cardPng = (svg) => {
144
+ const { width, height, rgb } = rasterize(svg);
145
+ return encodePng(width, height, rgb);
146
+ };
@@ -0,0 +1,59 @@
1
+ import { type BooksShare } from './query.js';
2
+ import type { Store } from './store.js';
3
+ import type { SurvivalState } from './survival.js';
4
+ export type CardPeriod = 'week' | 'month' | 'year';
5
+ export declare const OTHER = "other";
6
+ export declare const cardWindow: (period: CardPeriod, now: number) => {
7
+ days: number;
8
+ end: string;
9
+ };
10
+ export interface CardModel {
11
+ name: string;
12
+ prompts: number;
13
+ /** Input, cache, and output tokens over the prompts that recorded them. */
14
+ tokens: number;
15
+ }
16
+ export interface Card {
17
+ period: CardPeriod;
18
+ days: number;
19
+ /** The UTC date the window ends on. */
20
+ windowEnd: string;
21
+ sessions: number;
22
+ activeDays: number;
23
+ harnesses: Array<{
24
+ name: string;
25
+ sessions: number;
26
+ }>;
27
+ models: CardModel[];
28
+ /** Main-line tool calls, each call once. */
29
+ strokes: number;
30
+ /** Tool calls that failed. */
31
+ crabs: number;
32
+ /** Crabs whose next call was a different tool, over the crabs; a share under the floor reads `too few`. */
33
+ movedOn: BooksShare;
34
+ /** Edited prompts that were followed by a command, over edited prompts. */
35
+ verifyAfterEdit: BooksShare;
36
+ commits: number;
37
+ /** Commits whose survival is known, and how many survived. */
38
+ survived: BooksShare;
39
+ /** The harness's own `cost-state` totals, summed over the sessions that have one. Absent under `--no-cost`, so no output form carries it. */
40
+ cost?: {
41
+ usd: number | null;
42
+ recorded: number;
43
+ of: number;
44
+ };
45
+ }
46
+ export declare const cardOf: (store: Store, survival: ReadonlyMap<string, SurvivalState>, period: CardPeriod, now: number, includeCost?: boolean) => Card;
47
+ export interface CardRow {
48
+ label: string;
49
+ value: string;
50
+ }
51
+ /** The rows of the card, in order, shared by the table and the picture. `depositor` is the one line the sign-in state decides. */
52
+ export declare const cardRows: (card: Card, depositor: string) => CardRow[];
53
+ export declare const CARD_SITE = "manyoars.com";
54
+ export declare const CARD_COMMAND = "npx @manyoars/oars card";
55
+ export declare const cardTitle: (card: Card) => string;
56
+ /** The compact table `--text` prints, sized to screenshot in a terminal. */
57
+ export declare const cardText: (card: Card, depositor: string) => string;
58
+ /** The one link `card` prints: X's web intent with the text only. The person adds the picture; nothing is posted from here. */
59
+ export declare const tweetLink: (card: Card) => string;
@@ -0,0 +1,107 @@
1
+ // `oars card`: the person's own week, month, or year as counts, read from the local store only. It reuses `books` for the prompt-level columns (turns,
2
+ // commits, and commit survival) so no second parser of the store exists, and prints nothing a reader of the picture could trace to a folder, a path, a prompt,
3
+ // a repository, or a name outside the published model list.
4
+ import { firstCopies } from './derive.js';
5
+ import { books, BOOKS_MIN_PROMPTS } from './query.js';
6
+ import { publicModel } from './public-vocabulary.js';
7
+ import { harnessOf, observesUsage } from './schema.js';
8
+ const DAY_MS = 86_400_000;
9
+ const DAYS_IN = { week: 7, month: 30, year: 365 };
10
+ /** How many models the card names before the rest fold into `other`. */
11
+ const MODELS_SHOWN = 3;
12
+ export const OTHER = 'other';
13
+ export const cardWindow = (period, now) => ({ days: DAYS_IN[period], end: new Date(now).toISOString().slice(0, 10) });
14
+ const shareOf = (count, of) => ({ count, of, share: of < BOOKS_MIN_PROMPTS ? null : Math.round((count / of) * 1000) / 1000 });
15
+ const tokensOf = (prompt) => {
16
+ const { input, cacheWrite, cacheRead, output } = prompt.usage;
17
+ // A category the source never recorded is a placeholder zero, so it adds nothing either way; the check keeps the sum to what was measured.
18
+ return input + output + (observesUsage(prompt, 'cacheWrite') ? cacheWrite : 0) + (observesUsage(prompt, 'cacheRead') ? cacheRead : 0);
19
+ };
20
+ /** Models by prompts, the first few published names kept and every other name, published or not, counted under `other`. */
21
+ const modelMix = (prompts) => {
22
+ const byModel = new Map();
23
+ for (const prompt of prompts) {
24
+ const entry = byModel.get(prompt.model) ?? { name: prompt.model, prompts: 0, tokens: 0 };
25
+ entry.prompts += 1;
26
+ entry.tokens += tokensOf(prompt);
27
+ byModel.set(prompt.model, entry);
28
+ }
29
+ const ranked = [...byModel.values()].sort((a, b) => b.prompts - a.prompts || a.name.localeCompare(b.name));
30
+ const named = ranked.filter((entry) => publicModel(entry.name) === entry.name).slice(0, MODELS_SHOWN);
31
+ const rest = ranked.filter((entry) => !named.includes(entry));
32
+ return rest.length === 0 ? named : [...named, { name: OTHER, prompts: rest.reduce((total, entry) => total + entry.prompts, 0), tokens: rest.reduce((total, entry) => total + entry.tokens, 0) }];
33
+ };
34
+ const strokesOf = (tools) => {
35
+ const bySession = new Map();
36
+ for (const tool of tools)
37
+ bySession.set(tool.session, [...(bySession.get(tool.session) ?? []), tool]);
38
+ return [...bySession.values()].reduce((total, list) => total + firstCopies(list).length, 0);
39
+ };
40
+ export const cardOf = (store, survival, period, now, includeCost = true) => {
41
+ const { days, end } = cardWindow(period, now);
42
+ // Whole UTC dates: the window opens at 00:00 UTC of the date days - 1 before today, so it holds exactly `days` dates.
43
+ const cutoff = new Date(Date.parse(`${end}T00:00:00Z`) - (days - 1) * DAY_MS).toISOString();
44
+ const prompts = store.prompts.filter((prompt) => prompt.ts !== null && prompt.ts >= cutoff);
45
+ const promptKeys = new Set(prompts.map((prompt) => `${prompt.session}#${String(prompt.index)}`));
46
+ const inWindow = (record) => record.prompt !== null && promptKeys.has(`${record.session}#${String(record.prompt)}`);
47
+ const sessionIds = new Set(prompts.map((prompt) => prompt.session));
48
+ const sessions = store.sessions.filter((session) => sessionIds.has(session.session) && session.supersededBy === undefined);
49
+ const crabs = store.errors.filter((error) => error.agent === undefined && inWindow(error));
50
+ // `books` over one group: with no session naming a harness, every prompt falls under the one default key.
51
+ const [row] = books({ ...store, sessions: [], prompts }, 'harness', now, undefined, survival);
52
+ const verified = prompts.filter((prompt) => prompt.cls === 'edit-verify').length;
53
+ const edited = verified + prompts.filter((prompt) => prompt.cls === 'edit-only').length;
54
+ const harnessCounts = new Map();
55
+ for (const session of sessions)
56
+ harnessCounts.set(harnessOf(session), (harnessCounts.get(harnessOf(session)) ?? 0) + 1);
57
+ const costed = sessions.filter((session) => session.costUsd !== null);
58
+ return {
59
+ period,
60
+ days,
61
+ windowEnd: end,
62
+ sessions: sessions.length,
63
+ activeDays: new Set(prompts.map((prompt) => (prompt.ts ?? '').slice(0, 10))).size,
64
+ harnesses: [...harnessCounts.entries()].map(([name, count]) => ({ name, sessions: count })).sort((a, b) => b.sessions - a.sessions || a.name.localeCompare(b.name)),
65
+ models: modelMix(prompts),
66
+ strokes: strokesOf(store.tools.filter(inWindow)),
67
+ crabs: crabs.length,
68
+ movedOn: shareOf(crabs.filter((error) => error.next[0] !== undefined && error.next[0] !== error.tool).length, crabs.length),
69
+ verifyAfterEdit: shareOf(verified, edited),
70
+ commits: row?.commits ?? 0,
71
+ survived: row?.survived ?? shareOf(0, 0),
72
+ ...(includeCost ? { cost: { usd: costed.length === 0 ? null : costed.reduce((total, session) => total + (session.costUsd ?? 0), 0), recorded: costed.length, of: sessions.length } } : {}),
73
+ };
74
+ };
75
+ /** A share as the card prints it: a percent at or over the floor, else `too few`, with the counts it rests on. */
76
+ const shareText = (share, unit) => `${share.share === null ? 'too few' : `${String(Math.round(share.share * 100))}%`} (${String(share.count)} of ${String(share.of)} ${unit})`;
77
+ const dollars = (usd) => `$${usd.toFixed(2)}`;
78
+ /** The rows of the card, in order, shared by the table and the picture. `depositor` is the one line the sign-in state decides. */
79
+ export const cardRows = (card, depositor) => [
80
+ { label: 'sessions', value: `${String(card.sessions)} on ${String(card.activeDays)} active ${card.activeDays === 1 ? 'day' : 'days'}` },
81
+ { label: 'harnesses', value: card.harnesses.map((entry) => `${entry.name} ${String(entry.sessions)}`).join(', ') },
82
+ { label: 'models', value: card.models.map((entry) => `${entry.name} ${String(entry.prompts)}`).join(', ') },
83
+ ...card.models.map((entry) => ({ label: 'tokens', value: `${entry.name} ${String(entry.tokens)}` })),
84
+ { label: 'strokes', value: String(card.strokes) },
85
+ { label: 'crabs', value: String(card.crabs) },
86
+ { label: 'moved on', value: shareText(card.movedOn, 'crabs') },
87
+ { label: 'verified after edit', value: shareText(card.verifyAfterEdit, 'edits') },
88
+ { label: 'commits', value: String(card.commits) },
89
+ { label: 'survived', value: shareText(card.survived, 'settled') },
90
+ ...(card.cost === undefined ? [] : [{ label: 'cost', value: card.cost.usd === null ? 'not recorded by these harnesses' : `${dollars(card.cost.usd)} (${String(card.cost.recorded)} of ${String(card.cost.of)} sessions recorded one)` }]),
91
+ { label: 'depositor', value: depositor },
92
+ ];
93
+ export const CARD_SITE = 'manyoars.com';
94
+ export const CARD_COMMAND = 'npx @manyoars/oars card';
95
+ export const cardTitle = (card) => `oars crew card: the last ${String(card.days)} days to ${card.windowEnd}`;
96
+ /** The compact table `--text` prints, sized to screenshot in a terminal. */
97
+ export const cardText = (card, depositor) => {
98
+ const rows = cardRows(card, depositor).filter((row) => row.value !== '');
99
+ const width = Math.max(...rows.map((row) => row.label.length)) + 2;
100
+ return [cardTitle(card), '', ...rows.map((row) => `${row.label.padEnd(width)}${row.value}`), '', `${CARD_SITE} ${CARD_COMMAND}`].join('\n');
101
+ };
102
+ /** The one link `card` prints: X's web intent with the text only. The person adds the picture; nothing is posted from here. */
103
+ export const tweetLink = (card) => {
104
+ const stats = `${String(card.sessions)} ${card.sessions === 1 ? 'session' : 'sessions'}, ${String(card.strokes)} strokes, ${String(card.crabs)} crabs`;
105
+ const text = `My agents, the last ${String(card.days)} days: ${stats}. ${CARD_SITE} ${CARD_COMMAND}`;
106
+ return `https://x.com/intent/tweet?text=${encodeURIComponent(text)}`;
107
+ };
@@ -1,4 +1,4 @@
1
- import { type CellSet, type Grain } from './cells.js';
1
+ import { type CellSet, type ExposureGraph, type Exposure, type Grain } from './cells.js';
2
2
  import type { Motif } from './motif.js';
3
3
  import { type SessionSkeleton, type ShellReader } from './pool-wire.js';
4
4
  import { type RecallGraph } from './recall-consequence.js';
@@ -10,7 +10,10 @@ export interface CellRow {
10
10
  frameBucket: string;
11
11
  grain: Grain;
12
12
  day: string;
13
+ /** 1 for a shown cell, 0 for any other: the column the cell table held before held and unknown were told apart. */
13
14
  exposed: number;
15
+ /** Shown, held, or unknown; its code (`EXPOSURE_CODES`) is the last field of `key`. */
16
+ exposure: Exposure;
14
17
  sessions: number;
15
18
  ok: number;
16
19
  failed: number;
@@ -39,9 +42,17 @@ export interface Increments {
39
42
  * Rules 1 and 2: a window names a shell step by its program and subcommand alone, and each flag and each pull of it adds a one-step cell of its own.
40
43
  */
41
44
  export declare const cellsOf: (store: Store, graph?: RecallGraph) => CellSet;
42
- /** A local store with each shell step read as a deposit reads it (`projectedSteps`), so its named motifs are the ones the pool holds. It uses the open gate, which keeps the name shape and length but applies no redactor or exclusion term: a local read sends nothing. */
45
+ /**
46
+ * A local store with each shell step read as a deposit reads it (`projectedSteps`), so its named motifs are the ones the pool holds, and each call's key names as the wire sends them (`wireKeys`), so its shape cells are too.
47
+ * It uses the open gate, which keeps the name shape and length but applies no redactor or exclusion term: a local read sends nothing.
48
+ */
43
49
  export declare const projectedStore: (store: Store, read: ShellReader) => Store;
44
50
  /** One account's deposits as a store. A session's id is its skeleton's, which the client hashes per account, so it joins nothing across accounts. */
45
51
  export declare const storeOf: (skeletons: readonly SessionSkeleton[]) => Store;
46
- /** Whole cells and strata, summed over a batch's own sessions: the same cells a store of that batch alone derives. */
52
+ /**
53
+ * The graph a deposit derives its cells under: the recommendations the skeleton carries, placed on the main run's calls that are not sidechain (the line the windows were counted on). A call in a shown window is shown, in a held
54
+ * one held, and one the batch has no row for is unknown: the graph adds no step and never reads a call as unexposed, since no row cannot be told from a client that did not look. An arm this reader does not know places nothing.
55
+ */
56
+ export declare const exposureGraphOf: (store: Store, skeleton: SessionSkeleton) => ExposureGraph;
57
+ /** Whole cells and strata, summed over a batch's own sessions: the same cells a store of that batch alone derives, under the exposure the batch carries. */
47
58
  export declare const incrementsOf: (skeleton: SessionSkeleton) => Increments;
@@ -1,9 +1,9 @@
1
1
  // The cell computation a deposit and a local read share (pool vocabulary spec Section 2, "the deposit form", and Section 4, rules 1 to 6). The pool's ingest turns a batch's skeleton into
2
2
  // increments with `incrementsOf`, and a local read derives its cells with `cellsOf` over the store it holds, projected the way a deposit projects it (`projectedStore`). Both end in the
3
3
  // one `cellsOf`, so a session gives the same cells at the pool and on the machine. This file reads no file system: the pool's Worker bundles it.
4
- import { ARG_CLASSES } from './call-shape.js';
5
- import { bucketFields, deriveCells, ENVIRONMENT_BUCKET, isEnvironmentCell } from './cells.js';
6
- import { bucketOf, floorOf, OTHER, projectedSteps } from './pool-wire.js';
4
+ import { ARG_CLASSES, CHANGE_KINDS } from './call-shape.js';
5
+ import { bucketFields, deriveCells, ENVIRONMENT_BUCKET, EXPOSURE_CODES, exposureOfCell, isEnvironmentCell } from './cells.js';
6
+ import { bucketOf, floorOf, OTHER, OPEN_GATE, projectedSteps, wireChange, wireFeatures, wireKeys, wireValues } from './pool-wire.js';
7
7
  import { NO_RECALLS } from './recall-consequence.js';
8
8
  import { USAGE_FIELDS, harnessOf } from './schema.js';
9
9
  const floorTokens = (value) => floorOf('tokens', bucketOf('tokens', value));
@@ -53,13 +53,34 @@ export const cellsOf = (store, graph = NO_RECALLS) => {
53
53
  const named = new Set(marginal.motifs.filter(({ level, steps }) => level === 'named' && steps.length === 1 && steps[0]?.identity !== null).map(({ motifId }) => motifId));
54
54
  const motifs = new Map([...whole.motifs, ...marginal.motifs.filter(({ motifId }) => named.has(motifId))].map((motif) => [motif.motifId, motif]));
55
55
  const order = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
56
- const cells = [...whole.cells, ...marginal.cells.filter(({ motifId }) => named.has(motifId))].sort((a, b) => order(a.motifId, b.motifId) || order(a.frameBucket, b.frameBucket) || order(a.grain, b.grain) || order(a.day, b.day) || Number(a.exposed) - Number(b.exposed));
56
+ const cells = [...whole.cells, ...marginal.cells.filter(({ motifId }) => named.has(motifId))].sort((a, b) => order(a.motifId, b.motifId) || order(a.frameBucket, b.frameBucket) || order(a.grain, b.grain) || order(a.day, b.day) || EXPOSURE_CODES[exposureOfCell(a)] - EXPOSURE_CODES[exposureOfCell(b)]);
57
57
  return { cells, motifs: [...motifs.values()].sort((a, b) => order(a.motifId, b.motifId)) };
58
58
  };
59
- /** A local store with each shell step read as a deposit reads it (`projectedSteps`), so its named motifs are the ones the pool holds. It uses the open gate, which keeps the name shape and length but applies no redactor or exclusion term: a local read sends nothing. */
59
+ /**
60
+ * A local store with each shell step read as a deposit reads it (`projectedSteps`), so its named motifs are the ones the pool holds, and each call's key names as the wire sends them (`wireKeys`), so its shape cells are too.
61
+ * It uses the open gate, which keeps the name shape and length but applies no redactor or exclusion term: a local read sends nothing.
62
+ */
60
63
  export const projectedStore = (store, read) => {
61
64
  const harnesses = new Map([...store.sessions, ...store.subagents].map((session) => [session.session, harnessOf(session)]));
62
- return { ...store, tools: store.tools.map((tool) => (tool.steps === undefined ? tool : { ...tool, steps: projectedSteps(tool, read, harnesses.get(tool.session) ?? 'claude-code') })) };
65
+ return {
66
+ ...store,
67
+ tools: store.tools.map((tool) => {
68
+ if (tool.steps === undefined)
69
+ return tool;
70
+ const steps = projectedSteps(tool, read, harnesses.get(tool.session) ?? 'claude-code');
71
+ const { keys, dropped } = wireKeys(tool.keys, steps[0]?.native, OPEN_GATE);
72
+ const sent = new Set(keys);
73
+ const argFeatures = wireFeatures(tool.argFeatures, sent);
74
+ const argValues = wireValues(tool.argValues, sent, steps[0]?.native);
75
+ const changeKind = wireChange(tool.changeKind);
76
+ // Only what the wire would carry, so the shape cells here are the pool's.
77
+ const projected = { ...tool, steps, keys };
78
+ delete projected.argFeatures;
79
+ delete projected.argValues;
80
+ delete projected.changeKind;
81
+ return { ...projected, ...(dropped === 0 ? {} : { keysDropped: dropped }), ...(argFeatures.length === 0 ? {} : { argFeatures }), ...(argValues.length === 0 ? {} : { argValues }), ...(changeKind.length === 0 ? {} : { changeKind }) };
82
+ }),
83
+ };
63
84
  };
64
85
  const NO_USAGE = { input: 0, cacheWrite: 0, cacheRead: 0, output: 0, thinking: 0 };
65
86
  const isKnown = (transcript) => transcript.harness !== OTHER;
@@ -71,6 +92,20 @@ const frameOf = (frame, harness) => ({
71
92
  });
72
93
  /** A class this reader does not know counts in `mixed`, the class a prompt that fits no other takes. */
73
94
  const classOf = (cls) => (cls === OTHER ? 'mixed' : cls);
95
+ /** A feature whose key or size this reader does not know names nothing it can count, and a field it does not know is a field the call did not send. */
96
+ const knownFeatures = (features) => (features ?? []).flatMap(({ key, valueSize, pathDepth, extension, dirKind, patternFeatures }) => key === OTHER || valueSize === OTHER
97
+ ? []
98
+ : [
99
+ {
100
+ key,
101
+ valueSize,
102
+ ...(pathDepth === undefined || pathDepth === OTHER ? {} : { pathDepth }),
103
+ ...(extension === undefined || extension === OTHER ? {} : { extension }),
104
+ ...(dirKind === undefined || dirKind === OTHER ? {} : { dirKind }),
105
+ ...(patternFeatures === undefined ? {} : { patternFeatures: patternFeatures.flatMap((feature) => (feature === OTHER ? [] : [feature])) }),
106
+ },
107
+ ]);
108
+ const knownValues = (values) => (values ?? []).filter(({ key, enumValue }) => key !== OTHER && enumValue !== OTHER);
74
109
  /** A status this reader does not know is a segment the call did not report. */
75
110
  const statusOf = (status) => (status === OTHER ? 'none' : status);
76
111
  /** A skeleton's transcript as the record `deriveCells` reads. Everything a skeleton leaves out stays empty, so no derivation here can come to depend on it unnoticed. */
@@ -156,7 +191,7 @@ const stepPlaces = ({ tools }) => tools.flatMap(({ steps }, n) => steps.map((_,
156
191
  const toolRecords = (id, main, transcript, spawner) => {
157
192
  const own = stepPlaces(transcript);
158
193
  const spawning = spawner === null ? [] : stepPlaces(spawner);
159
- return transcript.tools.map(({ side, prompt, status, durationMs, errorCode, keys, targetClass, argClass, steps }, n) => ({
194
+ return transcript.tools.map(({ side, prompt, status, durationMs, errorCode, keys, keysDropped, targetClass, argClass, argFeatures, argValues, changeKind, steps }, n) => ({
160
195
  kind: 'tool',
161
196
  cv: '',
162
197
  sid: '',
@@ -170,10 +205,14 @@ const toolRecords = (id, main, transcript, spawner) => {
170
205
  status: statusOf(status),
171
206
  target: null,
172
207
  targetBase: null,
173
- // The names the wire carried, which are the only ones a skeleton-derived store ever holds; `keysDropped` is a count of names not sent and has no stored form.
208
+ // The names the wire carried, which are the only ones a skeleton-derived store ever holds, and the count of those it did not, at its bucket's lowest value.
174
209
  keys: keys ?? [],
210
+ ...(keysDropped === undefined ? {} : { keysDropped: floorOf('count', keysDropped) }),
175
211
  ...(targetClass === undefined ? {} : { targetClass }),
176
212
  ...(argClass === undefined ? {} : { argClass: ARG_CLASSES.filter((kind) => argClass.includes(kind)).sort() }),
213
+ ...(argFeatures === undefined ? {} : { argFeatures: knownFeatures(argFeatures) }),
214
+ ...(argValues === undefined ? {} : { argValues: knownValues(argValues) }),
215
+ ...(changeKind === undefined ? {} : { changeKind: CHANGE_KINDS.filter((kind) => changeKind.includes(kind)) }),
177
216
  durationMs,
178
217
  toolUseId: '',
179
218
  harnessVersion: null,
@@ -216,15 +255,43 @@ export const storeOf = (skeletons) => {
216
255
  }
217
256
  return store;
218
257
  };
219
- /** Whole cells and strata, summed over a batch's own sessions: the same cells a store of that batch alone derives. */
258
+ const callKey = (tool) => `${tool.session}#${String(tool.n)}`;
259
+ const promptKey = (session, index) => `${session}#${String(index)}`;
260
+ /**
261
+ * The graph a deposit derives its cells under: the recommendations the skeleton carries, placed on the main run's calls that are not sidechain (the line the windows were counted on). A call in a shown window is shown, in a held
262
+ * one held, and one the batch has no row for is unknown: the graph adds no step and never reads a call as unexposed, since no row cannot be told from a client that did not look. An arm this reader does not know places nothing.
263
+ */
264
+ export const exposureGraphOf = (store, skeleton) => {
265
+ const main = `${skeleton.id}#0`;
266
+ const line = store.tools.filter((tool) => tool.session === main && !tool.side).sort((a, b) => a.n - b.n);
267
+ const shown = { calls: new Set(), prompts: new Set() };
268
+ const held = { calls: new Set(), prompts: new Set() };
269
+ for (const { arm, from, to } of skeleton.recommendations ?? []) {
270
+ if (arm === OTHER)
271
+ continue;
272
+ const into = arm === 'shown' ? shown : held;
273
+ for (const tool of line.slice(from, to)) {
274
+ into.calls.add(callKey(tool));
275
+ into.prompts.add(promptKey(tool.session, tool.prompt));
276
+ }
277
+ }
278
+ return {
279
+ place: (tool, steps) => steps.map((step) => ({ step, exposed: shown.calls.has(callKey(tool)), held: held.calls.has(callKey(tool)) })),
280
+ exposedPrompt: (session, index) => shown.prompts.has(promptKey(session, index)),
281
+ heldPrompt: (session, index) => held.prompts.has(promptKey(session, index)),
282
+ };
283
+ };
284
+ /** Whole cells and strata, summed over a batch's own sessions: the same cells a store of that batch alone derives, under the exposure the batch carries. */
220
285
  export const incrementsOf = (skeleton) => {
221
- const derived = cellsOf(storeOf([skeleton]));
286
+ const store = storeOf([skeleton]);
287
+ const derived = cellsOf(store, exposureGraphOf(store, skeleton));
222
288
  const cells = new Map();
223
289
  for (const cell of derived.cells) {
290
+ const exposure = exposureOfCell(cell);
224
291
  const exposed = cell.exposed ? 1 : 0;
225
- const key = `${cell.motifId}/${cell.frameBucket}/${cell.grain}/${cell.day}/${String(exposed)}`;
292
+ const key = `${cell.motifId}/${cell.frameBucket}/${cell.grain}/${cell.day}/${String(EXPOSURE_CODES[exposure])}`;
226
293
  // The served column is a running sum, so a cell no session of this batch recorded tokens for adds nothing to it; the sum itself holds only what was observed, and `tokenSessions` and `tokenPrompts` say how much of the cell it covers.
227
- cells.set(key, { key, harnessBucket: isEnvironmentCell(cell) ? ENVIRONMENT_BUCKET : bucketFields(cell.frameBucket).harnessBucket, motifId: cell.motifId, frameBucket: cell.frameBucket, grain: cell.grain, day: cell.day, exposed, sessions: cell.sessions, ok: cell.ok, failed: cell.failed, tokens: cell.tokens ?? 0, tokenSessions: cell.tokenSessions ?? 0, tokenPrompts: cell.tokenPrompts ?? 0, durationMs: cell.durationMs });
294
+ cells.set(key, { key, harnessBucket: isEnvironmentCell(cell) ? ENVIRONMENT_BUCKET : bucketFields(cell.frameBucket).harnessBucket, motifId: cell.motifId, frameBucket: cell.frameBucket, grain: cell.grain, day: cell.day, exposed, exposure, sessions: cell.sessions, ok: cell.ok, failed: cell.failed, tokens: cell.tokens ?? 0, tokenSessions: cell.tokenSessions ?? 0, tokenPrompts: cell.tokenPrompts ?? 0, durationMs: cell.durationMs });
228
295
  }
229
296
  // A session counts once per value, so each value is one session of this batch.
230
297
  const strata = new Map();
@@ -1,6 +1,6 @@
1
- import { type Motif } from './motif.js';
2
- import { type RecallGraph } from './recall-consequence.js';
3
- import { type ErrorObservation, type Frame } from './schema.js';
1
+ import { type CountedStep, type Motif } from './motif.js';
2
+ import { type PlacedStep, type RecallGraph } from './recall-consequence.js';
3
+ import { type ErrorObservation, type Frame, type ToolObservation } from './schema.js';
4
4
  import type { Store } from './store.js';
5
5
  /** Rule 6: an `index` cell is weekly, except a live cell, which stays at the UTC day. */
6
6
  export type Grain = 'day' | 'week';
@@ -32,7 +32,27 @@ export interface Cell {
32
32
  durationMs: number;
33
33
  /** Whether these counts sit inside a shown recall's consequence window, wholly or in part. It is part of the cell's key, so a motif's exposed and unexposed counts stay apart. */
34
34
  exposed: boolean;
35
+ /**
36
+ * What a deposit's evidence says of the counts: `shown` (inside a shown window), `held` (inside a held one), or `unknown` (no recommendation covers them, which a pool cannot tell from a client that never looked).
37
+ * Present only on a cell derived with an `ExposureGraph`; a local cell leaves it out and reads `exposed` alone. Its code is the last field of a stored key (`EXPOSURE_CODES`).
38
+ */
39
+ exposure?: Exposure;
40
+ }
41
+ /** The three readings of a recommendation's exposure. There is no `unexposed`: absent evidence is `unknown`. */
42
+ export type Exposure = 'shown' | 'held' | 'unknown';
43
+ /** The last field of a stored cell key. `unknown` is 0 so every key written before held existed keeps its meaning. */
44
+ export declare const EXPOSURE_CODES: Readonly<Record<Exposure, number>>;
45
+ /** A step placed by a graph that also knows the recommendations a session was held back from. */
46
+ export interface HeldStep extends PlacedStep {
47
+ held: boolean;
48
+ }
49
+ /** A recall graph that states exposure in three readings. `deriveCells` reads `heldPrompt` and `held` beside the shown reading, and a cell it derives carries `exposure`. */
50
+ export interface ExposureGraph extends RecallGraph {
51
+ place: (tool: ToolObservation, steps: readonly CountedStep[]) => HeldStep[];
52
+ heldPrompt: (session: string, index: number) => boolean;
35
53
  }
54
+ /** A cell's reading: its own `exposure`, or for a local cell the two readings it can tell apart. */
55
+ export declare const exposureOfCell: (cell: Pick<Cell, "exposed" | "exposure">) => Exposure;
36
56
  export declare const UNKNOWN = "unknown";
37
57
  /** The class name an effort cell carries: a class motif with no prompt class in it, so a read of prompt classes skips it by this prefix. */
38
58
  export declare const EFFORT_CLASS = "effort:";
@@ -60,7 +80,25 @@ export declare const parseRecoveryClass: (cls: string) => {
60
80
  failed: string;
61
81
  next: string;
62
82
  } | null;
63
- /** Whether a class motif names a prompt class, as opposed to one of the side cells (effort, or a failed call's code or what followed it). */
83
+ /**
84
+ * The week cell of a call's shape: `shape:<action>/<facet>/<value>`, one facet of what the call held and never a value. It keeps the harness version bucket and no model, so it joins three dimensions: the call's action, the
85
+ * facet's value, and version. `target` is the class of the path target, `arg` one member of the argument classes (a set is one cell per member, rule 2), `dropped` the `count` bucket of key names not sent, and `keys` the
86
+ * sorted key names joined by commas, the call-shape method's key (a long tail, so the pool holds it to the thresholded floor). Layer 1 adds one cell per sent key's value feature (`size`, `depth`, `extension`, `dir`, and each
87
+ * `pattern` feature, its value `<key>=<member>`), per closed value (`enum`, `<key>=<value>`), and per change kind (`change`). A class motif with no prompt class in it, like the others.
88
+ */
89
+ export declare const SHAPE_CLASS = "shape:";
90
+ export declare const SHAPE_FACETS: readonly ["target", "arg", "dropped", "keys", "size", "depth", "extension", "dir", "pattern", "enum", "change"];
91
+ export type ShapeFacet = (typeof SHAPE_FACETS)[number];
92
+ export declare const shapeClass: (action: string, facet: ShapeFacet, value: string) => string;
93
+ /** The three parts of a shape class name, or null for a name that is not one. An action name and a facet hold no `/`; the value is the rest. */
94
+ export declare const parseShapeClass: (cls: string) => {
95
+ action: string;
96
+ facet: ShapeFacet;
97
+ value: string;
98
+ } | null;
99
+ /** The facets one call counts in: each that the record holds. A record captured before a facet existed has none of it, so it adds no cell of that facet. */
100
+ export declare const shapeFacetsOf: (tool: ToolObservation) => [ShapeFacet, string][];
101
+ /** Whether a class motif names a prompt class, as opposed to one of the side cells (effort, a failed call's code or what followed it, or a call's shape). */
64
102
  export declare const isPromptClass: (cls: string | null) => boolean;
65
103
  /** The frame bucket of a cell that joins no axis. */
66
104
  export declare const NO_AXES = "-";