@davidorex/pi-workflows 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 (215) hide show
  1. package/CHANGELOG.md +7 -4
  2. package/README.md +12 -2
  3. package/agents/architecture-designer.agent.yaml +58 -0
  4. package/agents/architecture-inferrer.agent.yaml +60 -0
  5. package/agents/gap-identifier.agent.yaml +66 -0
  6. package/agents/handoff-writer.agent.yaml +64 -0
  7. package/agents/plan-creator.agent.yaml +63 -0
  8. package/agents/project-definer.agent.yaml +53 -0
  9. package/agents/project-inferrer.agent.yaml +63 -0
  10. package/agents/requirements-gatherer.agent.yaml +58 -0
  11. package/dist/agent-spec.d.ts +27 -0
  12. package/dist/agent-spec.d.ts.map +1 -0
  13. package/dist/agent-spec.js +112 -0
  14. package/dist/agent-spec.js.map +1 -0
  15. package/dist/checkpoint.d.ts +50 -0
  16. package/dist/checkpoint.d.ts.map +1 -0
  17. package/dist/checkpoint.js +109 -0
  18. package/dist/checkpoint.js.map +1 -0
  19. package/dist/completion.d.ts +18 -0
  20. package/dist/completion.d.ts.map +1 -0
  21. package/dist/completion.js +58 -0
  22. package/dist/completion.js.map +1 -0
  23. package/dist/dag.d.ts +56 -0
  24. package/dist/dag.d.ts.map +1 -0
  25. package/dist/dag.js +172 -0
  26. package/dist/dag.js.map +1 -0
  27. package/dist/dispatch.d.ts +35 -0
  28. package/dist/dispatch.d.ts.map +1 -0
  29. package/dist/dispatch.js +311 -0
  30. package/dist/dispatch.js.map +1 -0
  31. package/dist/expression.d.ts +64 -0
  32. package/dist/expression.d.ts.map +1 -0
  33. package/dist/expression.js +294 -0
  34. package/dist/expression.js.map +1 -0
  35. package/dist/format.d.ts +19 -0
  36. package/dist/format.d.ts.map +1 -0
  37. package/dist/format.js +42 -0
  38. package/dist/format.js.map +1 -0
  39. package/dist/index.d.ts +8 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +675 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/output.d.ts +12 -0
  44. package/dist/output.d.ts.map +1 -0
  45. package/dist/output.js +59 -0
  46. package/dist/output.js.map +1 -0
  47. package/dist/state.d.ts +66 -0
  48. package/dist/state.d.ts.map +1 -0
  49. package/dist/state.js +227 -0
  50. package/dist/state.js.map +1 -0
  51. package/dist/step-agent.d.ts +35 -0
  52. package/dist/step-agent.d.ts.map +1 -0
  53. package/dist/step-agent.js +156 -0
  54. package/dist/step-agent.js.map +1 -0
  55. package/dist/step-command.d.ts +21 -0
  56. package/dist/step-command.d.ts.map +1 -0
  57. package/dist/step-command.js +150 -0
  58. package/dist/step-command.js.map +1 -0
  59. package/dist/step-foreach.d.ts +33 -0
  60. package/dist/step-foreach.d.ts.map +1 -0
  61. package/dist/step-foreach.js +112 -0
  62. package/dist/step-foreach.js.map +1 -0
  63. package/dist/step-gate.d.ts +18 -0
  64. package/dist/step-gate.d.ts.map +1 -0
  65. package/dist/step-gate.js +137 -0
  66. package/dist/step-gate.js.map +1 -0
  67. package/dist/step-loop.d.ts +32 -0
  68. package/dist/step-loop.d.ts.map +1 -0
  69. package/dist/step-loop.js +278 -0
  70. package/dist/step-loop.js.map +1 -0
  71. package/dist/step-monitor.d.ts +56 -0
  72. package/dist/step-monitor.d.ts.map +1 -0
  73. package/dist/step-monitor.js +317 -0
  74. package/dist/step-monitor.js.map +1 -0
  75. package/dist/step-parallel.d.ts +43 -0
  76. package/dist/step-parallel.d.ts.map +1 -0
  77. package/dist/step-parallel.js +122 -0
  78. package/dist/step-parallel.js.map +1 -0
  79. package/{src/step-pause.ts → dist/step-pause.d.ts} +3 -13
  80. package/dist/step-pause.d.ts.map +1 -0
  81. package/dist/step-pause.js +22 -0
  82. package/dist/step-pause.js.map +1 -0
  83. package/dist/step-shared.d.ts +57 -0
  84. package/dist/step-shared.d.ts.map +1 -0
  85. package/dist/step-shared.js +125 -0
  86. package/dist/step-shared.js.map +1 -0
  87. package/dist/step-transform.d.ts +11 -0
  88. package/dist/step-transform.d.ts.map +1 -0
  89. package/dist/step-transform.js +41 -0
  90. package/dist/step-transform.js.map +1 -0
  91. package/dist/template.d.ts +51 -0
  92. package/dist/template.d.ts.map +1 -0
  93. package/{src/template.ts → dist/template.js} +32 -38
  94. package/dist/template.js.map +1 -0
  95. package/dist/test-helpers.d.ts +17 -0
  96. package/dist/test-helpers.d.ts.map +1 -0
  97. package/dist/test-helpers.js +46 -0
  98. package/dist/test-helpers.js.map +1 -0
  99. package/dist/tui.d.ts +47 -0
  100. package/dist/tui.d.ts.map +1 -0
  101. package/dist/tui.js +143 -0
  102. package/dist/tui.js.map +1 -0
  103. package/dist/types.d.ts +164 -0
  104. package/dist/types.d.ts.map +1 -0
  105. package/dist/types.js +3 -0
  106. package/dist/types.js.map +1 -0
  107. package/dist/workflow-discovery.d.ts +23 -0
  108. package/dist/workflow-discovery.d.ts.map +1 -0
  109. package/dist/workflow-discovery.js +103 -0
  110. package/dist/workflow-discovery.js.map +1 -0
  111. package/dist/workflow-executor.d.ts +50 -0
  112. package/dist/workflow-executor.d.ts.map +1 -0
  113. package/dist/workflow-executor.js +726 -0
  114. package/dist/workflow-executor.js.map +1 -0
  115. package/dist/workflow-sdk.d.ts +44 -0
  116. package/dist/workflow-sdk.d.ts.map +1 -0
  117. package/dist/workflow-sdk.js +438 -0
  118. package/dist/workflow-sdk.js.map +1 -0
  119. package/dist/workflow-spec.d.ts +33 -0
  120. package/dist/workflow-spec.d.ts.map +1 -0
  121. package/dist/workflow-spec.js +464 -0
  122. package/dist/workflow-spec.js.map +1 -0
  123. package/dist/workflows-dir.d.ts +3 -0
  124. package/dist/workflows-dir.d.ts.map +1 -0
  125. package/{src/workflows-dir.ts → dist/workflows-dir.js} +1 -0
  126. package/dist/workflows-dir.js.map +1 -0
  127. package/package.json +53 -38
  128. package/schemas/decomposition-specs.schema.json +48 -48
  129. package/schemas/execution-results.schema.json +48 -48
  130. package/schemas/investigation-findings.schema.json +43 -43
  131. package/schemas/pattern-analysis.schema.json +41 -41
  132. package/schemas/phase.schema.json +48 -0
  133. package/schemas/plan-breakdown.schema.json +20 -20
  134. package/schemas/quality-analysis.schema.json +33 -33
  135. package/schemas/research-findings.schema.json +42 -42
  136. package/schemas/structure-analysis.schema.json +40 -40
  137. package/schemas/synthesis.schema.json +31 -31
  138. package/schemas/verifier-output.schema.json +94 -94
  139. package/skill-narrative.md +84 -0
  140. package/skills/pi-workflows/SKILL.md +247 -0
  141. package/templates/architecture-designer/task.md +117 -0
  142. package/templates/architecture-inferrer/task.md +61 -0
  143. package/templates/gap-identifier/task.md +103 -0
  144. package/templates/handoff-writer/task.md +91 -0
  145. package/templates/plan-creator/task.md +143 -0
  146. package/templates/project-definer/task.md +67 -0
  147. package/templates/project-inferrer/task.md +56 -0
  148. package/templates/requirements-gatherer/task.md +90 -0
  149. package/workflows/analyze-existing-project.workflow.yaml +74 -0
  150. package/workflows/create-handoff.workflow.yaml +58 -0
  151. package/workflows/create-phase.workflow.yaml +1 -1
  152. package/workflows/do-gap.workflow.yaml +4 -4
  153. package/workflows/fix-audit.workflow.yaml +1 -1
  154. package/workflows/gap-to-phase.workflow.yaml +1 -1
  155. package/workflows/init-new-project.workflow.yaml +55 -0
  156. package/workflows/plan-from-requirements.workflow.yaml +51 -0
  157. package/workflows/self-implement.workflow.yaml +3 -3
  158. package/workflows/typed-analysis.workflow.yaml +4 -4
  159. package/src/agent-spec.test.ts +0 -289
  160. package/src/agent-spec.ts +0 -122
  161. package/src/block-validation.test.ts +0 -350
  162. package/src/checkpoint.test.ts +0 -237
  163. package/src/checkpoint.ts +0 -140
  164. package/src/completion.test.ts +0 -143
  165. package/src/completion.ts +0 -68
  166. package/src/dag.test.ts +0 -350
  167. package/src/dag.ts +0 -219
  168. package/src/dispatch.test.ts +0 -475
  169. package/src/dispatch.ts +0 -353
  170. package/src/expression.test.ts +0 -353
  171. package/src/expression.ts +0 -332
  172. package/src/format.test.ts +0 -80
  173. package/src/format.ts +0 -41
  174. package/src/graduated-failure.test.ts +0 -556
  175. package/src/index.test.ts +0 -114
  176. package/src/index.ts +0 -516
  177. package/src/loop.test.ts +0 -551
  178. package/src/output.test.ts +0 -213
  179. package/src/output.ts +0 -70
  180. package/src/parallel-integration.test.ts +0 -175
  181. package/src/resume.test.ts +0 -192
  182. package/src/state.test.ts +0 -192
  183. package/src/state.ts +0 -254
  184. package/src/step-agent.test.ts +0 -327
  185. package/src/step-agent.ts +0 -178
  186. package/src/step-command.test.ts +0 -330
  187. package/src/step-command.ts +0 -132
  188. package/src/step-foreach.test.ts +0 -647
  189. package/src/step-foreach.ts +0 -148
  190. package/src/step-gate.test.ts +0 -185
  191. package/src/step-gate.ts +0 -116
  192. package/src/step-loop.test.ts +0 -626
  193. package/src/step-loop.ts +0 -323
  194. package/src/step-parallel.test.ts +0 -475
  195. package/src/step-parallel.ts +0 -168
  196. package/src/step-pause.test.ts +0 -30
  197. package/src/step-shared.test.ts +0 -355
  198. package/src/step-shared.ts +0 -157
  199. package/src/step-transform.test.ts +0 -155
  200. package/src/step-transform.ts +0 -47
  201. package/src/template-integration.test.ts +0 -72
  202. package/src/template.test.ts +0 -162
  203. package/src/test-helpers.ts +0 -51
  204. package/src/tui.test.ts +0 -355
  205. package/src/tui.ts +0 -182
  206. package/src/types.ts +0 -195
  207. package/src/verifier-schema.test.ts +0 -226
  208. package/src/workflow-discovery.test.ts +0 -105
  209. package/src/workflow-discovery.ts +0 -114
  210. package/src/workflow-executor.test.ts +0 -1532
  211. package/src/workflow-executor.ts +0 -810
  212. package/src/workflow-sdk.test.ts +0 -238
  213. package/src/workflow-sdk.ts +0 -262
  214. package/src/workflow-spec.test.ts +0 -576
  215. package/src/workflow-spec.ts +0 -471
