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.
@@ -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