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
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
# footprint.js — Copilot Instructions
|
|
2
2
|
|
|
3
|
-
This is the footprint.js library — the flowchart pattern for backend code.
|
|
4
|
-
|
|
5
|
-
## What It Does
|
|
6
|
-
|
|
7
|
-
Structures backend logic as a graph of named functions with transactional state. Every run auto-generates a causal trace of what happened and why. LLMs read the trace for grounded explanations — no hallucination.
|
|
3
|
+
This is the footprint.js library — the flowchart pattern for backend code. Self-explainable systems that AI can reason about.
|
|
8
4
|
|
|
9
5
|
## Core Principle
|
|
10
6
|
|
|
@@ -16,8 +12,10 @@ Structures backend logic as a graph of named functions with transactional state.
|
|
|
16
12
|
src/lib/
|
|
17
13
|
├── memory/ → Transactional state (SharedMemory, StageContext, TransactionBuffer)
|
|
18
14
|
├── schema/ → Validation (Zod optional, duck-typed)
|
|
19
|
-
├── builder/ → Fluent DSL (FlowChartBuilder, flowChart())
|
|
15
|
+
├── builder/ → Fluent DSL (FlowChartBuilder, flowChart(), typedFlowChart())
|
|
20
16
|
├── scope/ → Per-stage facades + recorders + providers
|
|
17
|
+
├── reactive/ → TypedScope<T> deep Proxy (typed property access, $-methods)
|
|
18
|
+
├── decide/ → decide()/select() decision evidence capture
|
|
21
19
|
├── engine/ → DFS traversal + narrative + handlers
|
|
22
20
|
├── runner/ → FlowChartExecutor
|
|
23
21
|
└── contract/ → I/O schema + OpenAPI
|
|
@@ -25,49 +23,82 @@ src/lib/
|
|
|
25
23
|
|
|
26
24
|
Entry points: `footprintjs` (public) and `footprintjs/advanced` (internals).
|
|
27
25
|
|
|
28
|
-
## Key API
|
|
29
|
-
|
|
30
|
-
### Builder Chain
|
|
26
|
+
## Key API — TypedScope (Recommended)
|
|
31
27
|
|
|
32
28
|
```typescript
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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')
|
|
41
41
|
.setEnableNarrative()
|
|
42
|
-
.
|
|
43
|
-
|
|
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
|
|
44
60
|
```
|
|
45
61
|
|
|
46
|
-
###
|
|
62
|
+
### TypedScope $-methods (escape hatches)
|
|
47
63
|
|
|
48
64
|
```typescript
|
|
49
|
-
|
|
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
|
|
50
70
|
```
|
|
51
71
|
|
|
52
|
-
###
|
|
72
|
+
### decide() / select()
|
|
53
73
|
|
|
54
74
|
```typescript
|
|
55
|
-
|
|
56
|
-
scope
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
]);
|
|
60
90
|
```
|
|
61
91
|
|
|
62
92
|
### Executor
|
|
63
93
|
|
|
64
94
|
```typescript
|
|
65
|
-
const executor = new FlowChartExecutor(chart);
|
|
66
|
-
await executor.run({ input,
|
|
95
|
+
const executor = new FlowChartExecutor(chart, createTypedScopeFactory<State>());
|
|
96
|
+
await executor.run({ input, env: { traceId: 'req-123' } });
|
|
67
97
|
executor.getNarrative() // string[]
|
|
68
98
|
executor.getNarrativeEntries() // CombinedNarrativeEntry[]
|
|
69
99
|
executor.getSnapshot() // memory state
|
|
70
|
-
executor.
|
|
100
|
+
executor.attachRecorder(recorder) // scope observer
|
|
101
|
+
executor.attachFlowRecorder(r) // flow observer
|
|
71
102
|
executor.setRedactionPolicy({ keys, patterns, fields })
|
|
72
103
|
```
|
|
73
104
|
|
|
@@ -75,12 +106,14 @@ executor.setRedactionPolicy({ keys, patterns, fields })
|
|
|
75
106
|
|
|
76
107
|
- **Scope Recorder**: fires DURING stage (`onRead`, `onWrite`, `onCommit`)
|
|
77
108
|
- **FlowRecorder**: fires AFTER stage (`onStageExecuted`, `onDecision`, `onFork`, `onLoop`)
|
|
78
|
-
- 8 built-in FlowRecorder strategies
|
|
79
|
-
- `setEnableNarrative()` auto-attaches `CombinedNarrativeRecorder`
|
|
109
|
+
- 8 built-in FlowRecorder strategies
|
|
110
|
+
- `setEnableNarrative()` auto-attaches `CombinedNarrativeRecorder`
|
|
80
111
|
|
|
81
112
|
## Rules
|
|
82
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
|
|
83
118
|
- Never post-process the tree — use recorders
|
|
84
|
-
- `getValue()`/`setValue()` for tracked state; `getArgs()` for frozen readonly input
|
|
85
|
-
- Don't use deprecated `CombinedNarrativeBuilder`
|
|
86
119
|
- `setEnableNarrative()` is all you need for narrative setup
|
|
@@ -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,60 +23,97 @@ 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
|
|
|
67
|
-
##
|
|
105
|
+
## Observer Systems
|
|
68
106
|
|
|
69
|
-
- **Scope Recorder**: `onRead`, `onWrite`, `onCommit
|
|
70
|
-
- **FlowRecorder**: `onStageExecuted`, `onDecision`, `onFork`, `
|
|
71
|
-
- 8
|
|
72
|
-
- `
|
|
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`
|
|
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` — use `CombinedNarrativeRecorder`
|
|
79
|
-
- Don't extract shared base for Recorder/FlowRecorder — coincidence, not pattern
|
|
80
|
-
- `setEnableNarrative()` is all you need
|
|
119
|
+
- `setEnableNarrative()` is all you need for narrative setup
|
|
@@ -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
|
|
@@ -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 built-in strategies
|
|
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 for narrative
|
|
119
|
+
- `setEnableNarrative()` is all you need for narrative setup
|