package/src/expression.ts DELETED
@@ -1,332 +0,0 @@
1
- import type { StepResult } from "./types.ts";
2
- import { formatDuration, formatCost } from "./format.ts";
3
-
4
- const EXPR_PATTERN = /\$\{\{\s*(.*?)\s*\}\}/g;
5
-
6
- /**
7
- * Built-in filters for expressions.
8
- * Applied via pipe syntax: ${{ totalDurationMs | duration }}
9
- */
10
- const FILTERS: Record<string, (value: unknown) => unknown> = {
11
- duration: (v) => formatDuration(Number(v)),
12
- currency: (v) => formatCost(Number(v)),
13
- json: (v) => JSON.stringify(v, null, 2),
14
- length: (v) => Array.isArray(v) ? v.length : typeof v === "string" ? v.length : 0,
15
- keys: (v) => typeof v === "object" && v !== null ? Object.keys(v) : [],
16
- filter: (v) => Array.isArray(v) ? v.filter(Boolean) : v,
17
- };
18
-
19
- /** Filter names derived from the FILTERS registry — add a filter above, this updates automatically. */
20
- export const FILTER_NAMES = Object.keys(FILTERS);
21
-
22
- /** Known root-level expression scope keys. */
23
- export const EXPRESSION_ROOTS = ["input", "steps"] as const;
24
-
25
- /**
26
- * Error class for expression resolution failures.
27
- * Contains the original expression and a diagnostic reason.
28
- */
29
- export class ExpressionError extends Error {
30
- readonly expression: string;
31
- readonly reason: string;
32
-
33
- constructor(expression: string, reason: string) {
34
- super(`Expression error in '\${{ ${expression} }}': ${reason}`);
35
- this.name = "ExpressionError";
36
- this.expression = expression;
37
- this.reason = reason;
38
- }
39
- }
40
-
41
- /**
42
- * Resolve a single expression string (without the ${{ }} delimiters).
43
- * E.g. "steps.diagnose.output.rootCause" walks scope.steps.diagnose.output.rootCause
44
- *
45
- * Supports pipe filters: "totalDurationMs | duration"
46
- *
47
- * Scope is Record<string, unknown> — accepts ExpressionScope, CompletionScope, or any object.
48
- *
49
- * Throws ExpressionError if any segment of the path is undefined or null,
50
- * or if a filter name is unknown.
51
- */
52
- export function resolveExpression(expr: string, scope: Record<string, unknown>): unknown {
53
- // Parse optional filter: "path | filterName"
54
- const pipeIdx = expr.indexOf("|");
55
- let pathExpr: string;
56
- let filterName: string | undefined;
57
- if (pipeIdx !== -1) {
58
- pathExpr = expr.slice(0, pipeIdx).trim();
59
- filterName = expr.slice(pipeIdx + 1).trim();
60
- } else {
61
- pathExpr = expr;
62
- }
63
-
64
- const segments = pathExpr.split(".");
65
- let current: unknown = scope;
66
- const traversed: string[] = [];
67
-
68
- for (const segment of segments) {
69
- // Container is undefined/null — can't traverse further. This is a broken reference.
70
- if (current === undefined || current === null) {
71
- const reason = buildErrorReason(segments, traversed, scope);
72
- throw new ExpressionError(expr, reason);
73
- }
74
-
75
- current = (current as Record<string, unknown>)[segment];
76
- traversed.push(segment);
77
-
78
- // Property doesn't exist on the container — return undefined (optional field).
79
- // But if this is the first segment (root lookup like "steps" or "input"),
80
- // or if we're looking up a step name that hasn't executed, that's an error.
81
- if (current === undefined) {
82
- // Root-level miss (e.g. "typo.something") — always an error
83
- if (traversed.length === 1) {
84
- const reason = buildErrorReason(segments, traversed, scope);
85
- throw new ExpressionError(expr, reason);
86
- }
87
- // Step reference that doesn't exist (e.g. "steps.nonexistent") — error
88
- if (segments[0] === "steps" && traversed.length === 2) {
89
- const reason = buildErrorReason(segments, traversed, scope);
90
- throw new ExpressionError(expr, reason);
91
- }
92
- // Otherwise: optional field on an existing object — return undefined
93
- return undefined;
94
- }
95
- }
96
-
97
- // Apply filter if specified
98
- if (filterName) {
99
- const filterFn = FILTERS[filterName];
100
- if (!filterFn) {
101
- throw new ExpressionError(expr, `unknown filter '${filterName}'`);
102
- }
103
- current = filterFn(current);
104
- }
105
-
106
- return current;
107
- }
108
-
109
- /**
110
- * Resolve all ${{ }} expressions in a value.
111
- *
112
- * - If `value` is a string containing `${{ expr }}`, resolve the expression.
113
- * - If `value` is a string that IS entirely `${{ expr }}`, return the resolved value
114
- * (preserving its type -- object, array, number, etc.).
115
- * - If `value` is a string with `${{ expr }}` embedded in other text,
116
- * stringify the resolved value and interpolate.
117
- * - If `value` is an object, recursively resolve all values.
118
- * - If `value` is an array, recursively resolve all elements.
119
- * - If `value` is anything else (number, boolean, null), return as-is.
120
- *
121
- * Throws ExpressionError if a property path doesn't resolve.
122
- */
123
- export function resolveExpressions(value: unknown, scope: Record<string, unknown>): unknown {
124
- if (typeof value === "string") {
125
- return resolveStringExpressions(value, scope);
126
- }
127
-
128
- if (Array.isArray(value)) {
129
- return value.map((element) => resolveExpressions(element, scope));
130
- }
131
-
132
- if (value !== null && typeof value === "object") {
133
- const result: Record<string, unknown> = {};
134
- for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
135
- result[key] = resolveExpressions(val, scope);
136
- }
137
- return result;
138
- }
139
-
140
- // number, boolean, null, undefined — pass through
141
- return value;
142
- }
143
-
144
- /**
145
- * Resolve expressions within a string value.
146
- * Handles whole-value expressions (type-preserving) and embedded expressions (string interpolation).
147
- */
148
- function resolveStringExpressions(value: string, scope: Record<string, unknown>): unknown {
149
- // Check if the entire string is a single whole-value expression
150
- const wholeMatch = value.match(/^\$\{\{\s*(.*?)\s*\}\}$/);
151
- if (wholeMatch) {
152
- return resolveExpression(wholeMatch[1], scope);
153
- }
154
-
155
- // Check if there are any expressions at all
156
- if (!value.includes("${{")) {
157
- return value;
158
- }
159
-
160
- // Embedded expressions: resolve each and interpolate as strings
161
- return value.replace(EXPR_PATTERN, (_match, expr: string) => {
162
- const resolved = resolveExpression(expr, scope);
163
- if (resolved === undefined || resolved === null) return "";
164
- return stringify(resolved);
165
- });
166
- }
167
-
168
- /**
169
- * Comparison operators, ordered from longest to shortest.
170
- * Order matters: the first-match scan in evaluateCondition must try
171
- * longer operators before shorter ones to avoid matching "!=" when
172
- * the actual operator is "!==" (or "==" when "===" is intended).
173
- */
174
- const COMPARISON_OPS = ["!==", "===", "!=", "==", ">=", "<=", ">", "<"] as const;
175
- type ComparisonOp = typeof COMPARISON_OPS[number];
176
-
177
- /**
178
- * Parse a right-hand operand string into a typed value.
179
- * Supports: string literals ('value' or "value"), number literals, boolean literals (true/false),
180
- * null, undefined, or expression paths resolved against scope.
181
- */
182
- export function parseRightOperand(str: string, scope: Record<string, unknown>): unknown {
183
- // String literals (single or double quoted)
184
- if ((str.startsWith("'") && str.endsWith("'")) || (str.startsWith('"') && str.endsWith('"'))) {
185
- return str.slice(1, -1);
186
- }
187
-
188
- // Boolean literals
189
- if (str === "true") return true;
190
- if (str === "false") return false;
191
-
192
- // null / undefined
193
- if (str === "null") return null;
194
- if (str === "undefined") return undefined;
195
-
196
- // Number literals
197
- const num = Number(str);
198
- if (!Number.isNaN(num) && str !== "") return num;
199
-
200
- // Otherwise treat as expression path
201
- return resolveExpression(str, scope);
202
- }
203
-
204
- /**
205
- * Compare two values using a comparison operator.
206
- * == and === both use strict equality (===).
207
- */
208
- export function compare(left: unknown, right: unknown, op: string): boolean {
209
- switch (op) {
210
- case "==":
211
- case "===":
212
- return left === right;
213
- case "!=":
214
- case "!==":
215
- return left !== right;
216
- case ">":
217
- return (left as number) > (right as number);
218
- case "<":
219
- return (left as number) < (right as number);
220
- case ">=":
221
- return (left as number) >= (right as number);
222
- case "<=":
223
- return (left as number) <= (right as number);
224
- default:
225
- throw new ExpressionError(op, `unknown comparison operator '${op}'`);
226
- }
227
- }
228
-
229
- /**
230
- * Evaluate a condition expression for boolean truthiness.
231
- *
232
- * Supports:
233
- * - Simple path expressions: truthy/falsy evaluation of the resolved value
234
- * - Negation with ! prefix
235
- * - Comparison operators: ==, !=, ===, !==, >, <, >=, <=
236
- * - Right operands: string literals, number literals, boolean literals, null, undefined, or expression paths
237
- *
238
- * JS truthiness rules: undefined, null, false, 0, "" are falsy; everything else is truthy.
239
- */
240
- export function evaluateCondition(expr: string, scope: Record<string, unknown>): boolean {
241
- const trimmed = expr.trim();
242
-
243
- // Check for comparison operators (split on first occurrence)
244
- for (const op of COMPARISON_OPS) {
245
- const opIdx = trimmed.indexOf(op);
246
- if (opIdx !== -1) {
247
- const leftExpr = trimmed.slice(0, opIdx).trim();
248
- const rightStr = trimmed.slice(opIdx + op.length).trim();
249
-
250
- // Resolve left side
251
- let leftValue: unknown;
252
- if (leftExpr.startsWith("!")) {
253
- leftValue = !resolveExpressionSafe(leftExpr.slice(1).trim(), scope);
254
- } else {
255
- leftValue = resolveExpressionSafe(leftExpr, scope);
256
- }
257
-
258
- const rightValue = parseRightOperand(rightStr, scope);
259
- return compare(leftValue, rightValue, op);
260
- }
261
- }
262
-
263
- // No comparison operator — evaluate as truthy/falsy
264
- if (trimmed.startsWith("!")) {
265
- const innerExpr = trimmed.slice(1).trim();
266
- const value = resolveExpressionSafe(innerExpr, scope);
267
- return !value;
268
- }
269
-
270
- const value = resolveExpressionSafe(trimmed, scope);
271
- return !!value;
272
- }
273
-
274
- /**
275
- * Resolve an expression, returning undefined for missing optional fields
276
- * instead of throwing. Root-level misses and missing step references still throw.
277
- */
278
- function resolveExpressionSafe(expr: string, scope: Record<string, unknown>): unknown {
279
- return resolveExpression(expr, scope);
280
- }
281
-
282
- /**
283
- * Stringify a resolved value for embedding in a larger string.
284
- * Objects and arrays use JSON.stringify; primitives use String().
285
- */
286
- function stringify(value: unknown): string {
287
- if (value !== null && typeof value === "object") {
288
- return JSON.stringify(value);
289
- }
290
- return String(value);
291
- }
292
-
293
- /**
294
- * Build a diagnostic error reason based on the path traversal state.
295
- * Provides context about step status when the path starts with "steps.".
296
- */
297
- function buildErrorReason(
298
- segments: string[],
299
- traversed: string[],
300
- scope: Record<string, unknown>,
301
- ): string {
302
- const failedSegment = traversed[traversed.length - 1];
303
- const parentPath = traversed.slice(0, -1).join(".");
304
-
305
- // Special case: referencing a step that doesn't exist in scope.steps
306
- if (segments[0] === "steps" && traversed.length === 2) {
307
- const stepName = segments[1];
308
- const stepsObj = scope.steps as Record<string, StepResult | undefined> | undefined;
309
- if (stepsObj && !(stepName in stepsObj)) {
310
- return `step '${stepName}' has not been executed yet`;
311
- }
312
- }
313
-
314
- // When the path starts with "steps.", include step status context if available
315
- if (segments[0] === "steps" && segments.length >= 2) {
316
- const stepName = segments[1];
317
- const stepsObj = scope.steps as Record<string, StepResult | undefined> | undefined;
318
- if (stepsObj) {
319
- const stepResult = stepsObj[stepName];
320
- if (stepResult && parentPath) {
321
- return `property '${failedSegment}' is undefined on ${parentPath} (step '${stepName}' status: ${stepResult.status})`;
322
- }
323
- }
324
- }
325
-
326
- if (parentPath) {
327
- return `property '${failedSegment}' is undefined on ${parentPath}`;
328
- }
329
-
330
- return `property '${failedSegment}' is undefined`;
331
- }
332
-
@@ -1,80 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert";
3
- import { formatDuration, formatCost, formatTokens } from "./format.ts";
4
-
5
- describe("formatDuration", () => {
6
- it("formats zero", () => {
7
- assert.strictEqual(formatDuration(0), "0s");
8
- });
9
-
10
- it("formats sub-minute", () => {
11
- assert.strictEqual(formatDuration(42000), "42s");
12
- });
13
-
14
- it("rounds to nearest second", () => {
15
- assert.strictEqual(formatDuration(1500), "2s");
16
- assert.strictEqual(formatDuration(1499), "1s");
17
- });
18
-
19
- it("formats minutes and seconds", () => {
20
- assert.strictEqual(formatDuration(92000), "1m32s");
21
- });
22
-
23
- it("pads seconds in minute format", () => {
24
- assert.strictEqual(formatDuration(60000), "1m00s");
25
- assert.strictEqual(formatDuration(63000), "1m03s");
26
- });
27
-
28
- it("formats hours and minutes", () => {
29
- assert.strictEqual(formatDuration(3720000), "1h02m");
30
- });
31
-
32
- it("formats exact hour", () => {
33
- assert.strictEqual(formatDuration(3600000), "1h00m");
34
- });
35
- });
36
-
37
- describe("formatCost", () => {
38
- it("formats zero", () => {
39
- assert.strictEqual(formatCost(0), "$0.00");
40
- });
41
-
42
- it("formats small cost", () => {
43
- assert.strictEqual(formatCost(0.03), "$0.03");
44
- });
45
-
46
- it("formats larger cost", () => {
47
- assert.strictEqual(formatCost(1.5), "$1.50");
48
- });
49
-
50
- it("rounds to two decimal places", () => {
51
- assert.strictEqual(formatCost(0.999), "$1.00");
52
- assert.strictEqual(formatCost(0.001), "$0.00");
53
- });
54
- });
55
-
56
- describe("formatTokens", () => {
57
- it("formats small counts as-is", () => {
58
- assert.strictEqual(formatTokens(0), "0 tok");
59
- assert.strictEqual(formatTokens(42), "42 tok");
60
- assert.strictEqual(formatTokens(999), "999 tok");
61
- });
62
-
63
- it("formats 1k-10k with one decimal", () => {
64
- assert.strictEqual(formatTokens(1000), "1.0k tok");
65
- assert.strictEqual(formatTokens(1234), "1.2k tok");
66
- assert.strictEqual(formatTokens(9999), "10.0k tok");
67
- });
68
-
69
- it("formats 10k-1M as rounded k", () => {
70
- assert.strictEqual(formatTokens(10000), "10k tok");
71
- assert.strictEqual(formatTokens(12345), "12k tok");
72
- assert.strictEqual(formatTokens(999999), "1000k tok");
73
- });
74
-
75
- it("formats 1M+ with one decimal", () => {
76
- assert.strictEqual(formatTokens(1000000), "1.0M tok");
77
- assert.strictEqual(formatTokens(1500000), "1.5M tok");
78
- assert.strictEqual(formatTokens(2345678), "2.3M tok");
79
- });
80
- });
package/src/format.ts DELETED
@@ -1,41 +0,0 @@
1
- /**
2
- * Shared formatting utilities for duration and cost display.
3
- * Used by state.ts (formatResult), expression.ts (filters), and tui.ts (widget).
4
- */
5
-
6
- /**
7
- * Format duration in milliseconds to human-readable string.
8
- * <60s -> "42s", <60m -> "1m32s", >=60m -> "1h02m"
9
- */
10
- export function formatDuration(ms: number): string {
11
- const totalSeconds = Math.round(ms / 1000);
12
- if (totalSeconds < 60) {
13
- return `${totalSeconds}s`;
14
- }
15
- const minutes = Math.floor(totalSeconds / 60);
16
- const seconds = totalSeconds % 60;
17
- if (minutes < 60) {
18
- return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
19
- }
20
- const hours = Math.floor(minutes / 60);
21
- const remainingMinutes = minutes % 60;
22
- return `${hours}h${remainingMinutes.toString().padStart(2, "0")}m`;
23
- }
24
-
25
- /**
26
- * Format cost as a dollar string.
27
- */
28
- export function formatCost(cost: number): string {
29
- return `$${cost.toFixed(2)}`;
30
- }
31
-
32
- /**
33
- * Format token count as compact human-readable string.
34
- * <1000 -> "123 tok", <10000 -> "1.2k tok", <1000000 -> "42k tok", >=1000000 -> "1.2M tok"
35
- */
36
- export function formatTokens(count: number): string {
37
- if (count < 1000) return `${count} tok`;
38
- if (count < 10000) return `${(count / 1000).toFixed(1)}k tok`;
39
- if (count < 1000000) return `${Math.round(count / 1000)}k tok`;
40
- return `${(count / 1000000).toFixed(1)}M tok`;
41
- }