@multileaf/ai-workflow 0.7.0 → 0.8.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.
@@ -0,0 +1,202 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { isMap, isScalar, isSeq, parseDocument } from "yaml";
3
+ import { composeContext, CONTEXT_LAYERS, } from "./context-store.js";
4
+ import { retrieveTaskContext } from "./context-retrieval.js";
5
+ import { estimateTokens } from "./context-benchmark.js";
6
+ /**
7
+ * Task parity suite (deterministic, offline, zero API cost).
8
+ *
9
+ * Each parity task pins a quality constraint on context assembly: the
10
+ * critical facts and completion markers a task needs must survive the
11
+ * just-in-time assembly path (`retrieveTaskContext`), not only the full
12
+ * composed context. If a fact only exists in a layer that retrieval drops,
13
+ * the task fails — that is a true-positive detection of context loss, not
14
+ * a benchmark artifact (see docs/token-efficiency-study-notes.md, step 2).
15
+ *
16
+ * Cost is reported (estimated, characters/4) but never gates: this suite is
17
+ * the quality gate; the cost gate is `aiw context-bench`.
18
+ */
19
+ export const PARITY_TASK_SCHEMA = 1;
20
+ /** The default just-in-time assembly path under test. */
21
+ export const TASK_ASSEMBLER = {
22
+ name: "task-retrieval",
23
+ assemble: retrieveTaskContext,
24
+ };
25
+ /** The full-context baseline every assembler is compared against. */
26
+ export const FULL_ASSEMBLER = {
27
+ name: "full-context",
28
+ assemble: composeContext,
29
+ };
30
+ export function parseParityTask(content) {
31
+ const document = parseDocument(content);
32
+ if (document.errors.length)
33
+ throw new Error(`Parity task is not valid YAML.`);
34
+ const schema = document.get("schema");
35
+ if (schema !== PARITY_TASK_SCHEMA)
36
+ throw new Error(`Parity task schema must be version ${PARITY_TASK_SCHEMA}.`);
37
+ const id = String(document.get("id") ?? "").trim();
38
+ if (!id)
39
+ throw new Error("Parity task id is required.");
40
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(id))
41
+ throw new Error(`Parity task id must be lowercase letters, numbers, and hyphens: ${id}.`);
42
+ const description = String(document.get("description") ?? "").trim();
43
+ const layersNode = document.get("layers");
44
+ const layers = {};
45
+ if (layersNode !== null && layersNode !== undefined) {
46
+ if (!isMap(layersNode))
47
+ throw new Error("Parity task layers must be a mapping.");
48
+ for (const layer of CONTEXT_LAYERS) {
49
+ const value = layersNode.get(layer);
50
+ if (value !== null && value !== undefined) {
51
+ if (typeof value !== "string")
52
+ throw new Error(`Parity task layer ${layer} must be a string.`);
53
+ layers[layer] = value;
54
+ }
55
+ }
56
+ }
57
+ if (!CONTEXT_LAYERS.some((layer) => (layers[layer] ?? "").trim()))
58
+ throw new Error(`Parity task ${id} needs at least one non-empty context layer.`);
59
+ const listOf = (key) => {
60
+ const node = document.get(key);
61
+ if (node === null || node === undefined)
62
+ return [];
63
+ if (!isSeq(node))
64
+ throw new Error(`Parity task ${key} must be a list.`);
65
+ return node.items.map((item) => {
66
+ const value = isScalar(item) ? item.value : item;
67
+ if (typeof value !== "string" || !value.trim())
68
+ throw new Error(`Parity task ${key} entries must be non-empty strings.`);
69
+ return value;
70
+ });
71
+ };
72
+ const criticalFacts = listOf("critical_facts");
73
+ const completionMarkers = listOf("completion_markers");
74
+ const requiredPatterns = listOf("required_patterns");
75
+ if (!criticalFacts.length && !completionMarkers.length && !requiredPatterns.length)
76
+ throw new Error(`Parity task ${id} needs at least one critical fact, completion marker, or required pattern.`);
77
+ for (const pattern of requiredPatterns)
78
+ try {
79
+ new RegExp(pattern);
80
+ }
81
+ catch {
82
+ throw new Error(`Parity task ${id} has an invalid required pattern: ${pattern}`);
83
+ }
84
+ const sourcesNode = document.get("sources");
85
+ const sources = {};
86
+ if (sourcesNode !== null && sourcesNode !== undefined) {
87
+ if (!isMap(sourcesNode))
88
+ throw new Error("Parity task sources must be a mapping of layer to path.");
89
+ for (const layer of CONTEXT_LAYERS) {
90
+ const value = sourcesNode.get(layer);
91
+ if (value === null || value === undefined)
92
+ continue;
93
+ if (typeof value !== "string" || !value.trim())
94
+ throw new Error(`Parity task source for ${layer} must be a non-empty project-relative path.`);
95
+ if (isAbsolute(value) || value.split(/[\\/]/).includes(".."))
96
+ throw new Error(`Parity task source for ${layer} must be a safe project-relative path: ${value}`);
97
+ sources[layer] = value;
98
+ }
99
+ }
100
+ return {
101
+ schema: PARITY_TASK_SCHEMA,
102
+ id,
103
+ description,
104
+ layers,
105
+ criticalFacts,
106
+ completionMarkers,
107
+ ...(Object.keys(sources).length ? { sources } : {}),
108
+ ...(requiredPatterns.length ? { requiredPatterns } : {}),
109
+ };
110
+ }
111
+ /**
112
+ * Load declared artifact sources into their layers (inter-stage parity).
113
+ * Source content is appended after any declared layer content.
114
+ */
115
+ export function materializeParityTask(task, readSource) {
116
+ if (!task.sources)
117
+ return task;
118
+ const layers = { ...task.layers };
119
+ for (const [layer, path] of Object.entries(task.sources)) {
120
+ let content;
121
+ try {
122
+ content = readSource(path);
123
+ }
124
+ catch (error) {
125
+ throw new Error(`Parity task ${task.id} cannot read source ${path}: ${error.message}`, {
126
+ cause: error,
127
+ });
128
+ }
129
+ layers[layer] = `${layers[layer] ? `${layers[layer].trimEnd()}\n\n` : ""}${content}`;
130
+ }
131
+ return { ...task, layers };
132
+ }
133
+ /** Run one task: baseline (full context) vs the assembler under test. Exits are pass/fail only. */
134
+ export function runParityTask(task, assembler = TASK_ASSEMBLER) {
135
+ const store = {
136
+ layers: Object.fromEntries(CONTEXT_LAYERS.map((layer) => [layer, (task.layers[layer] ?? "").trim()])),
137
+ };
138
+ const baseline = FULL_ASSEMBLER.assemble(store);
139
+ const assembled = assembler.assemble(store);
140
+ const missingFacts = task.criticalFacts.filter((fact) => !assembled.includes(fact));
141
+ const missingMarkers = task.completionMarkers.filter((marker) => !assembled.includes(marker));
142
+ const missingPatterns = [];
143
+ if (task.requiredPatterns?.length) {
144
+ const sourceContent = CONTEXT_LAYERS.map((layer) => store.layers[layer]).join("\n");
145
+ for (const pattern of task.requiredPatterns) {
146
+ const matches = new Set(sourceContent.match(new RegExp(pattern, "g")) ?? []);
147
+ for (const match of matches)
148
+ if (!assembled.includes(match))
149
+ missingPatterns.push(`${pattern} -> ${match}`);
150
+ }
151
+ }
152
+ const baselineTokens = estimateTokens(baseline);
153
+ const assembledTokens = estimateTokens(assembled);
154
+ return {
155
+ id: task.id,
156
+ passed: missingFacts.length === 0 && missingMarkers.length === 0 && missingPatterns.length === 0,
157
+ factsTotal: task.criticalFacts.length,
158
+ factsRetained: task.criticalFacts.length - missingFacts.length,
159
+ missingFacts,
160
+ missingMarkers,
161
+ missingPatterns,
162
+ baselineTokens,
163
+ assembledTokens,
164
+ tokenDelta: assembledTokens - baselineTokens,
165
+ };
166
+ }
167
+ export function runParitySuite(tasks, assembler = TASK_ASSEMBLER) {
168
+ const results = tasks.map((task) => runParityTask(task, assembler));
169
+ return {
170
+ schema: PARITY_TASK_SCHEMA,
171
+ assembler: assembler.name,
172
+ total: results.length,
173
+ passed: results.filter((result) => result.passed).length,
174
+ failed: results.filter((result) => !result.passed).length,
175
+ totalBaselineTokens: results.reduce((total, result) => total + result.baselineTokens, 0),
176
+ totalAssembledTokens: results.reduce((total, result) => total + result.assembledTokens, 0),
177
+ results,
178
+ };
179
+ }
180
+ export function serializeParitySuite(result) {
181
+ const lines = [
182
+ `schema: ${result.schema}`,
183
+ `assembler: ${result.assembler}`,
184
+ `tasks: ${result.total}`,
185
+ `passed: ${result.passed}`,
186
+ `failed: ${result.failed}`,
187
+ `total_baseline_tokens: ${result.totalBaselineTokens}`,
188
+ `total_assembled_tokens: ${result.totalAssembledTokens}`,
189
+ "token_estimate: characters/4 (estimated; quality gates are exact containment checks)",
190
+ "results:",
191
+ ];
192
+ for (const entry of result.results) {
193
+ lines.push(` - id: ${entry.id}`, ` passed: ${entry.passed}`, ` facts: ${entry.factsRetained}/${entry.factsTotal}`, ` baseline_tokens: ${entry.baselineTokens}`, ` assembled_tokens: ${entry.assembledTokens}`);
194
+ if (entry.missingFacts.length)
195
+ lines.push(...entry.missingFacts.map((fact) => ` missing_fact: ${fact}`));
196
+ if (entry.missingMarkers.length)
197
+ lines.push(...entry.missingMarkers.map((marker) => ` missing_marker: ${marker}`));
198
+ if (entry.missingPatterns.length)
199
+ lines.push(...entry.missingPatterns.map((pattern) => ` missing_pattern: ${pattern}`));
200
+ }
201
+ return lines.join("\n") + "\n";
202
+ }
@@ -10,6 +10,8 @@ export type TraceabilityGraph = {
10
10
  schema: 1;
11
11
  links: TraceabilityLink[];
12
12
  };
