footprintjs 1.0.0 → 2.0.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/AGENTS.md +81 -87
- package/README.md +2 -2
- package/ai-instructions/claude-code/SKILL.md +204 -91
- package/ai-instructions/clinerules +71 -31
- package/ai-instructions/copilot-instructions.md +67 -34
- package/ai-instructions/cursor/footprint.md +73 -34
- package/ai-instructions/kiro/footprint.md +70 -30
- package/ai-instructions/windsurfrules +71 -31
- package/dist/esm/index.js +8 -11
- package/dist/esm/lib/builder/FlowChartBuilder.js +26 -2
- package/dist/esm/lib/builder/typedFlowChart.js +4 -2
- package/dist/esm/lib/builder/types.js +1 -1
- package/dist/esm/lib/engine/types.js +1 -1
- package/dist/esm/lib/reactive/createTypedScope.js +28 -6
- package/dist/esm/lib/runner/FlowChartExecutor.js +13 -3
- package/dist/esm/lib/runner/RunContext.js +75 -0
- package/dist/esm/lib/runner/RunnableChart.js +104 -0
- package/dist/esm/lib/runner/index.js +2 -1
- package/dist/esm/lib/scope/ScopeFacade.js +12 -4
- package/dist/esm/recorders.js +69 -0
- package/dist/index.js +43 -45
- package/dist/lib/builder/FlowChartBuilder.js +26 -2
- package/dist/lib/builder/typedFlowChart.js +4 -2
- package/dist/lib/builder/types.js +1 -1
- package/dist/lib/engine/types.js +1 -1
- package/dist/lib/reactive/createTypedScope.js +28 -6
- package/dist/lib/runner/FlowChartExecutor.js +13 -3
- package/dist/lib/runner/RunContext.js +79 -0
- package/dist/lib/runner/RunnableChart.js +108 -0
- package/dist/lib/runner/index.js +4 -2
- package/dist/lib/scope/ScopeFacade.js +12 -4
- package/dist/recorders.js +79 -0
- package/dist/types/index.d.ts +8 -6
- package/dist/types/lib/builder/FlowChartBuilder.d.ts +18 -1
- package/dist/types/lib/builder/types.d.ts +5 -3
- package/dist/types/lib/engine/types.d.ts +2 -0
- package/dist/types/lib/runner/FlowChartExecutor.d.ts +9 -0
- package/dist/types/lib/runner/RunContext.d.ts +38 -0
- package/dist/types/lib/runner/RunnableChart.d.ts +42 -0
- package/dist/types/lib/runner/index.d.ts +3 -0
- package/dist/types/lib/scope/ScopeFacade.d.ts +11 -3
- package/dist/types/recorders.d.ts +43 -0
- package/package.json +6 -1
|
@@ -18,20 +18,26 @@ npm install footprintjs
|
|
|
18
18
|
## Quick Start
|
|
19
19
|
|
|
20
20
|
```typescript
|
|
21
|
-
import {
|
|
21
|
+
import { typedFlowChart, createTypedScopeFactory, FlowChartExecutor } from 'footprintjs';
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
interface OrderState {
|
|
24
|
+
orderId: string;
|
|
25
|
+
amount: number;
|
|
26
|
+
paymentStatus: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const chart = typedFlowChart<OrderState>('ReceiveOrder', (scope) => {
|
|
30
|
+
scope.orderId = 'ORD-123';
|
|
31
|
+
scope.amount = 49.99;
|
|
26
32
|
}, 'receive-order', undefined, 'Receive and validate the incoming order')
|
|
27
33
|
.addFunction('ProcessPayment', (scope) => {
|
|
28
|
-
const amount = scope.
|
|
29
|
-
scope.
|
|
34
|
+
const amount = scope.amount;
|
|
35
|
+
scope.paymentStatus = amount < 100 ? 'approved' : 'review';
|
|
30
36
|
}, 'process-payment', 'Charge customer and record payment status')
|
|
31
37
|
.setEnableNarrative()
|
|
32
38
|
.build();
|
|
33
39
|
|
|
34
|
-
const executor = new FlowChartExecutor(chart);
|
|
40
|
+
const executor = new FlowChartExecutor(chart, createTypedScopeFactory<OrderState>());
|
|
35
41
|
await executor.run({ input: { orderId: 'ORD-123' } });
|
|
36
42
|
|
|
37
43
|
console.log(executor.getNarrative());
|
|
@@ -47,14 +53,20 @@ console.log(executor.getNarrative());
|
|
|
47
53
|
|
|
48
54
|
## FlowChartBuilder API
|
|
49
55
|
|
|
50
|
-
Always chain from `
|
|
56
|
+
Always chain from `typedFlowChart<T>()` (recommended) or `flowChart()`.
|
|
51
57
|
|
|
52
58
|
### Linear Stages
|
|
53
59
|
|
|
54
60
|
```typescript
|
|
55
|
-
import {
|
|
61
|
+
import { typedFlowChart } from 'footprintjs';
|
|
62
|
+
|
|
63
|
+
interface MyState {
|
|
64
|
+
valueA: string;
|
|
65
|
+
valueB: number;
|
|
66
|
+
valueC: boolean;
|
|
67
|
+
}
|
|
56
68
|
|
|
57
|
-
const chart =
|
|
69
|
+
const chart = typedFlowChart<MyState>('StageA', fnA, 'stage-a', undefined, 'Description of A')
|
|
58
70
|
.addFunction('StageB', fnB, 'stage-b', 'Description of B')
|
|
59
71
|
.addFunction('StageC', fnC, 'stage-c', 'Description of C')
|
|
60
72
|
.build();
|
|
@@ -67,69 +79,102 @@ const chart = flowChart('StageA', fnA, 'stage-a', undefined, 'Description of A')
|
|
|
67
79
|
- `id` — stable identifier (used for branching, visualization, loop targets)
|
|
68
80
|
- `description` — optional, appears in narrative and auto-generated tool descriptions
|
|
69
81
|
|
|
70
|
-
### Stage Function Signature
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
type PipelineStageFunction = (
|
|
74
|
-
scope: ScopeFacade, // read/write transactional state
|
|
75
|
-
breakPipeline: () => void, // call to stop execution early
|
|
76
|
-
streamCallback?: StreamCallback, // for streaming stages
|
|
77
|
-
) => Promise<void> | void;
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### ScopeFacade — State Access
|
|
82
|
+
### Stage Function Signature (TypedScope)
|
|
81
83
|
|
|
82
|
-
|
|
84
|
+
With `typedFlowChart<T>()`, stage functions receive a `TypedScope<T>` proxy. All reads and writes use typed property access:
|
|
83
85
|
|
|
84
86
|
```typescript
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
scope.
|
|
97
|
-
|
|
98
|
-
//
|
|
99
|
-
|
|
87
|
+
interface LoanState {
|
|
88
|
+
creditTier: string;
|
|
89
|
+
amount: number;
|
|
90
|
+
customer: { name: string; address: { zip: string } };
|
|
91
|
+
tags: string[];
|
|
92
|
+
approved?: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const myStage = (scope: TypedScope<LoanState>) => {
|
|
96
|
+
// Typed writes (tracked — appear in narrative)
|
|
97
|
+
scope.creditTier = 'A';
|
|
98
|
+
scope.amount = 50000;
|
|
99
|
+
|
|
100
|
+
// Deep write (auto-delegates to updateValue)
|
|
101
|
+
scope.customer.address.zip = '90210';
|
|
102
|
+
|
|
103
|
+
// Array copy-on-write
|
|
104
|
+
scope.tags.push('vip');
|
|
105
|
+
|
|
106
|
+
// Optional fields
|
|
107
|
+
scope.approved = true;
|
|
108
|
+
|
|
109
|
+
// $-prefixed escape hatches for non-state operations
|
|
110
|
+
scope.$debug('checkpoint', { step: 1 });
|
|
111
|
+
scope.$metric('latency', 42);
|
|
112
|
+
const args = scope.$getArgs<{ requestId: string }>();
|
|
113
|
+
const env = scope.$getEnv();
|
|
114
|
+
scope.$break(); // stop pipeline execution early
|
|
100
115
|
};
|
|
101
116
|
```
|
|
102
117
|
|
|
103
|
-
**
|
|
118
|
+
**Three access tiers:**
|
|
119
|
+
- **Typed properties** (`scope.amount = 50000`) — mutable shared state, tracked in narrative
|
|
120
|
+
- **`$getArgs()`** — frozen business input from `run({ input })`, NOT tracked
|
|
121
|
+
- **`$getEnv()`** — frozen infrastructure context from `run({ env })`, NOT tracked. Returns `ExecutionEnv { signal?, timeoutMs?, traceId? }`. Auto-inherited by subflows. Closed type.
|
|
122
|
+
|
|
123
|
+
### Decider Branches with decide() (Single-Choice Conditional)
|
|
104
124
|
|
|
105
|
-
|
|
125
|
+
Use `decide()` for structured decision evidence capture. It auto-records which values led to the decision in the narrative.
|
|
106
126
|
|
|
107
127
|
```typescript
|
|
108
|
-
|
|
128
|
+
import { decide } from 'footprintjs';
|
|
129
|
+
|
|
130
|
+
interface RiskState {
|
|
131
|
+
creditScore: number;
|
|
132
|
+
dti: number;
|
|
133
|
+
riskTier: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const chart = typedFlowChart<RiskState>('Intake', intakeFn, 'intake')
|
|
109
137
|
.addDeciderFunction('AssessRisk', (scope) => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
138
|
+
// decide() captures filter evidence automatically
|
|
139
|
+
return decide(scope, [
|
|
140
|
+
{ when: { creditScore: { gt: 700 }, dti: { lt: 0.43 } }, then: 'low-risk', label: 'Good credit' },
|
|
141
|
+
{ when: (s) => s.creditScore > 600, then: 'medium-risk', label: 'Marginal credit' },
|
|
142
|
+
], 'high-risk');
|
|
143
|
+
// Narrative: "It evaluated Rule 0 'Good credit': creditScore 750 gt 700, and chose low-risk."
|
|
113
144
|
}, 'assess-risk', 'Evaluate risk and route accordingly')
|
|
114
145
|
.addFunctionBranch('high-risk', 'RejectApplication', rejectFn, 'Reject due to high risk')
|
|
146
|
+
.addFunctionBranch('medium-risk', 'ManualReview', reviewFn, 'Send to manual review')
|
|
115
147
|
.addFunctionBranch('low-risk', 'ApproveApplication', approveFn, 'Approve the application')
|
|
116
148
|
.setDefault('high-risk') // fallback if branch ID doesn't match
|
|
117
149
|
.end()
|
|
118
150
|
.build();
|
|
119
151
|
```
|
|
120
152
|
|
|
121
|
-
The
|
|
153
|
+
The `decide()` function accepts two `when` formats:
|
|
154
|
+
- **Filter format:** `{ creditScore: { gt: 700 } }` — declarative, auto-captures evidence
|
|
155
|
+
- **Function format:** `(s) => s.creditScore > 600` — arbitrary logic with optional `label`
|
|
156
|
+
|
|
157
|
+
The decider function **returns a branch ID string**. The engine matches it to a child and executes that branch. The decision and its evidence are recorded in the narrative.
|
|
158
|
+
|
|
159
|
+
### Selector Branches with select() (Multi-Choice Fan-Out)
|
|
122
160
|
|
|
123
|
-
|
|
161
|
+
Use `select()` for structured multi-choice evidence capture:
|
|
124
162
|
|
|
125
163
|
```typescript
|
|
126
|
-
|
|
164
|
+
import { select } from 'footprintjs';
|
|
165
|
+
|
|
166
|
+
interface CheckState {
|
|
167
|
+
needsCredit: boolean;
|
|
168
|
+
needsIdentity: boolean;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const chart = typedFlowChart<CheckState>('Intake', intakeFn, 'intake')
|
|
127
172
|
.addSelectorFunction('SelectChecks', (scope) => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
173
|
+
return select(scope, [
|
|
174
|
+
{ when: { needsCredit: { eq: true } }, then: 'credit-check', label: 'Credit required' },
|
|
175
|
+
{ when: { needsIdentity: { eq: true } }, then: 'identity-check', label: 'Identity required' },
|
|
176
|
+
]);
|
|
177
|
+
// Returns array of matching branch IDs
|
|
133
178
|
}, 'select-checks')
|
|
134
179
|
.addFunctionBranch('credit-check', 'CreditCheck', creditFn)
|
|
135
180
|
.addFunctionBranch('identity-check', 'IdentityCheck', identityFn)
|
|
@@ -151,13 +196,13 @@ builder.addListOfFunction([
|
|
|
151
196
|
|
|
152
197
|
```typescript
|
|
153
198
|
// Build a reusable sub-pipeline
|
|
154
|
-
const creditSubflow =
|
|
199
|
+
const creditSubflow = typedFlowChart<CreditState>('PullReport', pullReportFn, 'pull-report')
|
|
155
200
|
.addFunction('ScoreReport', scoreReportFn, 'score-report')
|
|
156
201
|
.build();
|
|
157
202
|
|
|
158
203
|
// Mount as linear continuation
|
|
159
204
|
builder.addSubFlowChartNext('credit-sub', creditSubflow, 'CreditCheck', {
|
|
160
|
-
inputMapper: (parentScope) => ({ ssn: parentScope.
|
|
205
|
+
inputMapper: (parentScope) => ({ ssn: parentScope.ssn }),
|
|
161
206
|
outputMapper: (subOut, parentScope) => ({ creditScore: subOut.score }),
|
|
162
207
|
});
|
|
163
208
|
|
|
@@ -171,11 +216,15 @@ builder.addDeciderFunction('Route', routerFn, 'route')
|
|
|
171
216
|
### Loops
|
|
172
217
|
|
|
173
218
|
```typescript
|
|
219
|
+
interface RetryState {
|
|
220
|
+
attempts: number;
|
|
221
|
+
paymentResult?: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
174
224
|
builder
|
|
175
225
|
.addFunction('RetryPayment', async (scope) => {
|
|
176
|
-
|
|
177
|
-
scope.
|
|
178
|
-
if (attempts >= 3) return; // exit loop by not looping
|
|
226
|
+
scope.attempts = (scope.attempts ?? 0) + 1;
|
|
227
|
+
if (scope.attempts >= 3) return; // exit loop by not looping
|
|
179
228
|
}, 'retry-payment')
|
|
180
229
|
.loopTo('retry-payment'); // back-edge to this stage's ID
|
|
181
230
|
```
|
|
@@ -206,30 +255,36 @@ const mermaid = builder.toMermaid(); // Mermaid diagram string
|
|
|
206
255
|
## FlowChartExecutor API
|
|
207
256
|
|
|
208
257
|
```typescript
|
|
209
|
-
import { FlowChartExecutor } from 'footprintjs';
|
|
258
|
+
import { FlowChartExecutor, createTypedScopeFactory } from 'footprintjs';
|
|
210
259
|
|
|
211
|
-
|
|
260
|
+
interface AppState {
|
|
261
|
+
applicantName: string;
|
|
262
|
+
income: number;
|
|
263
|
+
riskTier?: string;
|
|
264
|
+
decision?: string;
|
|
265
|
+
}
|
|
212
266
|
|
|
213
|
-
|
|
267
|
+
const executor = new FlowChartExecutor(chart, createTypedScopeFactory<AppState>());
|
|
268
|
+
|
|
269
|
+
// Run with input and optional execution environment
|
|
214
270
|
const result = await executor.run({
|
|
215
271
|
input: { applicantName: 'Bob', income: 42000 },
|
|
216
|
-
timeoutMs: 5000,
|
|
217
|
-
signal: controller.signal, // optional AbortSignal
|
|
272
|
+
env: { traceId: 'req-123', timeoutMs: 5000 },
|
|
218
273
|
});
|
|
219
274
|
|
|
220
275
|
// Get narrative (combined flow + data operations)
|
|
221
276
|
const narrative: string[] = executor.getNarrative();
|
|
222
277
|
// ["Stage 1: The process began with ReceiveApplication.",
|
|
223
|
-
// " Step 1: Write
|
|
278
|
+
// " Step 1: Write applicantName = \"Bob\"",
|
|
224
279
|
// "Stage 2: Next, it moved on to AssessRisk.",
|
|
225
|
-
// " Step 1: Read
|
|
280
|
+
// " Step 1: Read income = 42000",
|
|
226
281
|
// " Step 2: Write riskTier = \"high\"",
|
|
227
282
|
// "[Condition]: A decision was made, path taken was RejectApplication."]
|
|
228
283
|
|
|
229
284
|
// Structured entries (for programmatic access)
|
|
230
285
|
const entries: CombinedNarrativeEntry[] = executor.getNarrativeEntries();
|
|
231
286
|
// [{ type: 'stage', text: '...', depth: 0, stageName: 'ReceiveApplication' },
|
|
232
|
-
// { type: 'step', text: 'Write
|
|
287
|
+
// { type: 'step', text: 'Write applicantName = ...', depth: 1, stageName: 'ReceiveApplication', stepNumber: 1 },
|
|
233
288
|
// { type: 'condition', text: '...', depth: 0 }]
|
|
234
289
|
|
|
235
290
|
// Full memory snapshot
|
|
@@ -248,18 +303,20 @@ const flowOnly: string[] = executor.getFlowNarrative();
|
|
|
248
303
|
|
|
249
304
|
### Scope Recorders (data operations)
|
|
250
305
|
|
|
251
|
-
|
|
306
|
+
Fire during typed property access (reads/writes). Attach via `executor.attachRecorder()`:
|
|
252
307
|
|
|
253
308
|
```typescript
|
|
254
|
-
import { MetricRecorder, DebugRecorder
|
|
309
|
+
import { MetricRecorder, DebugRecorder } from 'footprintjs';
|
|
255
310
|
|
|
256
311
|
// Built-in recorders
|
|
257
312
|
const metrics = new MetricRecorder();
|
|
258
313
|
const debug = new DebugRecorder('verbose'); // or 'minimal'
|
|
259
314
|
|
|
260
|
-
// Attach to
|
|
261
|
-
|
|
262
|
-
|
|
315
|
+
// Attach to executor (one-liner, no custom scopeFactory needed)
|
|
316
|
+
executor.attachRecorder(metrics);
|
|
317
|
+
executor.attachRecorder(debug);
|
|
318
|
+
|
|
319
|
+
await executor.run({ input: data });
|
|
263
320
|
|
|
264
321
|
// After execution
|
|
265
322
|
metrics.getSummary(); // { totalReads: 12, totalWrites: 8, stages: {...} }
|
|
@@ -299,6 +356,10 @@ const myRecorder: FlowRecorder = {
|
|
|
299
356
|
},
|
|
300
357
|
onDecision(event: FlowDecisionEvent) {
|
|
301
358
|
console.log(`Decision at ${event.stageName}: chose ${event.chosen}`);
|
|
359
|
+
// event.evidence available when using decide()
|
|
360
|
+
if (event.evidence) {
|
|
361
|
+
console.log(`Evidence: ${JSON.stringify(event.evidence)}`);
|
|
362
|
+
}
|
|
302
363
|
},
|
|
303
364
|
clear() {
|
|
304
365
|
// Reset state before each run
|
|
@@ -370,15 +431,16 @@ When a stage executes, events fire in this exact order:
|
|
|
370
431
|
|
|
371
432
|
```
|
|
372
433
|
1. Recorder.onStageStart — stage begins
|
|
373
|
-
2. Recorder.onRead — each
|
|
374
|
-
3. Recorder.onWrite — each
|
|
434
|
+
2. Recorder.onRead — each typed property read (DURING execution)
|
|
435
|
+
3. Recorder.onWrite — each typed property write (DURING execution)
|
|
375
436
|
4. Recorder.onCommit — transaction buffer flushes to shared memory
|
|
376
437
|
5. Recorder.onStageEnd — stage completes
|
|
377
438
|
6. FlowRecorder.onStageExecuted — control flow records the stage
|
|
378
439
|
(CombinedNarrativeRecorder flushes buffered ops here)
|
|
379
440
|
7. FlowRecorder.onNext — moving to next stage
|
|
380
|
-
OR FlowRecorder.onDecision — if this was a decider
|
|
441
|
+
OR FlowRecorder.onDecision — if this was a decider (carries evidence from decide())
|
|
381
442
|
OR FlowRecorder.onFork — if children execute in parallel
|
|
443
|
+
OR FlowRecorder.onSelected — if this was a selector (carries evidence from select())
|
|
382
444
|
```
|
|
383
445
|
|
|
384
446
|
**This ordering is what makes inline collection work.** Scope events buffer during execution, flow events trigger the flush.
|
|
@@ -388,10 +450,13 @@ When a stage executes, events fire in this exact order:
|
|
|
388
450
|
## Anti-Patterns to Avoid
|
|
389
451
|
|
|
390
452
|
1. **Never post-process the tree.** Don't walk the spec after execution to collect data. Use recorders.
|
|
391
|
-
2. **
|
|
392
|
-
3. **Don't
|
|
393
|
-
4. **
|
|
394
|
-
5. **Don't
|
|
453
|
+
2. **Don't use `getValue()`/`setValue()` in TypedScope stages.** Use typed property access (`scope.amount = 50000`). The old ScopeFacade API is internal only.
|
|
454
|
+
3. **Don't use `$`-prefixed state keys** (e.g., `$break` as a property name) — they collide with TypedScope's `$`-prefixed escape hatches (`$getArgs`, `$getEnv`, `$break`, `$debug`, `$metric`).
|
|
455
|
+
4. **Never use `CombinedNarrativeBuilder`** — it's deprecated. Use `CombinedNarrativeRecorder` (auto-attached by `setEnableNarrative()`).
|
|
456
|
+
5. **Don't extract a shared base class** for Recorder and FlowRecorder. They look similar but serve different layers. Two instances = coincidence.
|
|
457
|
+
6. **Don't call `$getArgs()` for tracked data.** `$getArgs()` returns frozen readonly input. Use typed scope properties for state that should appear in the narrative.
|
|
458
|
+
7. **Don't put infrastructure data in `$getArgs()`.** Use `$getEnv()` via `run({ env })` for signals, timeouts, and trace IDs.
|
|
459
|
+
8. **Don't create scope recorders manually** unless building a custom recorder. `setEnableNarrative()` handles everything.
|
|
395
460
|
|
|
396
461
|
---
|
|
397
462
|
|
|
@@ -401,13 +466,17 @@ When a stage executes, events fire in this exact order:
|
|
|
401
466
|
src/lib/
|
|
402
467
|
├── memory/ → SharedMemory, StageContext, TransactionBuffer, EventLog (foundation)
|
|
403
468
|
├── schema/ → detectSchema, validate, InputValidationError (foundation)
|
|
404
|
-
├── builder/ → FlowChartBuilder, flowChart(), DeciderList, SelectorFnList (standalone)
|
|
469
|
+
├── builder/ → FlowChartBuilder, flowChart(), typedFlowChart(), DeciderList, SelectorFnList (standalone)
|
|
405
470
|
├── scope/ → ScopeFacade, recorders/, providers/, protection/ (depends: memory)
|
|
406
|
-
├──
|
|
471
|
+
├── reactive/ → TypedScope<T> deep Proxy, typed property access, $-methods, cycle-safe (depends: scope)
|
|
472
|
+
├── decide/ → decide()/select() decision evidence capture, filter + function when formats (depends: scope)
|
|
473
|
+
├── engine/ → FlowchartTraverser, handlers/, narrative/ (depends: memory, scope, reactive, builder)
|
|
407
474
|
├── runner/ → FlowChartExecutor, ExecutionRuntime (depends: engine, scope, schema)
|
|
408
475
|
└── contract/ → defineContract, generateOpenAPI (depends: schema)
|
|
409
476
|
```
|
|
410
477
|
|
|
478
|
+
Dependency DAG: `memory <- scope <- reactive <- engine <- runner`, `schema <- engine`, `builder (standalone) -> engine`, `contract <- schema`, `decide -> scope`
|
|
479
|
+
|
|
411
480
|
Two entry points:
|
|
412
481
|
- `import { ... } from 'footprintjs'` — public API
|
|
413
482
|
- `import { ... } from 'footprintjs/advanced'` — internals (memory, traverser, handlers)
|
|
@@ -416,21 +485,50 @@ Two entry points:
|
|
|
416
485
|
|
|
417
486
|
## Common Patterns
|
|
418
487
|
|
|
419
|
-
### Pipeline with
|
|
488
|
+
### Pipeline with decide() + narrative
|
|
420
489
|
|
|
421
490
|
```typescript
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
491
|
+
import { typedFlowChart, createTypedScopeFactory, FlowChartExecutor, decide } from 'footprintjs';
|
|
492
|
+
|
|
493
|
+
interface LoanState {
|
|
494
|
+
applicantName: string;
|
|
495
|
+
income: number;
|
|
496
|
+
creditScore: number;
|
|
497
|
+
dti: number;
|
|
498
|
+
decision?: string;
|
|
499
|
+
reason?: string;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const chart = typedFlowChart<LoanState>('Receive', (scope) => {
|
|
503
|
+
const args = scope.$getArgs<{ applicantName: string; income: number }>();
|
|
504
|
+
scope.applicantName = args.applicantName;
|
|
505
|
+
scope.income = args.income;
|
|
506
|
+
}, 'receive')
|
|
507
|
+
.addFunction('Analyze', (scope) => {
|
|
508
|
+
scope.creditScore = 750; // from credit bureau
|
|
509
|
+
scope.dti = 0.35; // computed
|
|
510
|
+
}, 'analyze')
|
|
511
|
+
.addDeciderFunction('Decide', (scope) => {
|
|
512
|
+
return decide(scope, [
|
|
513
|
+
{ when: { creditScore: { gt: 700 }, dti: { lt: 0.43 } }, then: 'approve', label: 'Good credit' },
|
|
514
|
+
{ when: (s) => s.creditScore > 600, then: 'approve', label: 'Marginal but acceptable' },
|
|
515
|
+
], 'reject');
|
|
516
|
+
}, 'decide')
|
|
517
|
+
.addFunctionBranch('approve', 'Approve', (scope) => {
|
|
518
|
+
scope.decision = 'approved';
|
|
519
|
+
scope.reason = 'Meets credit criteria';
|
|
520
|
+
})
|
|
521
|
+
.addFunctionBranch('reject', 'Reject', (scope) => {
|
|
522
|
+
scope.decision = 'rejected';
|
|
523
|
+
scope.reason = 'Does not meet credit criteria';
|
|
524
|
+
})
|
|
427
525
|
.setDefault('reject')
|
|
428
526
|
.end()
|
|
429
527
|
.setEnableNarrative()
|
|
430
528
|
.build();
|
|
431
529
|
|
|
432
|
-
const executor = new FlowChartExecutor(chart);
|
|
433
|
-
await executor.run({ input:
|
|
530
|
+
const executor = new FlowChartExecutor(chart, createTypedScopeFactory<LoanState>());
|
|
531
|
+
await executor.run({ input: { applicantName: 'Bob', income: 42000 } });
|
|
434
532
|
const trace = executor.getNarrative();
|
|
435
533
|
// Feed trace to LLM for grounded explanations
|
|
436
534
|
```
|
|
@@ -438,14 +536,25 @@ const trace = executor.getNarrative();
|
|
|
438
536
|
### Subflow with input/output mapping
|
|
439
537
|
|
|
440
538
|
```typescript
|
|
441
|
-
|
|
539
|
+
interface SubState {
|
|
540
|
+
ssn: string;
|
|
541
|
+
score: number;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
interface MainState {
|
|
545
|
+
ssn: string;
|
|
546
|
+
parentKey: string;
|
|
547
|
+
creditScore?: number;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const subflow = typedFlowChart<SubState>('SubStart', subStartFn, 'sub-start')
|
|
442
551
|
.addFunction('SubProcess', subProcessFn, 'sub-process')
|
|
443
552
|
.build();
|
|
444
553
|
|
|
445
|
-
const main =
|
|
554
|
+
const main = typedFlowChart<MainState>('Main', mainFn, 'main')
|
|
446
555
|
.addSubFlowChartNext('my-subflow', subflow, 'SubflowMount', {
|
|
447
|
-
inputMapper: (scope) => ({
|
|
448
|
-
outputMapper: (subOut) => ({
|
|
556
|
+
inputMapper: (scope) => ({ ssn: scope.ssn }),
|
|
557
|
+
outputMapper: (subOut) => ({ creditScore: subOut.score }),
|
|
449
558
|
})
|
|
450
559
|
.build();
|
|
451
560
|
```
|
|
@@ -453,8 +562,12 @@ const main = flowChart('Main', mainFn, 'main')
|
|
|
453
562
|
### Attach multiple recorders
|
|
454
563
|
|
|
455
564
|
```typescript
|
|
456
|
-
import { ManifestFlowRecorder, MilestoneNarrativeFlowRecorder } from 'footprintjs';
|
|
565
|
+
import { ManifestFlowRecorder, MilestoneNarrativeFlowRecorder, MetricRecorder } from 'footprintjs';
|
|
566
|
+
|
|
567
|
+
// Scope recorder (data ops) — via executor.attachRecorder()
|
|
568
|
+
executor.attachRecorder(new MetricRecorder());
|
|
457
569
|
|
|
570
|
+
// Flow recorders (control flow) — via executor.attachFlowRecorder()
|
|
458
571
|
executor.attachFlowRecorder(new ManifestFlowRecorder());
|
|
459
572
|
executor.attachFlowRecorder(new MilestoneNarrativeFlowRecorder());
|
|
460
573
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# footprint.js
|
|
1
|
+
# footprint.js
|
|
2
2
|
|
|
3
3
|
This is the footprint.js library — the flowchart pattern for backend code. Self-explainable systems that AI can reason about.
|
|
4
4
|
|
|
@@ -12,8 +12,10 @@ This is the footprint.js library — the flowchart pattern for backend code. Sel
|
|
|
12
12
|
src/lib/
|
|
13
13
|
├── memory/ → Transactional state (SharedMemory, StageContext, TransactionBuffer)
|
|
14
14
|
├── schema/ → Validation (Zod optional, duck-typed)
|
|
15
|
-
├── builder/ → Fluent DSL (FlowChartBuilder, flowChart())
|
|
15
|
+
├── builder/ → Fluent DSL (FlowChartBuilder, flowChart(), typedFlowChart())
|
|
16
16
|
├── scope/ → Per-stage facades + recorders + providers
|
|
17
|
+
├── reactive/ → TypedScope<T> deep Proxy (typed property access, $-methods)
|
|
18
|
+
├── decide/ → decide()/select() decision evidence capture
|
|
17
19
|
├── engine/ → DFS traversal + narrative + handlers
|
|
18
20
|
├── runner/ → FlowChartExecutor
|
|
19
21
|
└── contract/ → I/O schema + OpenAPI
|
|
@@ -21,46 +23,82 @@ src/lib/
|
|
|
21
23
|
|
|
22
24
|
Entry points: `footprintjs` (public) and `footprintjs/advanced` (internals).
|
|
23
25
|
|
|
24
|
-
##
|
|
26
|
+
## Key API — TypedScope (Recommended)
|
|
25
27
|
|
|
26
28
|
```typescript
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
import { typedFlowChart, createTypedScopeFactory, FlowChartExecutor, decide } from 'footprintjs';
|
|
30
|
+
|
|
31
|
+
interface State {
|
|
32
|
+
creditScore: number;
|
|
33
|
+
riskTier: string;
|
|
34
|
+
decision?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const chart = typedFlowChart<State>('Intake', async (scope) => {
|
|
38
|
+
scope.creditScore = 750; // typed write (no setValue needed)
|
|
39
|
+
scope.riskTier = 'low'; // typed write
|
|
40
|
+
}, 'intake')
|
|
35
41
|
.setEnableNarrative()
|
|
36
|
-
.
|
|
42
|
+
.addDeciderFunction('Route', (scope) => {
|
|
43
|
+
return decide(scope, [
|
|
44
|
+
{ when: { riskTier: { eq: 'low' } }, then: 'approved', label: 'Low risk' },
|
|
45
|
+
], 'rejected');
|
|
46
|
+
}, 'route', 'Route based on risk')
|
|
47
|
+
.addFunctionBranch('approved', 'Approve', async (scope) => {
|
|
48
|
+
scope.decision = 'Approved';
|
|
49
|
+
})
|
|
50
|
+
.addFunctionBranch('rejected', 'Reject', async (scope) => {
|
|
51
|
+
scope.decision = 'Rejected';
|
|
52
|
+
})
|
|
53
|
+
.setDefault('rejected')
|
|
54
|
+
.end()
|
|
55
|
+
.build();
|
|
56
|
+
|
|
57
|
+
const executor = new FlowChartExecutor(chart, createTypedScopeFactory<State>());
|
|
58
|
+
await executor.run();
|
|
59
|
+
executor.getNarrative(); // causal trace with decision evidence
|
|
37
60
|
```
|
|
38
61
|
|
|
39
|
-
|
|
62
|
+
### TypedScope $-methods (escape hatches)
|
|
40
63
|
|
|
41
64
|
```typescript
|
|
42
|
-
|
|
65
|
+
scope.$getArgs<T>() // frozen readonly input
|
|
66
|
+
scope.$getEnv() // execution environment (signal, timeoutMs, traceId)
|
|
67
|
+
scope.$break() // stop pipeline
|
|
68
|
+
scope.$debug(key, value) // debug info
|
|
69
|
+
scope.$metric(name, value) // metrics
|
|
43
70
|
```
|
|
44
71
|
|
|
45
|
-
|
|
72
|
+
### decide() / select()
|
|
46
73
|
|
|
47
74
|
```typescript
|
|
48
|
-
|
|
49
|
-
scope
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
75
|
+
// Filter syntax — captures operators + thresholds
|
|
76
|
+
decide(scope, [
|
|
77
|
+
{ when: { creditScore: { gt: 700 }, dti: { lt: 0.43 } }, then: 'approved', label: 'Good credit' },
|
|
78
|
+
], 'rejected');
|
|
79
|
+
|
|
80
|
+
// Function syntax — captures which keys were read
|
|
81
|
+
decide(scope, [
|
|
82
|
+
{ when: (s) => s.creditScore > 700, then: 'approved' },
|
|
83
|
+
], 'rejected');
|
|
84
|
+
|
|
85
|
+
// select() — all matching branches (not first-match)
|
|
86
|
+
select(scope, [
|
|
87
|
+
{ when: { glucose: { gt: 100 } }, then: 'diabetes' },
|
|
88
|
+
{ when: { bmi: { gt: 30 } }, then: 'obesity' },
|
|
89
|
+
]);
|
|
53
90
|
```
|
|
54
91
|
|
|
55
|
-
|
|
92
|
+
### Executor
|
|
56
93
|
|
|
57
94
|
```typescript
|
|
58
|
-
const executor = new FlowChartExecutor(chart);
|
|
59
|
-
await executor.run({ input,
|
|
60
|
-
executor.getNarrative() //
|
|
61
|
-
executor.getNarrativeEntries() //
|
|
95
|
+
const executor = new FlowChartExecutor(chart, createTypedScopeFactory<State>());
|
|
96
|
+
await executor.run({ input, env: { traceId: 'req-123' } });
|
|
97
|
+
executor.getNarrative() // string[]
|
|
98
|
+
executor.getNarrativeEntries() // CombinedNarrativeEntry[]
|
|
62
99
|
executor.getSnapshot() // memory state
|
|
63
|
-
executor.
|
|
100
|
+
executor.attachRecorder(recorder) // scope observer
|
|
101
|
+
executor.attachFlowRecorder(r) // flow observer
|
|
64
102
|
executor.setRedactionPolicy({ keys, patterns, fields })
|
|
65
103
|
```
|
|
66
104
|
|
|
@@ -68,12 +106,14 @@ executor.setRedactionPolicy({ keys, patterns, fields })
|
|
|
68
106
|
|
|
69
107
|
- **Scope Recorder**: fires DURING stage (`onRead`, `onWrite`, `onCommit`)
|
|
70
108
|
- **FlowRecorder**: fires AFTER stage (`onStageExecuted`, `onDecision`, `onFork`, `onLoop`)
|
|
71
|
-
- 8
|
|
72
|
-
- `
|
|
109
|
+
- 8 built-in FlowRecorder strategies
|
|
110
|
+
- `setEnableNarrative()` auto-attaches `CombinedNarrativeRecorder`
|
|
73
111
|
|
|
74
112
|
## Rules
|
|
75
113
|
|
|
114
|
+
- Use `typedFlowChart<T>()` + `createTypedScopeFactory<T>()` (not flowChart + ScopeFacade)
|
|
115
|
+
- Use `decide()` / `select()` in decider/selector functions
|
|
116
|
+
- Use typed property access (not getValue/setValue)
|
|
117
|
+
- Use `$getArgs()` for input, `$getEnv()` for environment
|
|
76
118
|
- Never post-process the tree — use recorders
|
|
77
|
-
- `
|
|
78
|
-
- Don't use deprecated `CombinedNarrativeBuilder`
|
|
79
|
-
- `setEnableNarrative()` is all you need
|
|
119
|
+
- `setEnableNarrative()` is all you need for narrative setup
|