footprintjs 3.0.18 → 3.0.21
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/README.md +34 -15
- package/dist/esm/lib/memory/SharedMemory.js +3 -5
- package/dist/esm/lib/memory/TransactionBuffer.js +2 -3
- package/dist/esm/lib/memory/index.js +2 -2
- package/dist/esm/lib/memory/pathOps.js +87 -0
- package/dist/esm/lib/memory/utils.js +3 -5
- package/dist/esm/lib/reactive/createTypedScope.js +2 -2
- package/dist/esm/lib/scope/ScopeFacade.js +4 -5
- package/dist/lib/memory/SharedMemory.js +3 -8
- package/dist/lib/memory/TransactionBuffer.js +8 -12
- package/dist/lib/memory/index.js +2 -2
- package/dist/lib/memory/pathOps.js +94 -0
- package/dist/lib/memory/utils.js +10 -15
- package/dist/lib/reactive/createTypedScope.js +4 -7
- package/dist/lib/scope/ScopeFacade.js +6 -10
- package/dist/types/lib/memory/index.d.ts +1 -1
- package/dist/types/lib/memory/pathOps.d.ts +20 -0
- package/dist/types/lib/memory/utils.d.ts +1 -1
- package/package.json +2 -15
- package/ai-instructions/claude-code/SKILL.md +0 -580
- package/ai-instructions/clinerules +0 -119
- package/ai-instructions/copilot-instructions.md +0 -118
- package/ai-instructions/cursor/footprint.md +0 -118
- package/ai-instructions/kiro/footprint.md +0 -118
- package/ai-instructions/setup.sh +0 -140
- package/ai-instructions/windsurfrules +0 -119
|
@@ -1,580 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: footprint
|
|
3
|
-
description: Use when building flowchart pipelines with footprintjs — stage functions, decider branches, selectors, subflows, loops, narrative traces, recorders, redaction, contracts, and LLM-ready output. Also use when someone asks how footprint.js works or wants to understand the library.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# footprint.js — The Flowchart Pattern for Backend Code
|
|
7
|
-
|
|
8
|
-
footprint.js structures backend logic as a graph of named functions with transactional state. The code becomes self-explainable: every run auto-generates a causal trace of what happened and why.
|
|
9
|
-
|
|
10
|
-
**Core principle:** All data collection happens during the single DFS traversal pass — never post-process or walk the tree again.
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
npm install footprintjs
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
## Quick Start
|
|
19
|
-
|
|
20
|
-
```typescript
|
|
21
|
-
import { flowChart, FlowChartExecutor } from 'footprintjs';
|
|
22
|
-
|
|
23
|
-
interface OrderState {
|
|
24
|
-
orderId: string;
|
|
25
|
-
amount: number;
|
|
26
|
-
paymentStatus: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const chart = flowChart<OrderState>('ReceiveOrder', (scope) => {
|
|
30
|
-
scope.orderId = 'ORD-123';
|
|
31
|
-
scope.amount = 49.99;
|
|
32
|
-
}, 'receive-order', undefined, 'Receive and validate the incoming order')
|
|
33
|
-
.addFunction('ProcessPayment', (scope) => {
|
|
34
|
-
const amount = scope.amount;
|
|
35
|
-
scope.paymentStatus = amount < 100 ? 'approved' : 'review';
|
|
36
|
-
}, 'process-payment', 'Charge customer and record payment status')
|
|
37
|
-
.build();
|
|
38
|
-
|
|
39
|
-
const executor = new FlowChartExecutor(chart);
|
|
40
|
-
await executor.run({ input: { orderId: 'ORD-123' } });
|
|
41
|
-
|
|
42
|
-
console.log(executor.getNarrative());
|
|
43
|
-
// Stage 1: The process began with ReceiveOrder.
|
|
44
|
-
// Step 1: Write orderId = "ORD-123"
|
|
45
|
-
// Step 2: Write amount = 49.99
|
|
46
|
-
// Stage 2: Next, it moved on to ProcessPayment.
|
|
47
|
-
// Step 1: Read amount = 49.99
|
|
48
|
-
// Step 2: Write paymentStatus = "approved"
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
## FlowChartBuilder API
|
|
54
|
-
|
|
55
|
-
Always chain from `flowChart<T>()` (recommended) or `flowChart()`.
|
|
56
|
-
|
|
57
|
-
### Linear Stages
|
|
58
|
-
|
|
59
|
-
```typescript
|
|
60
|
-
import { flowChart } from 'footprintjs';
|
|
61
|
-
|
|
62
|
-
interface MyState {
|
|
63
|
-
valueA: string;
|
|
64
|
-
valueB: number;
|
|
65
|
-
valueC: boolean;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const chart = flowChart<MyState>('StageA', fnA, 'stage-a', undefined, 'Description of A')
|
|
69
|
-
.addFunction('StageB', fnB, 'stage-b', 'Description of B')
|
|
70
|
-
.addFunction('StageC', fnC, 'stage-c', 'Description of C')
|
|
71
|
-
.build();
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
**Parameters:** `(name: string, fn: PipelineStageFunction, id: string, description?: string)`
|
|
75
|
-
|
|
76
|
-
- `name` — human-readable label (used in narrative)
|
|
77
|
-
- `fn` — the stage function
|
|
78
|
-
- `id` — stable identifier (used for branching, visualization, loop targets)
|
|
79
|
-
- `description` — optional, appears in narrative and auto-generated tool descriptions
|
|
80
|
-
|
|
81
|
-
### Stage Function Signature (TypedScope)
|
|
82
|
-
|
|
83
|
-
With `flowChart<T>()`, stage functions receive a `TypedScope<T>` proxy. All reads and writes use typed property access:
|
|
84
|
-
|
|
85
|
-
```typescript
|
|
86
|
-
interface LoanState {
|
|
87
|
-
creditTier: string;
|
|
88
|
-
amount: number;
|
|
89
|
-
customer: { name: string; address: { zip: string } };
|
|
90
|
-
tags: string[];
|
|
91
|
-
approved?: boolean;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const myStage = (scope: TypedScope<LoanState>) => {
|
|
95
|
-
// Typed writes (tracked — appear in narrative)
|
|
96
|
-
scope.creditTier = 'A';
|
|
97
|
-
scope.amount = 50000;
|
|
98
|
-
|
|
99
|
-
// Deep write (auto-delegates to updateValue)
|
|
100
|
-
scope.customer.address.zip = '90210';
|
|
101
|
-
|
|
102
|
-
// Array copy-on-write
|
|
103
|
-
scope.tags.push('vip');
|
|
104
|
-
|
|
105
|
-
// Optional fields
|
|
106
|
-
scope.approved = true;
|
|
107
|
-
|
|
108
|
-
// $-prefixed escape hatches for non-state operations
|
|
109
|
-
scope.$debug('checkpoint', { step: 1 });
|
|
110
|
-
scope.$metric('latency', 42);
|
|
111
|
-
const args = scope.$getArgs<{ requestId: string }>();
|
|
112
|
-
const env = scope.$getEnv();
|
|
113
|
-
scope.$break(); // stop pipeline execution early
|
|
114
|
-
};
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
**Three access tiers:**
|
|
118
|
-
- **Typed properties** (`scope.amount = 50000`) — mutable shared state, tracked in narrative
|
|
119
|
-
- **`$getArgs()`** — frozen business input from `run({ input })`, NOT tracked
|
|
120
|
-
- **`$getEnv()`** — frozen infrastructure context from `run({ env })`, NOT tracked. Returns `ExecutionEnv { signal?, timeoutMs?, traceId? }`. Auto-inherited by subflows. Closed type.
|
|
121
|
-
|
|
122
|
-
### Decider Branches with decide() (Single-Choice Conditional)
|
|
123
|
-
|
|
124
|
-
Use `decide()` for structured decision evidence capture. It auto-records which values led to the decision in the narrative.
|
|
125
|
-
|
|
126
|
-
```typescript
|
|
127
|
-
import { decide } from 'footprintjs';
|
|
128
|
-
|
|
129
|
-
interface RiskState {
|
|
130
|
-
creditScore: number;
|
|
131
|
-
dti: number;
|
|
132
|
-
riskTier: string;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const chart = flowChart<RiskState>('Intake', intakeFn, 'intake')
|
|
136
|
-
.addDeciderFunction('AssessRisk', (scope) => {
|
|
137
|
-
// decide() captures filter evidence automatically
|
|
138
|
-
return decide(scope, [
|
|
139
|
-
{ when: { creditScore: { gt: 700 }, dti: { lt: 0.43 } }, then: 'low-risk', label: 'Good credit' },
|
|
140
|
-
{ when: (s) => s.creditScore > 600, then: 'medium-risk', label: 'Marginal credit' },
|
|
141
|
-
], 'high-risk');
|
|
142
|
-
// Narrative: "It evaluated Rule 0 'Good credit': creditScore 750 gt 700, and chose low-risk."
|
|
143
|
-
}, 'assess-risk', 'Evaluate risk and route accordingly')
|
|
144
|
-
.addFunctionBranch('high-risk', 'RejectApplication', rejectFn, 'Reject due to high risk')
|
|
145
|
-
.addFunctionBranch('medium-risk', 'ManualReview', reviewFn, 'Send to manual review')
|
|
146
|
-
.addFunctionBranch('low-risk', 'ApproveApplication', approveFn, 'Approve the application')
|
|
147
|
-
.setDefault('high-risk') // fallback if branch ID doesn't match
|
|
148
|
-
.end()
|
|
149
|
-
.build();
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
The `decide()` function accepts two `when` formats:
|
|
153
|
-
- **Filter format:** `{ creditScore: { gt: 700 } }` — declarative, auto-captures evidence
|
|
154
|
-
- **Function format:** `(s) => s.creditScore > 600` — arbitrary logic with optional `label`
|
|
155
|
-
|
|
156
|
-
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.
|
|
157
|
-
|
|
158
|
-
### Selector Branches with select() (Multi-Choice Fan-Out)
|
|
159
|
-
|
|
160
|
-
Use `select()` for structured multi-choice evidence capture:
|
|
161
|
-
|
|
162
|
-
```typescript
|
|
163
|
-
import { select } from 'footprintjs';
|
|
164
|
-
|
|
165
|
-
interface CheckState {
|
|
166
|
-
needsCredit: boolean;
|
|
167
|
-
needsIdentity: boolean;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const chart = flowChart<CheckState>('Intake', intakeFn, 'intake')
|
|
171
|
-
.addSelectorFunction('SelectChecks', (scope) => {
|
|
172
|
-
return select(scope, [
|
|
173
|
-
{ when: { needsCredit: { eq: true } }, then: 'credit-check', label: 'Credit required' },
|
|
174
|
-
{ when: { needsIdentity: { eq: true } }, then: 'identity-check', label: 'Identity required' },
|
|
175
|
-
]);
|
|
176
|
-
// Returns array of matching branch IDs
|
|
177
|
-
}, 'select-checks')
|
|
178
|
-
.addFunctionBranch('credit-check', 'CreditCheck', creditFn)
|
|
179
|
-
.addFunctionBranch('identity-check', 'IdentityCheck', identityFn)
|
|
180
|
-
.end()
|
|
181
|
-
.build();
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
### Parallel Execution (Fork)
|
|
185
|
-
|
|
186
|
-
```typescript
|
|
187
|
-
builder.addListOfFunction([
|
|
188
|
-
{ id: 'check-a', name: 'CheckA', fn: checkAFn },
|
|
189
|
-
{ id: 'check-b', name: 'CheckB', fn: checkBFn },
|
|
190
|
-
{ id: 'check-c', name: 'CheckC', fn: checkCFn },
|
|
191
|
-
], { failFast: true }); // reject on first error
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
### Subflows (Nested Flowcharts)
|
|
195
|
-
|
|
196
|
-
```typescript
|
|
197
|
-
// Build a reusable sub-pipeline
|
|
198
|
-
const creditSubflow = flowChart<CreditState>('PullReport', pullReportFn, 'pull-report')
|
|
199
|
-
.addFunction('ScoreReport', scoreReportFn, 'score-report')
|
|
200
|
-
.build();
|
|
201
|
-
|
|
202
|
-
// Mount as linear continuation
|
|
203
|
-
builder.addSubFlowChartNext('credit-sub', creditSubflow, 'CreditCheck', {
|
|
204
|
-
inputMapper: (parentScope) => ({ ssn: parentScope.ssn }),
|
|
205
|
-
outputMapper: (subOut, parentScope) => ({ creditScore: subOut.score }),
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// Mount as decider branch
|
|
209
|
-
builder.addDeciderFunction('Route', routerFn, 'route')
|
|
210
|
-
.addSubFlowChartBranch('detailed', creditSubflow, 'DetailedCheck')
|
|
211
|
-
.addFunctionBranch('simple', 'SimpleCheck', simpleFn)
|
|
212
|
-
.end();
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### Loops
|
|
216
|
-
|
|
217
|
-
```typescript
|
|
218
|
-
interface RetryState {
|
|
219
|
-
attempts: number;
|
|
220
|
-
paymentResult?: string;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
builder
|
|
224
|
-
.addFunction('RetryPayment', async (scope) => {
|
|
225
|
-
scope.attempts = (scope.attempts ?? 0) + 1;
|
|
226
|
-
if (scope.attempts >= 3) return; // exit loop by not looping
|
|
227
|
-
}, 'retry-payment')
|
|
228
|
-
.loopTo('retry-payment'); // back-edge to this stage's ID
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
### Configuration
|
|
232
|
-
|
|
233
|
-
```typescript
|
|
234
|
-
builder
|
|
235
|
-
.contract({ // I/O schemas + output mapper
|
|
236
|
-
input: zodSchema, // validate input (Zod or JSON Schema)
|
|
237
|
-
output: outputZodSchema, // declare output shape
|
|
238
|
-
mapper: (state) => ({ // map final state to response
|
|
239
|
-
decision: state.decision,
|
|
240
|
-
reason: state.reason,
|
|
241
|
-
}),
|
|
242
|
-
});
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
### Output
|
|
246
|
-
|
|
247
|
-
```typescript
|
|
248
|
-
const chart = builder.build(); // FlowChart object (for executor)
|
|
249
|
-
const spec = builder.toSpec(); // JSON-safe structure (for visualization)
|
|
250
|
-
const mermaid = builder.toMermaid(); // Mermaid diagram string
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
---
|
|
254
|
-
|
|
255
|
-
## FlowChartExecutor API
|
|
256
|
-
|
|
257
|
-
```typescript
|
|
258
|
-
import { FlowChartExecutor } from 'footprintjs';
|
|
259
|
-
|
|
260
|
-
interface AppState {
|
|
261
|
-
applicantName: string;
|
|
262
|
-
income: number;
|
|
263
|
-
riskTier?: string;
|
|
264
|
-
decision?: string;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const executor = new FlowChartExecutor(chart);
|
|
268
|
-
|
|
269
|
-
// Run with input and optional execution environment
|
|
270
|
-
const result = await executor.run({
|
|
271
|
-
input: { applicantName: 'Bob', income: 42000 },
|
|
272
|
-
env: { traceId: 'req-123', timeoutMs: 5000 },
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// Get narrative (combined flow + data operations)
|
|
276
|
-
const narrative: string[] = executor.getNarrative();
|
|
277
|
-
// ["Stage 1: The process began with ReceiveApplication.",
|
|
278
|
-
// " Step 1: Write applicantName = \"Bob\"",
|
|
279
|
-
// "Stage 2: Next, it moved on to AssessRisk.",
|
|
280
|
-
// " Step 1: Read income = 42000",
|
|
281
|
-
// " Step 2: Write riskTier = \"high\"",
|
|
282
|
-
// "[Condition]: A decision was made, path taken was RejectApplication."]
|
|
283
|
-
|
|
284
|
-
// Structured entries (for programmatic access)
|
|
285
|
-
const entries: CombinedNarrativeEntry[] = executor.getNarrativeEntries();
|
|
286
|
-
// [{ type: 'stage', text: '...', depth: 0, stageName: 'ReceiveApplication' },
|
|
287
|
-
// { type: 'step', text: 'Write applicantName = ...', depth: 1, stageName: 'ReceiveApplication', stepNumber: 1 },
|
|
288
|
-
// { type: 'condition', text: '...', depth: 0 }]
|
|
289
|
-
|
|
290
|
-
// Full memory snapshot
|
|
291
|
-
const snapshot = executor.getSnapshot();
|
|
292
|
-
// { sharedState: { ... }, commitLog: [...], subflowResults: Map }
|
|
293
|
-
|
|
294
|
-
// Flow-only narrative (no data operations)
|
|
295
|
-
const flowOnly: string[] = executor.getFlowNarrative();
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
---
|
|
299
|
-
|
|
300
|
-
## Recorder System — Collect During Traversal
|
|
301
|
-
|
|
302
|
-
**The core innovation.** Two observer layers fire during the single DFS pass:
|
|
303
|
-
|
|
304
|
-
### Scope Recorders (data operations)
|
|
305
|
-
|
|
306
|
-
Fire during typed property access (reads/writes). Attach via `executor.attachRecorder()`:
|
|
307
|
-
|
|
308
|
-
```typescript
|
|
309
|
-
import { MetricRecorder, DebugRecorder } from 'footprintjs';
|
|
310
|
-
|
|
311
|
-
// Built-in recorders
|
|
312
|
-
const metrics = new MetricRecorder();
|
|
313
|
-
const debug = new DebugRecorder('verbose'); // or 'minimal'
|
|
314
|
-
|
|
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 });
|
|
320
|
-
|
|
321
|
-
// After execution
|
|
322
|
-
metrics.getSummary(); // { totalReads: 12, totalWrites: 8, stages: {...} }
|
|
323
|
-
debug.getEntries(); // [{ type: 'read', key: 'app', value: {...}, stage: 'Intake' }, ...]
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
### FlowRecorders (control flow events)
|
|
327
|
-
|
|
328
|
-
Attached to executor, fire after each stage/decision/fork:
|
|
329
|
-
|
|
330
|
-
```typescript
|
|
331
|
-
import { NarrativeFlowRecorder, AdaptiveNarrativeFlowRecorder } from 'footprintjs';
|
|
332
|
-
|
|
333
|
-
// Attach before run()
|
|
334
|
-
executor.attachFlowRecorder(new NarrativeFlowRecorder());
|
|
335
|
-
|
|
336
|
-
// 8 built-in strategies:
|
|
337
|
-
// NarrativeFlowRecorder — all events as sentences (default)
|
|
338
|
-
// AdaptiveNarrativeFlowRecorder — full detail then sampling for loops
|
|
339
|
-
// WindowedNarrativeFlowRecorder — keep last N iterations only
|
|
340
|
-
// RLENarrativeFlowRecorder — run-length encode repeated loops
|
|
341
|
-
// MilestoneNarrativeFlowRecorder — only decisions, errors, subflows
|
|
342
|
-
// ProgressiveNarrativeFlowRecorder — progress markers for streaming UIs
|
|
343
|
-
// SeparateNarrativeFlowRecorder — dual channels (main + loop detail)
|
|
344
|
-
// ManifestFlowRecorder — subflow tree + spec catalog for LLM exploration
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
### Custom FlowRecorder
|
|
348
|
-
|
|
349
|
-
```typescript
|
|
350
|
-
import type { FlowRecorder, FlowStageEvent, FlowDecisionEvent } from 'footprintjs';
|
|
351
|
-
|
|
352
|
-
const myRecorder: FlowRecorder = {
|
|
353
|
-
id: 'my-recorder',
|
|
354
|
-
onStageExecuted(event: FlowStageEvent) {
|
|
355
|
-
console.log(`Executed: ${event.stageName}`);
|
|
356
|
-
},
|
|
357
|
-
onDecision(event: FlowDecisionEvent) {
|
|
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
|
-
}
|
|
363
|
-
},
|
|
364
|
-
clear() {
|
|
365
|
-
// Reset state before each run
|
|
366
|
-
},
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
executor.attachFlowRecorder(myRecorder);
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
### CombinedNarrativeRecorder (the inline dual-channel recorder)
|
|
373
|
-
|
|
374
|
-
This is what powers `getNarrative()` and `getNarrativeEntries()`. It implements BOTH `Recorder` (scope) and `FlowRecorder` (engine) interfaces. It buffers scope ops per-stage, then flushes when the flow event arrives — producing merged entries in a single pass.
|
|
375
|
-
|
|
376
|
-
**You don't need to create this manually.** Use `executor.recorder(narrative())` at runtime to attach it.
|
|
377
|
-
|
|
378
|
-
---
|
|
379
|
-
|
|
380
|
-
## Redaction (PII Protection)
|
|
381
|
-
|
|
382
|
-
```typescript
|
|
383
|
-
executor.setRedactionPolicy({
|
|
384
|
-
keys: ['ssn', 'creditCardNumber'], // exact key names
|
|
385
|
-
patterns: [/password/i, /^secret.*/], // regex patterns
|
|
386
|
-
fields: { applicant: ['ssn', 'address.zip'] }, // nested field paths
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
await executor.run({ input: { ... } });
|
|
390
|
-
|
|
391
|
-
// Narrative shows: Write ssn = [REDACTED]
|
|
392
|
-
// Recorders receive scrubbed values
|
|
393
|
-
const report = executor.getRedactionReport();
|
|
394
|
-
// { redactedKeys: ['ssn'], patternsMatched: [...] } — never contains actual values
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
---
|
|
398
|
-
|
|
399
|
-
## Contracts & OpenAPI
|
|
400
|
-
|
|
401
|
-
```typescript
|
|
402
|
-
import { flowChart } from 'footprintjs';
|
|
403
|
-
import { z } from 'zod';
|
|
404
|
-
|
|
405
|
-
const chart = flowChart('ProcessLoan', receiveFn)
|
|
406
|
-
.addFunction('Assess', assessFn)
|
|
407
|
-
.contract({
|
|
408
|
-
input: z.object({
|
|
409
|
-
applicantName: z.string(),
|
|
410
|
-
income: z.number(),
|
|
411
|
-
}),
|
|
412
|
-
output: z.object({
|
|
413
|
-
decision: z.enum(['approved', 'rejected']),
|
|
414
|
-
reason: z.string(),
|
|
415
|
-
}),
|
|
416
|
-
mapper: (state) => ({
|
|
417
|
-
decision: state.decision,
|
|
418
|
-
reason: state.reason,
|
|
419
|
-
}),
|
|
420
|
-
})
|
|
421
|
-
.build();
|
|
422
|
-
|
|
423
|
-
const openApiSpec = chart.toOpenAPI({
|
|
424
|
-
title: 'Loan Underwriting API',
|
|
425
|
-
version: '1.0.0',
|
|
426
|
-
});
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
---
|
|
430
|
-
|
|
431
|
-
## Event Ordering (Critical for Understanding)
|
|
432
|
-
|
|
433
|
-
When a stage executes, events fire in this exact order:
|
|
434
|
-
|
|
435
|
-
```
|
|
436
|
-
1. Recorder.onStageStart — stage begins
|
|
437
|
-
2. Recorder.onRead — each typed property read (DURING execution)
|
|
438
|
-
3. Recorder.onWrite — each typed property write (DURING execution)
|
|
439
|
-
4. Recorder.onCommit — transaction buffer flushes to shared memory
|
|
440
|
-
5. Recorder.onStageEnd — stage completes
|
|
441
|
-
6. FlowRecorder.onStageExecuted — control flow records the stage
|
|
442
|
-
(CombinedNarrativeRecorder flushes buffered ops here)
|
|
443
|
-
7. FlowRecorder.onNext — moving to next stage
|
|
444
|
-
OR FlowRecorder.onDecision — if this was a decider (carries evidence from decide())
|
|
445
|
-
OR FlowRecorder.onFork — if children execute in parallel
|
|
446
|
-
OR FlowRecorder.onSelected — if this was a selector (carries evidence from select())
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
**This ordering is what makes inline collection work.** Scope events buffer during execution, flow events trigger the flush.
|
|
450
|
-
|
|
451
|
-
---
|
|
452
|
-
|
|
453
|
-
## Anti-Patterns to Avoid
|
|
454
|
-
|
|
455
|
-
1. **Never post-process the tree.** Don't walk the spec after execution to collect data. Use recorders.
|
|
456
|
-
2. **Don't use `getValue()`/`setValue()` in TypedScope stages.** Use typed property access (`scope.amount = 50000`). The old ScopeFacade API is internal only.
|
|
457
|
-
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`).
|
|
458
|
-
4. **Never use `CombinedNarrativeBuilder`** — it's deprecated. Use `CombinedNarrativeRecorder` (attached via `executor.recorder(narrative())`).
|
|
459
|
-
5. **Don't extract a shared base class** for Recorder and FlowRecorder. They look similar but serve different layers. Two instances = coincidence.
|
|
460
|
-
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.
|
|
461
|
-
7. **Don't put infrastructure data in `$getArgs()`.** Use `$getEnv()` via `run({ env })` for signals, timeouts, and trace IDs.
|
|
462
|
-
8. **Don't create scope recorders manually** unless building a custom recorder. `executor.recorder(narrative())` handles everything.
|
|
463
|
-
|
|
464
|
-
---
|
|
465
|
-
|
|
466
|
-
## Library Structure (for contributors)
|
|
467
|
-
|
|
468
|
-
```
|
|
469
|
-
src/lib/
|
|
470
|
-
├── memory/ → SharedMemory, StageContext, TransactionBuffer, EventLog (foundation)
|
|
471
|
-
├── schema/ → detectSchema, validate, InputValidationError (foundation)
|
|
472
|
-
├── builder/ → FlowChartBuilder, flowChart(), DeciderList, SelectorFnList (standalone)
|
|
473
|
-
├── scope/ → ScopeFacade, recorders/, providers/, protection/ (depends: memory)
|
|
474
|
-
├── reactive/ → TypedScope<T> deep Proxy, typed property access, $-methods, cycle-safe (depends: scope)
|
|
475
|
-
├── decide/ → decide()/select() decision evidence capture, filter + function when formats (depends: scope)
|
|
476
|
-
├── engine/ → FlowchartTraverser, handlers/, narrative/ (depends: memory, scope, reactive, builder)
|
|
477
|
-
├── runner/ → FlowChartExecutor, ExecutionRuntime (depends: engine, scope, schema)
|
|
478
|
-
└── contract/ → I/O schema + OpenAPI generation (depends: schema)
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
Dependency DAG: `memory <- scope <- reactive <- engine <- runner`, `schema <- engine`, `builder (standalone) -> engine`, `contract <- schema`, `decide -> scope`
|
|
482
|
-
|
|
483
|
-
Two entry points:
|
|
484
|
-
- `import { ... } from 'footprintjs'` — public API
|
|
485
|
-
- `import { ... } from 'footprintjs/advanced'` — internals (memory, traverser, handlers)
|
|
486
|
-
|
|
487
|
-
---
|
|
488
|
-
|
|
489
|
-
## Common Patterns
|
|
490
|
-
|
|
491
|
-
### Pipeline with decide() + narrative
|
|
492
|
-
|
|
493
|
-
```typescript
|
|
494
|
-
import { flowChart, FlowChartExecutor, decide } from 'footprintjs';
|
|
495
|
-
|
|
496
|
-
interface LoanState {
|
|
497
|
-
applicantName: string;
|
|
498
|
-
income: number;
|
|
499
|
-
creditScore: number;
|
|
500
|
-
dti: number;
|
|
501
|
-
decision?: string;
|
|
502
|
-
reason?: string;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
const chart = flowChart<LoanState>('Receive', (scope) => {
|
|
506
|
-
const args = scope.$getArgs<{ applicantName: string; income: number }>();
|
|
507
|
-
scope.applicantName = args.applicantName;
|
|
508
|
-
scope.income = args.income;
|
|
509
|
-
}, 'receive')
|
|
510
|
-
.addFunction('Analyze', (scope) => {
|
|
511
|
-
scope.creditScore = 750; // from credit bureau
|
|
512
|
-
scope.dti = 0.35; // computed
|
|
513
|
-
}, 'analyze')
|
|
514
|
-
.addDeciderFunction('Decide', (scope) => {
|
|
515
|
-
return decide(scope, [
|
|
516
|
-
{ when: { creditScore: { gt: 700 }, dti: { lt: 0.43 } }, then: 'approve', label: 'Good credit' },
|
|
517
|
-
{ when: (s) => s.creditScore > 600, then: 'approve', label: 'Marginal but acceptable' },
|
|
518
|
-
], 'reject');
|
|
519
|
-
}, 'decide')
|
|
520
|
-
.addFunctionBranch('approve', 'Approve', (scope) => {
|
|
521
|
-
scope.decision = 'approved';
|
|
522
|
-
scope.reason = 'Meets credit criteria';
|
|
523
|
-
})
|
|
524
|
-
.addFunctionBranch('reject', 'Reject', (scope) => {
|
|
525
|
-
scope.decision = 'rejected';
|
|
526
|
-
scope.reason = 'Does not meet credit criteria';
|
|
527
|
-
})
|
|
528
|
-
.setDefault('reject')
|
|
529
|
-
.end()
|
|
530
|
-
.build();
|
|
531
|
-
|
|
532
|
-
const executor = new FlowChartExecutor(chart);
|
|
533
|
-
await executor.run({ input: { applicantName: 'Bob', income: 42000 } });
|
|
534
|
-
const trace = executor.getNarrative();
|
|
535
|
-
// Feed trace to LLM for grounded explanations
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
### Subflow with input/output mapping
|
|
539
|
-
|
|
540
|
-
```typescript
|
|
541
|
-
interface SubState {
|
|
542
|
-
ssn: string;
|
|
543
|
-
score: number;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
interface MainState {
|
|
547
|
-
ssn: string;
|
|
548
|
-
parentKey: string;
|
|
549
|
-
creditScore?: number;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const subflow = flowChart<SubState>('SubStart', subStartFn, 'sub-start')
|
|
553
|
-
.addFunction('SubProcess', subProcessFn, 'sub-process')
|
|
554
|
-
.build();
|
|
555
|
-
|
|
556
|
-
const main = flowChart<MainState>('Main', mainFn, 'main')
|
|
557
|
-
.addSubFlowChartNext('my-subflow', subflow, 'SubflowMount', {
|
|
558
|
-
inputMapper: (scope) => ({ ssn: scope.ssn }),
|
|
559
|
-
outputMapper: (subOut) => ({ creditScore: subOut.score }),
|
|
560
|
-
})
|
|
561
|
-
.build();
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
### Attach multiple recorders
|
|
565
|
-
|
|
566
|
-
```typescript
|
|
567
|
-
import { ManifestFlowRecorder, MilestoneNarrativeFlowRecorder, MetricRecorder } from 'footprintjs';
|
|
568
|
-
|
|
569
|
-
// Scope recorder (data ops) — via executor.attachRecorder()
|
|
570
|
-
executor.attachRecorder(new MetricRecorder());
|
|
571
|
-
|
|
572
|
-
// Flow recorders (control flow) — via executor.attachFlowRecorder()
|
|
573
|
-
executor.attachFlowRecorder(new ManifestFlowRecorder());
|
|
574
|
-
executor.attachFlowRecorder(new MilestoneNarrativeFlowRecorder());
|
|
575
|
-
|
|
576
|
-
await executor.run({ input: data });
|
|
577
|
-
|
|
578
|
-
const manifest = executor.getSubflowManifest(); // subflow catalog
|
|
579
|
-
const milestones = executor.getFlowNarrative(); // key events only
|
|
580
|
-
```
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
# footprint.js
|
|
2
|
-
|
|
3
|
-
This is the footprint.js library — the flowchart pattern for backend code. Self-explainable systems that AI can reason about.
|
|
4
|
-
|
|
5
|
-
## Core Principle
|
|
6
|
-
|
|
7
|
-
**Collect during traversal, never post-process.** All data collection happens as side effects of the single DFS traversal. Never walk the tree after execution.
|
|
8
|
-
|
|
9
|
-
## Architecture
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
src/lib/
|
|
13
|
-
├── memory/ → Transactional state (SharedMemory, StageContext, TransactionBuffer)
|
|
14
|
-
├── schema/ → Validation (Zod optional, duck-typed)
|
|
15
|
-
├── builder/ → Fluent DSL (FlowChartBuilder, flowChart(), typedFlowChart())
|
|
16
|
-
├── scope/ → Per-stage facades + recorders + providers
|
|
17
|
-
├── reactive/ → TypedScope<T> deep Proxy (typed property access, $-methods)
|
|
18
|
-
├── decide/ → decide()/select() decision evidence capture
|
|
19
|
-
├── engine/ → DFS traversal + narrative + handlers
|
|
20
|
-
├── runner/ → FlowChartExecutor
|
|
21
|
-
└── contract/ → I/O schema + OpenAPI
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Entry points: `footprintjs` (public) and `footprintjs/advanced` (internals).
|
|
25
|
-
|
|
26
|
-
## Key API — TypedScope (Recommended)
|
|
27
|
-
|
|
28
|
-
```typescript
|
|
29
|
-
import { typedFlowChart, 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')
|
|
41
|
-
.setEnableNarrative()
|
|
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<State>());
|
|
58
|
-
await executor.run();
|
|
59
|
-
executor.getNarrative(); // causal trace with decision evidence
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### TypedScope $-methods (escape hatches)
|
|
63
|
-
|
|
64
|
-
```typescript
|
|
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
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
### decide() / select()
|
|
73
|
-
|
|
74
|
-
```typescript
|
|
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
|
-
]);
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### Executor
|
|
93
|
-
|
|
94
|
-
```typescript
|
|
95
|
-
const executor = new FlowChartExecutor(chart<State>());
|
|
96
|
-
await executor.run({ input, env: { traceId: 'req-123' } });
|
|
97
|
-
executor.getNarrative() // string[]
|
|
98
|
-
executor.getNarrativeEntries() // CombinedNarrativeEntry[]
|
|
99
|
-
executor.getSnapshot() // memory state
|
|
100
|
-
executor.attachRecorder(recorder) // scope observer
|
|
101
|
-
executor.attachFlowRecorder(r) // flow observer
|
|
102
|
-
executor.setRedactionPolicy({ keys, patterns, fields })
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
## Observer Systems
|
|
106
|
-
|
|
107
|
-
- **Scope Recorder**: fires DURING stage (`onRead`, `onWrite`, `onCommit`)
|
|
108
|
-
- **FlowRecorder**: fires AFTER stage (`onStageExecuted`, `onDecision`, `onFork`, `onLoop`)
|
|
109
|
-
- 8 built-in FlowRecorder strategies
|
|
110
|
-
- `setEnableNarrative()` auto-attaches `CombinedNarrativeRecorder`
|
|
111
|
-
|
|
112
|
-
## Rules
|
|
113
|
-
|
|
114
|
-
- Use `typedFlowChart<T>()` — scopeFactory is auto-embedded, no `createTypedScopeFactory` needed
|
|
115
|
-
- Use `decide()` / `select()` in decider/selector functions
|
|
116
|
-
- Use typed property access (not getValue/setValue)
|
|
117
|
-
- Use `$getArgs()` for input, `$getEnv()` for environment
|
|
118
|
-
- Never post-process the tree — use recorders
|
|
119
|
-
- `setEnableNarrative()` is all you need for narrative setup
|