@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.
- package/CHANGELOG.md +7 -4
- package/README.md +12 -2
- package/agents/architecture-designer.agent.yaml +58 -0
- package/agents/architecture-inferrer.agent.yaml +60 -0
- package/agents/gap-identifier.agent.yaml +66 -0
- package/agents/handoff-writer.agent.yaml +64 -0
- package/agents/plan-creator.agent.yaml +63 -0
- package/agents/project-definer.agent.yaml +53 -0
- package/agents/project-inferrer.agent.yaml +63 -0
- package/agents/requirements-gatherer.agent.yaml +58 -0
- package/dist/agent-spec.d.ts +27 -0
- package/dist/agent-spec.d.ts.map +1 -0
- package/dist/agent-spec.js +112 -0
- package/dist/agent-spec.js.map +1 -0
- package/dist/checkpoint.d.ts +50 -0
- package/dist/checkpoint.d.ts.map +1 -0
- package/dist/checkpoint.js +109 -0
- package/dist/checkpoint.js.map +1 -0
- package/dist/completion.d.ts +18 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +58 -0
- package/dist/completion.js.map +1 -0
- package/dist/dag.d.ts +56 -0
- package/dist/dag.d.ts.map +1 -0
- package/dist/dag.js +172 -0
- package/dist/dag.js.map +1 -0
- package/dist/dispatch.d.ts +35 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +311 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/expression.d.ts +64 -0
- package/dist/expression.d.ts.map +1 -0
- package/dist/expression.js +294 -0
- package/dist/expression.js.map +1 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +42 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +675 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +12 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +59 -0
- package/dist/output.js.map +1 -0
- package/dist/state.d.ts +66 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +227 -0
- package/dist/state.js.map +1 -0
- package/dist/step-agent.d.ts +35 -0
- package/dist/step-agent.d.ts.map +1 -0
- package/dist/step-agent.js +156 -0
- package/dist/step-agent.js.map +1 -0
- package/dist/step-command.d.ts +21 -0
- package/dist/step-command.d.ts.map +1 -0
- package/dist/step-command.js +150 -0
- package/dist/step-command.js.map +1 -0
- package/dist/step-foreach.d.ts +33 -0
- package/dist/step-foreach.d.ts.map +1 -0
- package/dist/step-foreach.js +112 -0
- package/dist/step-foreach.js.map +1 -0
- package/dist/step-gate.d.ts +18 -0
- package/dist/step-gate.d.ts.map +1 -0
- package/dist/step-gate.js +137 -0
- package/dist/step-gate.js.map +1 -0
- package/dist/step-loop.d.ts +32 -0
- package/dist/step-loop.d.ts.map +1 -0
- package/dist/step-loop.js +278 -0
- package/dist/step-loop.js.map +1 -0
- package/dist/step-monitor.d.ts +56 -0
- package/dist/step-monitor.d.ts.map +1 -0
- package/dist/step-monitor.js +317 -0
- package/dist/step-monitor.js.map +1 -0
- package/dist/step-parallel.d.ts +43 -0
- package/dist/step-parallel.d.ts.map +1 -0
- package/dist/step-parallel.js +122 -0
- package/dist/step-parallel.js.map +1 -0
- package/{src/step-pause.ts → dist/step-pause.d.ts} +3 -13
- package/dist/step-pause.d.ts.map +1 -0
- package/dist/step-pause.js +22 -0
- package/dist/step-pause.js.map +1 -0
- package/dist/step-shared.d.ts +57 -0
- package/dist/step-shared.d.ts.map +1 -0
- package/dist/step-shared.js +125 -0
- package/dist/step-shared.js.map +1 -0
- package/dist/step-transform.d.ts +11 -0
- package/dist/step-transform.d.ts.map +1 -0
- package/dist/step-transform.js +41 -0
- package/dist/step-transform.js.map +1 -0
- package/dist/template.d.ts +51 -0
- package/dist/template.d.ts.map +1 -0
- package/{src/template.ts → dist/template.js} +32 -38
- package/dist/template.js.map +1 -0
- package/dist/test-helpers.d.ts +17 -0
- package/dist/test-helpers.d.ts.map +1 -0
- package/dist/test-helpers.js +46 -0
- package/dist/test-helpers.js.map +1 -0
- package/dist/tui.d.ts +47 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +143 -0
- package/dist/tui.js.map +1 -0
- package/dist/types.d.ts +164 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/workflow-discovery.d.ts +23 -0
- package/dist/workflow-discovery.d.ts.map +1 -0
- package/dist/workflow-discovery.js +103 -0
- package/dist/workflow-discovery.js.map +1 -0
- package/dist/workflow-executor.d.ts +50 -0
- package/dist/workflow-executor.d.ts.map +1 -0
- package/dist/workflow-executor.js +726 -0
- package/dist/workflow-executor.js.map +1 -0
- package/dist/workflow-sdk.d.ts +44 -0
- package/dist/workflow-sdk.d.ts.map +1 -0
- package/dist/workflow-sdk.js +438 -0
- package/dist/workflow-sdk.js.map +1 -0
- package/dist/workflow-spec.d.ts +33 -0
- package/dist/workflow-spec.d.ts.map +1 -0
- package/dist/workflow-spec.js +464 -0
- package/dist/workflow-spec.js.map +1 -0
- package/dist/workflows-dir.d.ts +3 -0
- package/dist/workflows-dir.d.ts.map +1 -0
- package/{src/workflows-dir.ts → dist/workflows-dir.js} +1 -0
- package/dist/workflows-dir.js.map +1 -0
- package/package.json +53 -38
- package/schemas/decomposition-specs.schema.json +48 -48
- package/schemas/execution-results.schema.json +48 -48
- package/schemas/investigation-findings.schema.json +43 -43
- package/schemas/pattern-analysis.schema.json +41 -41
- package/schemas/phase.schema.json +48 -0
- package/schemas/plan-breakdown.schema.json +20 -20
- package/schemas/quality-analysis.schema.json +33 -33
- package/schemas/research-findings.schema.json +42 -42
- package/schemas/structure-analysis.schema.json +40 -40
- package/schemas/synthesis.schema.json +31 -31
- package/schemas/verifier-output.schema.json +94 -94
- package/skill-narrative.md +84 -0
- package/skills/pi-workflows/SKILL.md +247 -0
- package/templates/architecture-designer/task.md +117 -0
- package/templates/architecture-inferrer/task.md +61 -0
- package/templates/gap-identifier/task.md +103 -0
- package/templates/handoff-writer/task.md +91 -0
- package/templates/plan-creator/task.md +143 -0
- package/templates/project-definer/task.md +67 -0
- package/templates/project-inferrer/task.md +56 -0
- package/templates/requirements-gatherer/task.md +90 -0
- package/workflows/analyze-existing-project.workflow.yaml +74 -0
- package/workflows/create-handoff.workflow.yaml +58 -0
- package/workflows/create-phase.workflow.yaml +1 -1
- package/workflows/do-gap.workflow.yaml +4 -4
- package/workflows/fix-audit.workflow.yaml +1 -1
- package/workflows/gap-to-phase.workflow.yaml +1 -1
- package/workflows/init-new-project.workflow.yaml +55 -0
- package/workflows/plan-from-requirements.workflow.yaml +51 -0
- package/workflows/self-implement.workflow.yaml +3 -3
- package/workflows/typed-analysis.workflow.yaml +4 -4
- package/src/agent-spec.test.ts +0 -289
- package/src/agent-spec.ts +0 -122
- package/src/block-validation.test.ts +0 -350
- package/src/checkpoint.test.ts +0 -237
- package/src/checkpoint.ts +0 -140
- package/src/completion.test.ts +0 -143
- package/src/completion.ts +0 -68
- package/src/dag.test.ts +0 -350
- package/src/dag.ts +0 -219
- package/src/dispatch.test.ts +0 -475
- package/src/dispatch.ts +0 -353
- package/src/expression.test.ts +0 -353
- package/src/expression.ts +0 -332
- package/src/format.test.ts +0 -80
- package/src/format.ts +0 -41
- package/src/graduated-failure.test.ts +0 -556
- package/src/index.test.ts +0 -114
- package/src/index.ts +0 -516
- package/src/loop.test.ts +0 -551
- package/src/output.test.ts +0 -213
- package/src/output.ts +0 -70
- package/src/parallel-integration.test.ts +0 -175
- package/src/resume.test.ts +0 -192
- package/src/state.test.ts +0 -192
- package/src/state.ts +0 -254
- package/src/step-agent.test.ts +0 -327
- package/src/step-agent.ts +0 -178
- package/src/step-command.test.ts +0 -330
- package/src/step-command.ts +0 -132
- package/src/step-foreach.test.ts +0 -647
- package/src/step-foreach.ts +0 -148
- package/src/step-gate.test.ts +0 -185
- package/src/step-gate.ts +0 -116
- package/src/step-loop.test.ts +0 -626
- package/src/step-loop.ts +0 -323
- package/src/step-parallel.test.ts +0 -475
- package/src/step-parallel.ts +0 -168
- package/src/step-pause.test.ts +0 -30
- package/src/step-shared.test.ts +0 -355
- package/src/step-shared.ts +0 -157
- package/src/step-transform.test.ts +0 -155
- package/src/step-transform.ts +0 -47
- package/src/template-integration.test.ts +0 -72
- package/src/template.test.ts +0 -162
- package/src/test-helpers.ts +0 -51
- package/src/tui.test.ts +0 -355
- package/src/tui.ts +0 -182
- package/src/types.ts +0 -195
- package/src/verifier-schema.test.ts +0 -226
- package/src/workflow-discovery.test.ts +0 -105
- package/src/workflow-discovery.ts +0 -114
- package/src/workflow-executor.test.ts +0 -1532
- package/src/workflow-executor.ts +0 -810
- package/src/workflow-sdk.test.ts +0 -238
- package/src/workflow-sdk.ts +0 -262
- package/src/workflow-spec.test.ts +0 -576
- 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
|
-
|
package/src/format.test.ts
DELETED
|
@@ -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
|
-
}
|