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.
Files changed (43) hide show
  1. package/AGENTS.md +81 -87
  2. package/README.md +2 -2
  3. package/ai-instructions/claude-code/SKILL.md +204 -91
  4. package/ai-instructions/clinerules +71 -31
  5. package/ai-instructions/copilot-instructions.md +67 -34
  6. package/ai-instructions/cursor/footprint.md +73 -34
  7. package/ai-instructions/kiro/footprint.md +70 -30
  8. package/ai-instructions/windsurfrules +71 -31
  9. package/dist/esm/index.js +8 -11
  10. package/dist/esm/lib/builder/FlowChartBuilder.js +26 -2
  11. package/dist/esm/lib/builder/typedFlowChart.js +4 -2
  12. package/dist/esm/lib/builder/types.js +1 -1
  13. package/dist/esm/lib/engine/types.js +1 -1
  14. package/dist/esm/lib/reactive/createTypedScope.js +28 -6
  15. package/dist/esm/lib/runner/FlowChartExecutor.js +13 -3
  16. package/dist/esm/lib/runner/RunContext.js +75 -0
  17. package/dist/esm/lib/runner/RunnableChart.js +104 -0
  18. package/dist/esm/lib/runner/index.js +2 -1
  19. package/dist/esm/lib/scope/ScopeFacade.js +12 -4
  20. package/dist/esm/recorders.js +69 -0
  21. package/dist/index.js +43 -45
  22. package/dist/lib/builder/FlowChartBuilder.js +26 -2
  23. package/dist/lib/builder/typedFlowChart.js +4 -2
  24. package/dist/lib/builder/types.js +1 -1
  25. package/dist/lib/engine/types.js +1 -1
  26. package/dist/lib/reactive/createTypedScope.js +28 -6
  27. package/dist/lib/runner/FlowChartExecutor.js +13 -3
  28. package/dist/lib/runner/RunContext.js +79 -0
  29. package/dist/lib/runner/RunnableChart.js +108 -0
  30. package/dist/lib/runner/index.js +4 -2
  31. package/dist/lib/scope/ScopeFacade.js +12 -4
  32. package/dist/recorders.js +79 -0
  33. package/dist/types/index.d.ts +8 -6
  34. package/dist/types/lib/builder/FlowChartBuilder.d.ts +18 -1
  35. package/dist/types/lib/builder/types.d.ts +5 -3
  36. package/dist/types/lib/engine/types.d.ts +2 -0
  37. package/dist/types/lib/runner/FlowChartExecutor.d.ts +9 -0
  38. package/dist/types/lib/runner/RunContext.d.ts +38 -0
  39. package/dist/types/lib/runner/RunnableChart.d.ts +42 -0
  40. package/dist/types/lib/runner/index.d.ts +3 -0
  41. package/dist/types/lib/scope/ScopeFacade.d.ts +11 -3
  42. package/dist/types/recorders.d.ts +43 -0
  43. package/package.json +6 -1
@@ -18,20 +18,26 @@ npm install footprintjs
18
18
  ## Quick Start
19
19
 
20
20
  ```typescript
21
- import { flowChart, FlowChartExecutor } from 'footprintjs';
21
+ import { typedFlowChart, createTypedScopeFactory, FlowChartExecutor } from 'footprintjs';
22
22
 
23
- const chart = flowChart('ReceiveOrder', (scope) => {
24
- scope.setValue('orderId', 'ORD-123');
25
- scope.setValue('amount', 49.99);
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.getValue('amount');
29
- scope.setValue('paymentStatus', amount < 100 ? 'approved' : 'review');
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 `flowChart()` or `new FlowChartBuilder()`.
56
+ Always chain from `typedFlowChart<T>()` (recommended) or `flowChart()`.
51
57
 
52
58
  ### Linear Stages
53
59
 
54
60
  ```typescript
55
- import { flowChart } from 'footprintjs';
61
+ import { typedFlowChart } from 'footprintjs';
62
+
63
+ interface MyState {
64
+ valueA: string;
65
+ valueB: number;
66
+ valueC: boolean;
67
+ }
56
68
 