13
+ /** Parse a serialized traceability graph (round-trips serializeTraceabilityGraph). */
14
+ export declare function parseTraceabilityGraph(content: string): TraceabilityGraph;
13
15
  type TraceabilityInput = {
14
16
  specification: string;
15
17
  plan: string;
@@ -1,3 +1,38 @@
1
+ /** Parse a serialized traceability graph (round-trips serializeTraceabilityGraph). */
2
+ export function parseTraceabilityGraph(content) {
3
+ if (!content.trim())
4
+ throw new Error("Traceability graph is empty.");
5
+ const schema = content.match(/^schema: (\d+)$/m)?.[1];
6
+ if (schema !== "1")
7
+ throw new Error("Traceability graph schema must be version 1.");
8
+ const links = [];
9
+ for (const match of content.matchAll(/^ {2}- requirement: (REQ-\d+)$/gm)) {
10
+ const requirement = match[1];
11
+ const blockStart = (match.index ?? 0) + match[0].length;
12
+ const next = content.slice(blockStart).match(/^ {2}- requirement: /m);
13
+ const block = content.slice(blockStart, next?.index === undefined ? undefined : blockStart + next.index);
14
+ const listOf = (field) => {
15
+ const raw = block?.match(new RegExp(`^ {4}${field}: (.+)$`, "m"))?.[1] ?? "[]";
16
+ if (raw === "[]")
17
+ return [];
18
+ return raw
19
+ .slice(1, -1)
20
+ .split(/,\s*/)
21
+ .map((value) => (value.startsWith('"') && value.endsWith('"') ? JSON.parse(value) : value));
22
+ };
23
+ links.push({
24
+ requirement,
25
+ decisions: listOf("decisions"),
26
+ tasks: listOf("tasks"),
27
+ code: listOf("code"),
28
+ tests: listOf("tests"),
29
+ evidence: listOf("evidence"),
30
+ });
31
+ }
32
+ if (!links.length)
33
+ throw new Error("Traceability graph has no requirement links.");
34
+ return { schema: 1, links };
35
+ }
1
36
  const uniqueSorted = (values) => [...new Set(values)].sort();
2
37
  function valuesForField(block, field) {
3
38
  const expression = new RegExp(`^\\s*-?\\s*${field}:\\s*(.+)$`, "gim");
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Usage sidecar (switchboard pattern): per-run token records with billing
3
+ * tiers, measurement labels, and harness attribution, stored alongside
4
+ * workflow checkpoints.
5
+ *
6
+ * Honest-measurement rules (docs/token-efficiency-study-notes.md):
7
+ * - every entry carries `measured` or `estimated`; aggregates propagate the
8
+ * label pessimistically (any estimated component makes the total estimated);
9
+ * - no timestamps: entries are ordered by a monotonic sequence number, so
10
+ * unchanged input produces byte-identical output;
11
+ * - naive `tokens x price` accounting overstates savings on cache-heavy
12
+ * agents, so tiers are kept separate and cache hit rate is a first-class
13
+ * metric. Cost conversion is deliberately not built in: provider prices
14
+ * rot, and unlabeled dollars would be dishonest by default.
15
+ */
16
+ export declare const USAGE_SIDECAR_SCHEMA = 1;
17
+ export declare const USAGE_SIDECAR_PATH = "checkpoints/usage-sidecar.yml";
18
+ export declare const USAGE_SIDECAR_CAP = 500;
19
+ export declare const USAGE_TIERS: readonly ["cache-read", "fresh-input", "cache-write", "output"];
20
+ export type UsageTier = (typeof USAGE_TIERS)[number];
21
+ export type UsageLabel = "measured" | "estimated";
22
+ export type UsageSidecarEntry = {
23
+ seq: number;
24
+ command: string;
25
+ label: UsageLabel;
26
+ tiers: Record<UsageTier, number>;
27
+ model?: string;
28
+ harness?: string;
29
+ harnessVersion?: string;
30
+ requirement?: string;
31
+ /** Correlates entries with one SDD task run (`aiw task start --id=` or ticket), start to merged PR. */
32
+ workflow?: string;
33
+ };
34
+ export type UsageSidecarSummary = {
35
+ entries: number;
36
+ label: UsageLabel;
37
+ tiers: Record<UsageTier, number>;
38
+ cacheHitRate?: number;
39
+ byModel: {
40
+ model: string;
41
+ tokens: number;
42
+ label: UsageLabel;
43
+ }[];
44
+ byHarness: {
45
+ harness: string;
46
+ tokens: number;
47
+ label: UsageLabel;
48
+ }[];
49
+ };
50
+ export declare function validateUsageSidecarEntry(entry: UsageSidecarEntry): UsageSidecarEntry;
51
+ export declare function parseUsageSidecar(content: string): UsageSidecarEntry[];
52
+ export declare function serializeUsageSidecar(entries: UsageSidecarEntry[]): string;
53
+ /** Append an entry, assigning the next sequence number. Drops nothing: the ledger is capped by the caller. */
54
+ export declare function appendUsageSidecarEntry(entries: UsageSidecarEntry[], entry: Omit<UsageSidecarEntry, "seq">): UsageSidecarEntry[];
55
+ export declare function summarizeUsageSidecar(entries: UsageSidecarEntry[]): UsageSidecarSummary;
56
+ export declare function serializeUsageSidecarSummary(summary: UsageSidecarSummary): string;
@@ -0,0 +1,187 @@
1
+ import { isSeq, parseDocument } from "yaml";
2
+ /**
3
+ * Usage sidecar (switchboard pattern): per-run token records with billing
4
+ * tiers, measurement labels, and harness attribution, stored alongside
5
+ * workflow checkpoints.
6
+ *
7
+ * Honest-measurement rules (docs/token-efficiency-study-notes.md):
8
+ * - every entry carries `measured` or `estimated`; aggregates propagate the
9
+ * label pessimistically (any estimated component makes the total estimated);
10
+ * - no timestamps: entries are ordered by a monotonic sequence number, so
11
+ * unchanged input produces byte-identical output;
12
+ * - naive `tokens x price` accounting overstates savings on cache-heavy
13
+ * agents, so tiers are kept separate and cache hit rate is a first-class
14
+ * metric. Cost conversion is deliberately not built in: provider prices
15
+ * rot, and unlabeled dollars would be dishonest by default.
16
+ */
17
+ export const USAGE_SIDECAR_SCHEMA = 1;
18
+ export const USAGE_SIDECAR_PATH = "checkpoints/usage-sidecar.yml";
19
+ export const USAGE_SIDECAR_CAP = 500;
20
+ export const USAGE_TIERS = ["cache-read", "fresh-input", "cache-write", "output"];
21
+ export function validateUsageSidecarEntry(entry) {
22
+ if (!Number.isInteger(entry.seq) || entry.seq < 1)
23
+ throw new Error("Usage sidecar seq must be a positive integer.");
24
+ if (!entry.command.trim())
25
+ throw new Error("Usage sidecar command must not be empty.");
26
+ for (const tier of USAGE_TIERS) {
27
+ const value = entry.tiers[tier];
28
+ if (!Number.isInteger(value) || value < 0)
29
+ throw new Error(`Usage sidecar tier ${tier} must be a non-negative integer.`);
30
+ }
31
+ if (!USAGE_TIERS.some((tier) => entry.tiers[tier] > 0))
32
+ throw new Error("Usage sidecar entry needs at least one non-zero tier.");
33
+ if (entry.model !== undefined && !entry.model.trim())
34
+ throw new Error("Usage sidecar model must be a non-empty string when present.");
35
+ if (entry.requirement !== undefined && !/^REQ-\d+$/.test(entry.requirement))
36
+ throw new Error("Usage sidecar requirement must use the REQ-### identifier format.");
37
+ if (entry.workflow !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(entry.workflow))
38
+ throw new Error("Usage sidecar workflow must be a workflow id or ticket identifier without spaces.");
39
+ for (const [name, value] of [
40
+ ["harness", entry.harness],
41
+ ["harnessVersion", entry.harnessVersion],
42
+ ])
43
+ if (value !== undefined && !value.trim())
44
+ throw new Error(`Usage sidecar ${name} must be a non-empty string when present.`);
45
+ return entry;
46
+ }
47
+ export function parseUsageSidecar(content) {
48
+ if (!content.trim())
49
+ return [];
50
+ const document = parseDocument(content);
51
+ if (document.errors.length)
52
+ throw new Error("Usage sidecar is not valid YAML.");
53
+ const node = document.get("entries");
54
+ if (node === null || node === undefined)
55
+ throw new Error("Usage sidecar is missing entries.");
56
+ if (!isSeq(node))
57
+ throw new Error("Usage sidecar entries must be a list.");
58
+ const schema = document.get("schema");
59
+ if (schema !== USAGE_SIDECAR_SCHEMA)
60
+ throw new Error(`Usage sidecar schema must be version ${USAGE_SIDECAR_SCHEMA}.`);
61
+ const entries = node.items.map((raw) => {
62
+ const pick = (key) => typeof raw.get === "function"
63
+ ? raw.get(key)
64
+ : raw[key];
65
+ const tiers = {};
66
+ const rawTiers = pick("tiers");
67
+ if (rawTiers === null || typeof rawTiers !== "object")
68
+ throw new Error("Usage sidecar entry is missing tiers.");
69
+ const readTier = (tier) => typeof rawTiers.get === "function"
70
+ ? rawTiers.get(tier)
71
+ : rawTiers[tier];
72
+ for (const tier of USAGE_TIERS) {
73
+ const value = readTier(tier);
74
+ if (value !== null && value !== undefined)
75
+ tiers[tier] = value;
76
+ }
77
+ const optional = (key) => {
78
+ const value = pick(key);
79
+ return value === null || value === undefined ? undefined : String(value);
80
+ };
81
+ return validateUsageSidecarEntry({
82
+ seq: pick("seq"),
83
+ command: String(pick("command")),
84
+ label: pick("label"),
85
+ tiers: Object.fromEntries(USAGE_TIERS.map((tier) => [tier, tiers[tier] ?? 0])),
86
+ ...(optional("model") === undefined ? {} : { model: optional("model") }),
87
+ ...(optional("harness") === undefined ? {} : { harness: optional("harness") }),
88
+ ...(optional("harness_version") === undefined
89
+ ? {}
90
+ : { harnessVersion: optional("harness_version") }),
91
+ ...(optional("requirement") === undefined ? {} : { requirement: optional("requirement") }),
92
+ ...(optional("workflow") === undefined ? {} : { workflow: optional("workflow") }),
93
+ });
94
+ });
95
+ const sequences = entries.map((entry) => entry.seq);
96
+ if (new Set(sequences).size !== sequences.length)
97
+ throw new Error("Usage sidecar sequence numbers must be unique.");
98
+ return entries;
99
+ }
100
+ export function serializeUsageSidecar(entries) {
101
+ const lines = [`schema: ${USAGE_SIDECAR_SCHEMA}`, "entries:"];
102
+ for (const entry of entries) {
103
+ lines.push(` - seq: ${entry.seq}`, ` command: ${entry.command}`, ` label: ${entry.label}`, " tiers:", ...USAGE_TIERS.map((tier) => ` ${tier}: ${entry.tiers[tier]}`), ...(entry.model === undefined ? [] : [` model: ${entry.model}`]), ...(entry.harness === undefined ? [] : [` harness: ${entry.harness}`]), ...(entry.harnessVersion === undefined
104
+ ? []
105
+ : [` harness_version: ${entry.harnessVersion}`]), ...(entry.requirement === undefined ? [] : [` requirement: ${entry.requirement}`]), ...(entry.workflow === undefined ? [] : [` workflow: ${entry.workflow}`]));
106
+ }
107
+ return lines.join("\n") + "\n";
108
+ }
109
+ /** Append an entry, assigning the next sequence number. Drops nothing: the ledger is capped by the caller. */
110
+ export function appendUsageSidecarEntry(entries, entry) {
111
+ validateUsageSidecarEntry({ ...entry, seq: 1 });
112
+ const nextSeq = entries.reduce((max, existing) => Math.max(max, existing.seq), 0) + 1;
113
+ const recorded = [...entries, { ...entry, seq: nextSeq }];
114
+ return recorded.length > USAGE_SIDECAR_CAP
115
+ ? recorded.slice(recorded.length - USAGE_SIDECAR_CAP)
116
+ : recorded;
117
+ }
118
+ const totalTokens = (entry) => USAGE_TIERS.reduce((total, tier) => total + entry.tiers[tier], 0);
119
+ export function summarizeUsageSidecar(entries) {
120
+ if (!entries.length)
121
+ return {
122
+ entries: 0,
123
+ label: "measured",
124
+ tiers: Object.fromEntries(USAGE_TIERS.map((tier) => [tier, 0])),
125
+ cacheHitRate: undefined,
126
+ byModel: [],
127
+ byHarness: [],
128
+ };
129
+ const tiers = Object.fromEntries(USAGE_TIERS.map((tier) => [
130
+ tier,
131
+ entries.reduce((total, entry) => total + entry.tiers[tier], 0),
132
+ ]));
133
+ const billableInput = tiers["cache-read"] + tiers["fresh-input"];
134
+ const group = (key) => {
135
+ const map = new Map();
136
+ for (const entry of entries) {
137
+ const name = entry[key] ?? "unattributed";
138
+ const bucket = map.get(name) ?? { tokens: 0, measured: true };
139
+ bucket.tokens += totalTokens(entry);
140
+ bucket.measured = bucket.measured && entry.label === "measured";
141
+ map.set(name, bucket);
142
+ }
143
+ return [...map]
144
+ .map(([name, { tokens, measured }]) => ({
145
+ name,
146
+ tokens,
147
+ label: measured ? "measured" : "estimated",
148
+ }))
149
+ .sort((a, b) => b.tokens - a.tokens || a.name.localeCompare(b.name));
150
+ };
151
+ return {
152
+ entries: entries.length,
153
+ label: entries.every((entry) => entry.label === "measured") ? "measured" : "estimated",
154
+ tiers,
155
+ ...(billableInput > 0 ? { cacheHitRate: tiers["cache-read"] / billableInput } : {}),
156
+ byModel: group("model").map(({ name, tokens, label }) => ({ model: name, tokens, label })),
157
+ byHarness: group("harness").map(({ name, tokens, label }) => ({
158
+ harness: name,
159
+ tokens,
160
+ label,
161
+ })),
162
+ };
163
+ }
164
+ export function serializeUsageSidecarSummary(summary) {
165
+ if (!summary.entries)
166
+ return "No usage sidecar entries recorded.";
167
+ const percent = summary.cacheHitRate === undefined ? undefined : `${Math.round(summary.cacheHitRate * 100)}%`;
168
+ return [
169
+ `entries: ${summary.entries} (label: ${summary.label})`,
170
+ ...USAGE_TIERS.map((tier) => ` ${tier}: ${summary.tiers[tier].toLocaleString("en-US")} tokens`),
171
+ ...(percent === undefined
172
+ ? []
173
+ : [` cache_hit_rate: ${percent} (cache-read / billable input)`]),
174
+ ...(summary.byModel.length
175
+ ? [
176
+ "by model:",
177
+ ...summary.byModel.map(({ model, tokens, label }) => ` - ${model}: ${tokens.toLocaleString("en-US")} (${label})`),
178
+ ]
179
+ : []),
180
+ ...(summary.byHarness.length
181
+ ? [
182
+ "by harness:",
183
+ ...summary.byHarness.map(({ harness, tokens, label }) => ` - ${harness}: ${tokens.toLocaleString("en-US")} (${label})`),
184
+ ]
185
+ : []),
186
+ ].join("\n");
187
+ }