57
- const chart = flowChart('StageA', fnA, 'stage-a', undefined, 'Description of A')
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
- Every stage receives a `ScopeFacade` that tracks all reads and writes:
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
- const myStage = (scope: ScopeFacade) => {
86
- // Read (tracked — appears in narrative)
87
- const name = scope.getValue('applicantName') as string;
88
-
89
- // Write (tracked — appears in narrative)
90
- scope.setValue('greeting', `Hello, ${name}!`);
91
-
92
- // Update (deep merge for objects)
93
- scope.updateValue('profile', { verified: true });
94
-
95
- // Delete
96
- scope.deleteValue('tempData');
97
-
98
- // Readonly input (frozen, not tracked in narrative)
99
- const config = scope.getArgs<{ maxRetries: number }>();
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
- **IMPORTANT:** `getValue`/`setValue` are tracked and produce narrative. `getArgs()` returns frozen readonly input and is NOT tracked.
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
- ### Decider Branches (Single-Choice Conditional)
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
- const chart = flowChart('Intake', intakeFn, 'intake')
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
- const score = scope.getValue('riskScore') as number;
111
- // Return the branch ID to take
112
- return score > 70 ? 'high-risk' : 'low-risk';
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 decider function **returns a branch ID string**. The engine matches it to a child and executes that branch. The decision is recorded in the narrative.
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
- ### Selector Branches (Multi-Choice Fan-Out)
161
+ Use `select()` for structured multi-choice evidence capture:
124
162
 
125
163
  ```typescript
126
- const chart = flowChart('Intake', intakeFn, 'intake')
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
- const checks = [];
129
- if (scope.getValue('needsCredit')) checks.push('credit-check');
130
- if (scope.getValue('needsIdentity')) checks.push('identity-check');
131
- // Return array of branch IDs to execute in parallel
132
- return checks;
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 = flowChart('PullReport', pullReportFn, 'pull-report')
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.getValue('ssn') }),
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
- const attempts = (scope.getValue('attempts') as number ?? 0) + 1;
177
- scope.setValue('attempts', attempts);
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
- const executor = new FlowChartExecutor(chart);
260
+ interface AppState {
261
+ applicantName: string;
262
+ income: number;
263
+ riskTier?: string;
264
+ decision?: string;
265
+ }
212
266
 
213
- // Run with input
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, // optional auto-abort
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 app = {applicantName, income, ...}",
278
+ // " Step 1: Write applicantName = \"Bob\"",
224
279
  // "Stage 2: Next, it moved on to AssessRisk.",
225
- // " Step 1: Read app = ...",
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 app = ...', depth: 1, stageName: 'ReceiveApplication', stepNumber: 1 },
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
- Attached to `ScopeFacade`, fire during `getValue()`/`setValue()`:
306
+ Fire during typed property access (reads/writes). Attach via `executor.attachRecorder()`:
252
307
 
253
308
  ```typescript
254
- import { MetricRecorder, DebugRecorder, NarrativeRecorder } from 'footprintjs';
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 scope (usually via scope factory)
261
- scope.attachRecorder(metrics);
262
- scope.attachRecorder(debug);
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 getValue() call (DURING execution)
374
- 3. Recorder.onWrite — each setValue() call (DURING execution)
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. **Never use `CombinedNarrativeBuilder`** it's deprecated. Use `CombinedNarrativeRecorder` (auto-attached by `setEnableNarrative()`).
392
- 3. **Don't extract a shared base class** for Recorder and FlowRecorder. They look similar but serve different layers. Two instances = coincidence.
393
- 4. **Don't call `getArgs()` for tracked data.** `getArgs()` returns frozen readonly input. Use `getValue()`/`setValue()` for state that should appear in the narrative.
394
- 5. **Don't create scope recorders manually** unless building a custom recorder. `setEnableNarrative()` handles everything.
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
- ├── engine/ FlowchartTraverser, handlers/, narrative/ (depends: memory, scope, builder)
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 decision + narrative
488
+ ### Pipeline with decide() + narrative
420
489
 
421
490
  ```typescript
422
- const chart = flowChart('Receive', receiveFn, 'receive')
423
- .addFunction('Analyze', analyzeFn, 'analyze')
424
- .addDeciderFunction('Decide', decideFn, 'decide')
425
- .addFunctionBranch('approve', 'Approve', approveFn)
426
- .addFunctionBranch('reject', 'Reject', rejectFn)
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: data });
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
- const subflow = flowChart('SubStart', subStartFn, 'sub-start')
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 = flowChart('Main', mainFn, 'main')
554
+ const main = typedFlowChart<MainState>('Main', mainFn, 'main')
446
555
  .addSubFlowChartNext('my-subflow', subflow, 'SubflowMount', {
447
- inputMapper: (scope) => ({ key: scope.getValue('parentKey') }),
448
- outputMapper: (subOut) => ({ result: subOut.processed }),
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 — Cline Rules
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
- ## Builder API
26
+ ## Key API — TypedScope (Recommended)
25
27
 
26
28
  ```typescript
27
- flowChart(name, fn, id, extractor?, description?)
28
- .addFunction(name, fn, id, description?)
29
- .addDeciderFunction(name, fn, id, description?)
30
- .addFunctionBranch(branchId, name, fn) / .setDefault(id) / .end()
31
- .addSelectorFunction(name, fn, id, description?)
32
- .addListOfFunction([...], { failFast? })
33
- .addSubFlowChartNext(id, subflow, mount, { inputMapper?, outputMapper? })
34
- .loopTo(stageId)
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
- .build() / .toSpec() / .toMermaid()
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
- ## Stage Functions
62
+ ### TypedScope $-methods (escape hatches)
40
63
 
41
64
  ```typescript
42
- (scope: ScopeFacade, breakPipeline: () => void, streamCallback?: StreamCallback) => void | Promise<void>
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
- ## ScopeFacade
72
+ ### decide() / select()
46
73
 
47
74
  ```typescript
48
- scope.getValue('key') // tracked read narrative
49
- scope.setValue('key', value) // tracked write → narrative
50
- scope.updateValue('key', partial) // deep merge (tracked)
51
- scope.deleteValue('key') // tracked delete
52
- scope.getArgs<T>() // frozen readonly input (NOT tracked)
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
- ## Executor
92
+ ### Executor
56
93
 
57
94
  ```typescript
58
- const executor = new FlowChartExecutor(chart);
59
- await executor.run({ input, timeoutMs?, signal? });
60
- executor.getNarrative() // combined flow + data
61
- executor.getNarrativeEntries() // structured entries
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.attachFlowRecorder(r) // plug flow observer
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 strategies: Narrative, Adaptive, Windowed, RLE, Milestone, Progressive, Separate, Manifest
72
- - `CombinedNarrativeRecorder` implements both — auto-attached by `setEnableNarrative()`
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
- - `getValue()`/`setValue()` for tracked state; `getArgs()` for frozen input
78
- - Don't use deprecated `CombinedNarrativeBuilder`
79
- - `setEnableNarrative()` is all you need
119
+ - `setEnableNarrative()` is all you need for narrative setup