apcore-js 0.1.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 (142) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/.gitmessage +60 -0
  3. package/.pre-commit-config.yaml +28 -0
  4. package/CHANGELOG.md +47 -0
  5. package/CLAUDE.md +68 -0
  6. package/README.md +131 -0
  7. package/apcore-logo.svg +79 -0
  8. package/package.json +37 -0
  9. package/planning/acl-system/overview.md +54 -0
  10. package/planning/acl-system/plan.md +92 -0
  11. package/planning/acl-system/state.json +76 -0
  12. package/planning/acl-system/tasks/acl-core.md +226 -0
  13. package/planning/acl-system/tasks/acl-rule.md +92 -0
  14. package/planning/acl-system/tasks/conditional-rules.md +259 -0
  15. package/planning/acl-system/tasks/pattern-matching.md +152 -0
  16. package/planning/acl-system/tasks/yaml-loading.md +271 -0
  17. package/planning/core-executor/overview.md +53 -0
  18. package/planning/core-executor/plan.md +88 -0
  19. package/planning/core-executor/state.json +76 -0
  20. package/planning/core-executor/tasks/async-support.md +106 -0
  21. package/planning/core-executor/tasks/execution-pipeline.md +113 -0
  22. package/planning/core-executor/tasks/redaction.md +85 -0
  23. package/planning/core-executor/tasks/safety-checks.md +65 -0
  24. package/planning/core-executor/tasks/setup.md +75 -0
  25. package/planning/decorator-bindings/overview.md +62 -0
  26. package/planning/decorator-bindings/plan.md +104 -0
  27. package/planning/decorator-bindings/state.json +87 -0
  28. package/planning/decorator-bindings/tasks/binding-directory.md +79 -0
  29. package/planning/decorator-bindings/tasks/binding-loader.md +148 -0
  30. package/planning/decorator-bindings/tasks/explicit-schemas.md +85 -0
  31. package/planning/decorator-bindings/tasks/function-module.md +127 -0
  32. package/planning/decorator-bindings/tasks/module-factory.md +89 -0
  33. package/planning/decorator-bindings/tasks/schema-modes.md +142 -0
  34. package/planning/middleware-system/overview.md +48 -0
  35. package/planning/middleware-system/plan.md +102 -0
  36. package/planning/middleware-system/state.json +65 -0
  37. package/planning/middleware-system/tasks/adapters.md +170 -0
  38. package/planning/middleware-system/tasks/base.md +115 -0
  39. package/planning/middleware-system/tasks/logging-middleware.md +304 -0
  40. package/planning/middleware-system/tasks/manager.md +313 -0
  41. package/planning/observability/overview.md +53 -0
  42. package/planning/observability/plan.md +119 -0
  43. package/planning/observability/state.json +98 -0
  44. package/planning/observability/tasks/context-logger.md +201 -0
  45. package/planning/observability/tasks/exporters.md +121 -0
  46. package/planning/observability/tasks/metrics-collector.md +162 -0
  47. package/planning/observability/tasks/metrics-middleware.md +141 -0
  48. package/planning/observability/tasks/obs-logging-middleware.md +179 -0
  49. package/planning/observability/tasks/span-model.md +120 -0
  50. package/planning/observability/tasks/tracing-middleware.md +179 -0
  51. package/planning/overview.md +81 -0
  52. package/planning/registry-system/overview.md +57 -0
  53. package/planning/registry-system/plan.md +114 -0
  54. package/planning/registry-system/state.json +109 -0
  55. package/planning/registry-system/tasks/dependencies.md +157 -0
  56. package/planning/registry-system/tasks/entry-point.md +148 -0
  57. package/planning/registry-system/tasks/metadata.md +198 -0
  58. package/planning/registry-system/tasks/registry-core.md +323 -0
  59. package/planning/registry-system/tasks/scanner.md +172 -0
  60. package/planning/registry-system/tasks/schema-export.md +261 -0
  61. package/planning/registry-system/tasks/types.md +124 -0
  62. package/planning/registry-system/tasks/validation.md +177 -0
  63. package/planning/schema-system/overview.md +56 -0
  64. package/planning/schema-system/plan.md +121 -0
  65. package/planning/schema-system/state.json +98 -0
  66. package/planning/schema-system/tasks/exporter.md +153 -0
  67. package/planning/schema-system/tasks/loader.md +106 -0
  68. package/planning/schema-system/tasks/ref-resolver.md +133 -0
  69. package/planning/schema-system/tasks/strict-mode.md +140 -0
  70. package/planning/schema-system/tasks/typebox-generation.md +133 -0
  71. package/planning/schema-system/tasks/types-and-annotations.md +160 -0
  72. package/planning/schema-system/tasks/validator.md +149 -0
  73. package/src/acl.ts +188 -0
  74. package/src/bindings.ts +208 -0
  75. package/src/config.ts +24 -0
  76. package/src/context.ts +75 -0
  77. package/src/decorator.ts +110 -0
  78. package/src/errors.ts +369 -0
  79. package/src/executor.ts +348 -0
  80. package/src/index.ts +81 -0
  81. package/src/middleware/adapters.ts +54 -0
  82. package/src/middleware/base.ts +33 -0
  83. package/src/middleware/index.ts +6 -0
  84. package/src/middleware/logging.ts +103 -0
  85. package/src/middleware/manager.ts +105 -0
  86. package/src/module.ts +41 -0
  87. package/src/observability/context-logger.ts +201 -0
  88. package/src/observability/index.ts +4 -0
  89. package/src/observability/metrics.ts +212 -0
  90. package/src/observability/tracing.ts +187 -0
  91. package/src/registry/dependencies.ts +99 -0
  92. package/src/registry/entry-point.ts +64 -0
  93. package/src/registry/index.ts +8 -0
  94. package/src/registry/metadata.ts +111 -0
  95. package/src/registry/registry.ts +314 -0
  96. package/src/registry/scanner.ts +150 -0
  97. package/src/registry/schema-export.ts +177 -0
  98. package/src/registry/types.ts +32 -0
  99. package/src/registry/validation.ts +38 -0
  100. package/src/schema/annotations.ts +67 -0
  101. package/src/schema/exporter.ts +93 -0
  102. package/src/schema/index.ts +14 -0
  103. package/src/schema/loader.ts +270 -0
  104. package/src/schema/ref-resolver.ts +235 -0
  105. package/src/schema/strict.ts +128 -0
  106. package/src/schema/types.ts +73 -0
  107. package/src/schema/validator.ts +82 -0
  108. package/src/utils/index.ts +1 -0
  109. package/src/utils/pattern.ts +30 -0
  110. package/tests/helpers.ts +30 -0
  111. package/tests/integration/test-acl-safety.test.ts +268 -0
  112. package/tests/integration/test-binding-executor.test.ts +194 -0
  113. package/tests/integration/test-e2e-flow.test.ts +117 -0
  114. package/tests/integration/test-error-propagation.test.ts +259 -0
  115. package/tests/integration/test-middleware-chain.test.ts +120 -0
  116. package/tests/integration/test-observability-integration.test.ts +438 -0
  117. package/tests/observability/test-context-logger.test.ts +123 -0
  118. package/tests/observability/test-metrics.test.ts +89 -0
  119. package/tests/observability/test-tracing.test.ts +131 -0
  120. package/tests/registry/test-dependencies.test.ts +70 -0
  121. package/tests/registry/test-entry-point.test.ts +133 -0
  122. package/tests/registry/test-metadata.test.ts +265 -0
  123. package/tests/registry/test-registry.test.ts +140 -0
  124. package/tests/registry/test-scanner.test.ts +257 -0
  125. package/tests/registry/test-schema-export.test.ts +224 -0
  126. package/tests/registry/test-validation.test.ts +75 -0
  127. package/tests/schema/test-loader.test.ts +97 -0
  128. package/tests/schema/test-ref-resolver.test.ts +105 -0
  129. package/tests/schema/test-strict.test.ts +139 -0
  130. package/tests/schema/test-validator.test.ts +64 -0
  131. package/tests/test-acl.test.ts +206 -0
  132. package/tests/test-bindings.test.ts +227 -0
  133. package/tests/test-config.test.ts +76 -0
  134. package/tests/test-context.test.ts +151 -0
  135. package/tests/test-decorator.test.ts +173 -0
  136. package/tests/test-errors.test.ts +204 -0
  137. package/tests/test-executor.test.ts +252 -0
  138. package/tests/test-middleware-manager.test.ts +185 -0
  139. package/tests/test-middleware.test.ts +86 -0
  140. package/tsconfig.build.json +8 -0
  141. package/tsconfig.json +20 -0
  142. package/vitest.config.ts +18 -0
@@ -0,0 +1,152 @@
1
+ # Task: matchPattern() Wildcard Matching (Algorithm A08)
2
+
3
+ ## Goal
4
+
5
+ Implement the `matchPattern()` utility function that performs wildcard glob-style pattern matching against module IDs. The algorithm (designated A08) splits patterns on `*` delimiters and verifies each literal segment appears in order within the target string, supporting prefix, suffix, infix, and multi-wildcard patterns.
6
+
7
+ ## Files Involved
8
+
9
+ - `src/utils/pattern.ts` -- Standalone utility function (~30 lines)
10
+
11
+ ## Steps (TDD)
12
+
13
+ ### 1. Write failing tests for all pattern matching cases
14
+
15
+ ```typescript
16
+ // tests/utils/pattern.test.ts
17
+ import { describe, it, expect } from 'vitest';
18
+ import { matchPattern } from '../../src/utils/pattern.js';
19
+
20
+ describe('matchPattern (Algorithm A08)', () => {
21
+ describe('exact matching', () => {
22
+ it('should match identical strings', () => {
23
+ expect(matchPattern('moduleA', 'moduleA')).toBe(true);
24
+ });
25
+
26
+ it('should reject non-matching strings', () => {
27
+ expect(matchPattern('moduleA', 'moduleB')).toBe(false);
28
+ });
29
+ });
30
+
31
+ describe('bare wildcard', () => {
32
+ it('should match any string with bare *', () => {
33
+ expect(matchPattern('*', 'anything')).toBe(true);
34
+ });
35
+
36
+ it('should match empty string with bare *', () => {
37
+ expect(matchPattern('*', '')).toBe(true);
38
+ });
39
+ });
40
+
41
+ describe('prefix wildcard', () => {
42
+ it('should match suffix with *suffix pattern', () => {
43
+ expect(matchPattern('*.handler', 'auth.handler')).toBe(true);
44
+ });
45
+
46
+ it('should reject non-matching suffix', () => {
47
+ expect(matchPattern('*.handler', 'auth.controller')).toBe(false);
48
+ });
49
+ });
50
+
51
+ describe('suffix wildcard', () => {
52
+ it('should match prefix with prefix* pattern', () => {
53
+ expect(matchPattern('auth.*', 'auth.login')).toBe(true);
54
+ });
55
+
56
+ it('should reject non-matching prefix', () => {
57
+ expect(matchPattern('auth.*', 'user.login')).toBe(false);
58
+ });
59
+ });
60
+
61
+ describe('infix wildcard', () => {
62
+ it('should match with pattern containing middle wildcard', () => {
63
+ expect(matchPattern('auth.*.handler', 'auth.login.handler')).toBe(true);
64
+ });
65
+
66
+ it('should reject when middle segment is absent', () => {
67
+ expect(matchPattern('auth.*.handler', 'user.login.handler')).toBe(false);
68
+ });
69
+ });
70
+
71
+ describe('multi-wildcard', () => {
72
+ it('should match with multiple wildcards', () => {
73
+ expect(matchPattern('a*b*c', 'aXXbYYc')).toBe(true);
74
+ });
75
+
76
+ it('should handle adjacent wildcards', () => {
77
+ expect(matchPattern('a**b', 'aXb')).toBe(true);
78
+ });
79
+ });
80
+
81
+ describe('edge cases', () => {
82
+ it('should handle pattern longer than moduleId', () => {
83
+ expect(matchPattern('very.long.pattern', 'short')).toBe(false);
84
+ });
85
+
86
+ it('should handle empty pattern with non-empty moduleId', () => {
87
+ expect(matchPattern('', '')).toBe(true);
88
+ expect(matchPattern('', 'notempty')).toBe(false);
89
+ });
90
+ });
91
+ });
92
+ ```
93
+
94
+ ### 2. Implement matchPattern with segment-based algorithm
95
+
96
+ ```typescript
97
+ // src/utils/pattern.ts
98
+ export function matchPattern(pattern: string, moduleId: string): boolean {
99
+ if (pattern === '*') return true;
100
+ if (!pattern.includes('*')) return pattern === moduleId;
101
+
102
+ const segments = pattern.split('*');
103
+ let pos = 0;
104
+
105
+ // Check prefix (first segment before first *)
106
+ if (!pattern.startsWith('*')) {
107
+ if (!moduleId.startsWith(segments[0])) return false;
108
+ pos = segments[0].length;
109
+ }
110
+
111
+ // Check each intermediate segment appears in order
112
+ for (let i = 1; i < segments.length; i++) {
113
+ const segment = segments[i];
114
+ if (!segment) continue; // empty segment from adjacent ** or trailing *
115
+ const idx = moduleId.indexOf(segment, pos);
116
+ if (idx === -1) return false;
117
+ pos = idx + segment.length;
118
+ }
119
+
120
+ // Check suffix (last segment after last *)
121
+ if (!pattern.endsWith('*')) {
122
+ if (!moduleId.endsWith(segments[segments.length - 1])) return false;
123
+ }
124
+
125
+ return true;
126
+ }
127
+ ```
128
+
129
+ ### 3. Run tests and verify
130
+
131
+ Run `vitest` to confirm all pattern matching tests pass. Run `tsc --noEmit` to verify type safety.
132
+
133
+ ## Acceptance Criteria
134
+
135
+ - [x] `matchPattern()` is exported from `src/utils/pattern.ts`
136
+ - [x] Bare `*` matches any string (including empty)
137
+ - [x] No `*` in pattern means exact string match
138
+ - [x] Prefix wildcard (`*.suffix`) matches strings ending with the suffix
139
+ - [x] Suffix wildcard (`prefix.*`) matches strings starting with the prefix
140
+ - [x] Infix wildcard (`prefix.*.suffix`) matches strings with the prefix and suffix surrounding any middle content
141
+ - [x] Multiple wildcards (`a*b*c`) verified by sequential segment search
142
+ - [x] Adjacent wildcards (`a**b`) handled correctly (empty segments are skipped)
143
+ - [x] Algorithm runs in O(n * m) worst case where n = moduleId length, m = number of segments
144
+ - [x] All tests pass with `vitest`; zero errors from `tsc --noEmit`
145
+
146
+ ## Dependencies
147
+
148
+ - None (standalone utility with no imports)
149
+
150
+ ## Estimated Time
151
+
152
+ 2 hours
@@ -0,0 +1,271 @@
1
+ # Task: ACL.load() from YAML and reload() Support
2
+
3
+ ## Goal
4
+
5
+ Implement the static `ACL.load()` factory method that parses YAML configuration files into a validated `ACL` instance, and the `reload()` instance method that re-reads the original YAML file to hot-swap rules without reconstructing the ACL object.
6
+
7
+ ## Files Involved
8
+
9
+ - `src/acl.ts` -- `ACL.load()` static method and `reload()` instance method
10
+ - `src/errors.ts` -- `ACLRuleError` (invalid YAML/structure), `ConfigNotFoundError` (missing file)
11
+
12
+ ## Steps (TDD)
13
+
14
+ ### 1. Write failing tests for ACL.load() valid YAML
15
+
16
+ ```typescript
17
+ // tests/acl.test.ts
18
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
19
+ import { writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
20
+ import { ACL } from '../src/acl.js';
21
+ import { ACLRuleError, ConfigNotFoundError } from '../src/errors.js';
22
+
23
+ describe('ACL.load()', () => {
24
+ const tmpDir = '/tmp/acl-test';
25
+ const yamlPath = `${tmpDir}/acl.yaml`;
26
+
27
+ beforeEach(() => {
28
+ mkdirSync(tmpDir, { recursive: true });
29
+ });
30
+
31
+ afterEach(() => {
32
+ try { unlinkSync(yamlPath); } catch {}
33
+ });
34
+
35
+ it('should load valid YAML with rules and default_effect', () => {
36
+ writeFileSync(yamlPath, `
37
+ default_effect: allow
38
+ rules:
39
+ - callers: ["moduleA"]
40
+ targets: ["moduleB"]
41
+ effect: allow
42
+ description: "Allow A to B"
43
+ `);
44
+ const acl = ACL.load(yamlPath);
45
+ expect(acl.check('moduleA', 'moduleB')).toBe(true);
46
+ expect(acl.check('moduleX', 'moduleY')).toBe(true); // default allow
47
+ });
48
+
49
+ it('should default to deny when default_effect is not specified', () => {
50
+ writeFileSync(yamlPath, `
51
+ rules:
52
+ - callers: ["moduleA"]
53
+ targets: ["moduleB"]
54
+ effect: allow
55
+ description: "test"
56
+ `);
57
+ const acl = ACL.load(yamlPath);
58
+ expect(acl.check('moduleX', 'moduleY')).toBe(false);
59
+ });
60
+
61
+ it('should load rules with conditions', () => {
62
+ writeFileSync(yamlPath, `
63
+ rules:
64
+ - callers: ["*"]
65
+ targets: ["admin.*"]
66
+ effect: allow
67
+ description: "Admin access with role check"
68
+ conditions:
69
+ roles: ["admin"]
70
+ `);
71
+ const acl = ACL.load(yamlPath);
72
+ // Without context, conditions fail
73
+ expect(acl.check('moduleA', 'admin.panel')).toBe(false);
74
+ });
75
+ });
76
+ ```
77
+
78
+ ### 2. Write failing tests for ACL.load() error cases
79
+
80
+ ```typescript
81
+ describe('ACL.load() error handling', () => {
82
+ it('should throw ConfigNotFoundError for missing file', () => {
83
+ expect(() => ACL.load('/nonexistent/acl.yaml')).toThrow(ConfigNotFoundError);
84
+ });
85
+
86
+ it('should throw ACLRuleError for invalid YAML', () => {
87
+ writeFileSync(yamlPath, '{ invalid yaml :::');
88
+ expect(() => ACL.load(yamlPath)).toThrow(ACLRuleError);
89
+ });
90
+
91
+ it('should throw ACLRuleError when rules key is missing', () => {
92
+ writeFileSync(yamlPath, 'default_effect: allow\n');
93
+ expect(() => ACL.load(yamlPath)).toThrow(ACLRuleError);
94
+ });
95
+
96
+ it('should throw ACLRuleError for invalid effect value', () => {
97
+ writeFileSync(yamlPath, `
98
+ rules:
99
+ - callers: ["*"]
100
+ targets: ["*"]
101
+ effect: maybe
102
+ description: "bad effect"
103
+ `);
104
+ expect(() => ACL.load(yamlPath)).toThrow(ACLRuleError);
105
+ });
106
+
107
+ it('should throw ACLRuleError when rule is not a mapping', () => {
108
+ writeFileSync(yamlPath, `
109
+ rules:
110
+ - "not a mapping"
111
+ `);
112
+ expect(() => ACL.load(yamlPath)).toThrow(ACLRuleError);
113
+ });
114
+
115
+ it('should throw ACLRuleError when rule is missing required keys', () => {
116
+ writeFileSync(yamlPath, `
117
+ rules:
118
+ - callers: ["*"]
119
+ description: "missing targets and effect"
120
+ `);
121
+ expect(() => ACL.load(yamlPath)).toThrow(ACLRuleError);
122
+ });
123
+ });
124
+ ```
125
+
126
+ ### 3. Implement ACL.load() static method
127
+
128
+ ```typescript
129
+ static load(yamlPath: string): ACL {
130
+ if (!existsSync(yamlPath)) {
131
+ throw new ConfigNotFoundError(yamlPath);
132
+ }
133
+
134
+ let data: unknown;
135
+ try {
136
+ const content = readFileSync(yamlPath, 'utf-8');
137
+ data = yaml.load(content);
138
+ } catch (e) {
139
+ if (e instanceof ConfigNotFoundError) throw e;
140
+ throw new ACLRuleError(`Invalid YAML in ${yamlPath}: ${e}`);
141
+ }
142
+
143
+ // Validate top-level structure
144
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
145
+ throw new ACLRuleError(`ACL config must be a mapping, got ${typeof data}`);
146
+ }
147
+
148
+ const dataObj = data as Record<string, unknown>;
149
+ if (!('rules' in dataObj)) {
150
+ throw new ACLRuleError("ACL config missing required 'rules' key");
151
+ }
152
+
153
+ const rawRules = dataObj['rules'];
154
+ if (!Array.isArray(rawRules)) {
155
+ throw new ACLRuleError(`'rules' must be a list, got ${typeof rawRules}`);
156
+ }
157
+
158
+ const defaultEffect = (dataObj['default_effect'] as string) ?? 'deny';
159
+ const rules: ACLRule[] = [];
160
+
161
+ for (let i = 0; i < rawRules.length; i++) {
162
+ const rawRule = rawRules[i];
163
+ // Validate each rule is a mapping with required keys
164
+ if (typeof rawRule !== 'object' || rawRule === null || Array.isArray(rawRule)) {
165
+ throw new ACLRuleError(`Rule ${i} must be a mapping, got ${typeof rawRule}`);
166
+ }
167
+
168
+ const ruleObj = rawRule as Record<string, unknown>;
169
+ for (const key of ['callers', 'targets', 'effect']) {
170
+ if (!(key in ruleObj)) {
171
+ throw new ACLRuleError(`Rule ${i} missing required key '${key}'`);
172
+ }
173
+ }
174
+
175
+ const effect = ruleObj['effect'] as string;
176
+ if (effect !== 'allow' && effect !== 'deny') {
177
+ throw new ACLRuleError(`Rule ${i} has invalid effect '${effect}', must be 'allow' or 'deny'`);
178
+ }
179
+
180
+ rules.push({
181
+ callers: ruleObj['callers'] as string[],
182
+ targets: ruleObj['targets'] as string[],
183
+ effect,
184
+ description: (ruleObj['description'] as string) ?? '',
185
+ conditions: (ruleObj['conditions'] as Record<string, unknown>) ?? null,
186
+ });
187
+ }
188
+
189
+ const acl = new ACL(rules, defaultEffect);
190
+ acl._yamlPath = yamlPath;
191
+ return acl;
192
+ }
193
+ ```
194
+
195
+ ### 4. Write failing tests for reload()
196
+
197
+ ```typescript
198
+ describe('ACL.reload()', () => {
199
+ it('should reload rules from the original YAML path', () => {
200
+ writeFileSync(yamlPath, `
201
+ rules:
202
+ - callers: ["moduleA"]
203
+ targets: ["moduleB"]
204
+ effect: deny
205
+ description: "initial deny"
206
+ `);
207
+ const acl = ACL.load(yamlPath);
208
+ expect(acl.check('moduleA', 'moduleB')).toBe(false);
209
+
210
+ // Update YAML
211
+ writeFileSync(yamlPath, `
212
+ rules:
213
+ - callers: ["moduleA"]
214
+ targets: ["moduleB"]
215
+ effect: allow
216
+ description: "updated allow"
217
+ `);
218
+ acl.reload();
219
+ expect(acl.check('moduleA', 'moduleB')).toBe(true);
220
+ });
221
+
222
+ it('should throw ACLRuleError when not loaded from YAML', () => {
223
+ const acl = new ACL([]);
224
+ expect(() => acl.reload()).toThrow(ACLRuleError);
225
+ expect(() => acl.reload()).toThrow('Cannot reload: ACL was not loaded from a YAML file');
226
+ });
227
+ });
228
+ ```
229
+
230
+ ### 5. Implement reload() method
231
+
232
+ ```typescript
233
+ reload(): void {
234
+ if (this._yamlPath === null) {
235
+ throw new ACLRuleError('Cannot reload: ACL was not loaded from a YAML file');
236
+ }
237
+ const reloaded = ACL.load(this._yamlPath);
238
+ this._rules = reloaded._rules;
239
+ this._defaultEffect = reloaded._defaultEffect;
240
+ }
241
+ ```
242
+
243
+ ### 6. Run full test suite and type-check
244
+
245
+ Run `tsc --noEmit` and `vitest` to confirm everything passes.
246
+
247
+ ## Acceptance Criteria
248
+
249
+ - [x] `ACL.load()` reads YAML via `readFileSync` and parses with `js-yaml`
250
+ - [x] Throws `ConfigNotFoundError` when the YAML file does not exist
251
+ - [x] Throws `ACLRuleError` for invalid YAML syntax
252
+ - [x] Throws `ACLRuleError` when top-level structure is not a mapping
253
+ - [x] Throws `ACLRuleError` when `rules` key is missing
254
+ - [x] Throws `ACLRuleError` when `rules` is not an array
255
+ - [x] Validates each rule has `callers`, `targets`, and `effect` keys
256
+ - [x] Validates `effect` is either `'allow'` or `'deny'`
257
+ - [x] Validates `callers` and `targets` are arrays
258
+ - [x] Defaults `description` to empty string and `conditions` to `null`
259
+ - [x] Defaults `default_effect` to `'deny'` when not specified in YAML
260
+ - [x] Stores `_yamlPath` on the ACL instance for `reload()` support
261
+ - [x] `reload()` re-reads the YAML and replaces `_rules` and `_defaultEffect` in-place
262
+ - [x] `reload()` throws `ACLRuleError` when the ACL was not loaded from YAML
263
+ - [x] All tests pass with `vitest`; zero errors from `tsc --noEmit`
264
+
265
+ ## Dependencies
266
+
267
+ - **acl-core** -- ACL class constructor and rule storage
268
+
269
+ ## Estimated Time
270
+
271
+ 2 hours
@@ -0,0 +1,53 @@
1
+ # Feature: Core Executor
2
+
3
+ ## Overview
4
+
5
+ The Core Execution Engine is the central orchestration component of apcore. It processes module calls through a structured 10-step pipeline: context creation, safety checks (call depth, circular detection, frequency throttling), module lookup from the registry, ACL enforcement, input validation with sensitive field redaction, middleware before chain, module execution with timeout enforcement, output validation, middleware after chain, and result return. The TypeScript implementation uses a single async `call()` method with `Promise.race` for timeout enforcement.
6
+
7
+ ## Scope
8
+
9
+ ### Included
10
+
11
+ - `Context` class and `Identity` interface for call metadata propagation and caller identity
12
+ - `Config` accessor with dot-path key support for executor settings
13
+ - `Executor` class implementing the full 10-step async pipeline
14
+ - Safety checks: call depth limits, circular call detection (cycles of length >= 2), frequency throttling
15
+ - Timeout enforcement via `Promise.race` with `setTimeout`
16
+ - `redactSensitive` utility for masking `x-sensitive` fields and `_secret_`-prefixed keys
17
+ - Structured error hierarchy (`ModuleError` base with specialized subclasses for every failure mode)
18
+ - Standalone `validate()` method for pre-flight schema checks without execution
19
+ - Middleware management via `MiddlewareManager` with before/after/onError chains
20
+
21
+ ### Excluded
22
+
23
+ - Registry implementation (consumed as a dependency)
24
+ - Schema system internals (consumed via TypeBox validation)
25
+ - ACL rule definition and management (consumed via `ACL.check()` interface)
26
+ - Middleware implementation details (managed by `MiddlewareManager`)
27
+
28
+ ## Technology Stack
29
+
30
+ - **TypeScript 5.5+** with strict mode
31
+ - **@sinclair/typebox >= 0.34.0** for input/output schema validation
32
+ - **Node.js >= 18.0.0** with ES Module support
33
+ - **vitest** for unit and integration testing
34
+
35
+ ## Task Execution Order
36
+
37
+ | # | Task File | Description | Status |
38
+ |---|-----------|-------------|--------|
39
+ | 1 | [setup](./tasks/setup.md) | Context, Identity, and Config classes | completed |
40
+ | 2 | [safety-checks](./tasks/safety-checks.md) | Call depth limits, circular detection, frequency throttling | completed |
41
+ | 3 | [execution-pipeline](./tasks/execution-pipeline.md) | 10-step async execution pipeline with middleware and timeout | completed |
42
+ | 4 | [async-support](./tasks/async-support.md) | Unified async execution (single async call()) | completed |
43
+ | 5 | [redaction](./tasks/redaction.md) | Sensitive field redaction utility (`redactSensitive`) | completed |
44
+
45
+ ## Progress
46
+
47
+ | Total | Completed | In Progress | Pending |
48
+ |-------|-----------|-------------|---------|
49
+ | 5 | 5 | 0 | 0 |
50
+
51
+ ## Reference Documents
52
+
53
+ - [Core Executor Feature Specification](../../features/core-executor.md)
@@ -0,0 +1,88 @@
1
+ # Implementation Plan: Core Executor
2
+
3
+ ## Goal
4
+
5
+ Implement the central 10-step execution pipeline that orchestrates all module calls in apcore, supporting context propagation, safety constraints, access control, schema validation with sensitive field redaction, middleware chains, and timeout-enforced module execution through a unified async code path.
6
+
7
+ ## Architecture Design
8
+
9
+ ### Component Structure
10
+
11
+ - **Executor** (`executor.ts`, ~300 lines) -- Main engine class implementing the 10-step pipeline. Accepts options object with `registry`, optional `middlewares` array, optional `acl`, and optional `config` at construction. Manages middleware registration via `MiddlewareManager`, configurable timeouts (default 30s, global 60s), max call depth (32), and max module repeat (3). Exposes `call()` (async) and `validate()` (pre-flight) entry points.
12
+
13
+ - **Context** (`context.ts`, ~60 lines) -- Class carrying per-call metadata: `traceId` (UUID v4 via `crypto.randomUUID()`), `callerId`, `callChain` (array of module IDs visited), `executor` reference, `identity`, `redactedInputs`, and a shared `data` record. Factory methods `create()` for root contexts and `child()` for nested calls. The `data` record is intentionally shared (not copied) between parent and child contexts to support middleware span/timing stacks.
14
+
15
+ - **Identity** (`context.ts`) -- Frozen interface representing the caller: `id`, `type` (default "user"), `roles` array, and `attrs` record. Created via `createIdentity()` factory with `Object.freeze()`.
16
+
17
+ - **Config** (`config.ts`, ~20 lines) -- Configuration accessor with dot-path key support (e.g., `executor.default_timeout`). Wraps a nested record and navigates through path segments.
18
+
19
+ - **Error Hierarchy** (`errors.ts`, ~280 lines) -- `ModuleError` base class with `timestamp`, `code`, `message`, `details` record, and `cause`. Specialized subclasses: `CallDepthExceededError`, `CircularCallError`, `CallFrequencyExceededError`, `ModuleNotFoundError`, `ACLDeniedError`, `SchemaValidationError`, `ModuleTimeoutError`, `InvalidInputError`.
20
+
21
+ ### Data Flow
22
+
23
+ The 10-step pipeline processes every module call in this order:
24
+
25
+ 1. **Context Creation** -- Build or derive `Context` via `create()` / `child()`
26
+ 2. **Safety Checks** -- Call depth limit, circular detection (cycles >= 2), frequency throttling
27
+ 3. **Module Lookup** -- `Registry.get()` with `ModuleNotFoundError` on miss
28
+ 4. **ACL Enforcement** -- `ACL.check()` with `ACLDeniedError` on denial
29
+ 5. **Input Validation** -- TypeBox `Value.Check()` + `redactSensitive()` for context logging
30
+ 6. **Middleware Before** -- `MiddlewareManager.executeBefore()` with `MiddlewareChainError` handling
31
+ 7. **Module Execution** -- Timeout via `Promise.race` with `setTimeout`
32
+ 8. **Output Validation** -- TypeBox `Value.Check()` on return value
33
+ 9. **Middleware After** -- `MiddlewareManager.executeAfter()` in reverse order
34
+ 10. **Result Return** -- Return output record or propagate error with `onError` recovery
35
+
36
+ ### Technical Choices and Rationale
37
+
38
+ - **`Promise.race` for timeout**: Simple and idiomatic for Node.js async timeout enforcement. The timeout promise rejects with `ModuleTimeoutError` if the execution exceeds the limit.
39
+ - **Unified async `call()`**: TypeScript's async-first model eliminates the need for separate sync/async code paths. All module executions go through `Promise.resolve()` which handles both sync and async return values transparently.
40
+ - **Shared `data` record in child contexts**: Enables middleware like tracing and metrics to maintain span stacks across nested module-to-module calls without additional plumbing.
41
+ - **`JSON.parse(JSON.stringify())` in `redactSensitive`**: Simple deep clone for JSON-compatible input data. Suitable since module inputs are expected to be JSON-serializable.
42
+
43
+ ## Task Breakdown
44
+
45
+ ```mermaid
46
+ graph TD
47
+ T1[setup] --> T2[safety-checks]
48
+ T1 --> T3[execution-pipeline]
49
+ T2 --> T3
50
+ T3 --> T4[async-support]
51
+ T1 --> T5[redaction]
52
+ T5 --> T3
53
+ ```
54
+
55
+ | Task ID | Title | Estimated Time | Dependencies |
56
+ |---------|-------|---------------|--------------|
57
+ | setup | Context, Identity, and Config classes | 2h | none |
58
+ | safety-checks | Call depth, circular detection, frequency throttling | 3h | setup |
59
+ | execution-pipeline | 10-step async pipeline with middleware and timeout | 6h | setup, safety-checks, redaction |
60
+ | async-support | Unified async execution and Promise-based bridge | 4h | execution-pipeline |
61
+ | redaction | Sensitive field redaction utility | 2h | setup |
62
+
63
+ ## Risks and Considerations
64
+
65
+ - **`setTimeout` timer leak**: The timeout timer is not cleaned up if execution completes before timeout. Harmless since the timer fires into a resolved promise, but wasteful.
66
+ - **Middleware error recovery**: If a middleware `before()` fails, previously executed middlewares must have their `onError()` called. The `MiddlewareChainError` wrapping captures which middlewares have been executed.
67
+ - **Circular detection strictness**: Only cycles of length >= 2 are detected (A calling B calling A), not self-recursion (A calling A), which is covered by frequency throttling.
68
+
69
+ ## Acceptance Criteria
70
+
71
+ - [ ] All 10 pipeline steps are implemented and tested in isolation and end-to-end
72
+ - [ ] Timeout enforcement works via `Promise.race` with `setTimeout`
73
+ - [ ] Timeout of 0 disables enforcement with a logged warning
74
+ - [ ] Negative timeout raises `InvalidInputError`
75
+ - [ ] Safety checks correctly detect call depth violations, circular calls, and frequency throttling
76
+ - [ ] `redactSensitive` handles nested objects, arrays with `x-sensitive` items, and `_secret_`-prefixed keys
77
+ - [ ] Error recovery via `onError` returns recovery record or re-raises original exception
78
+ - [ ] `validate()` provides standalone pre-flight schema checks without execution
79
+ - [ ] All error types carry structured details with timestamps
80
+ - [ ] All tests pass with `vitest`; zero errors from `tsc --noEmit`
81
+
82
+ ## References
83
+
84
+ - `src/executor.ts` -- Core execution engine
85
+ - `src/context.ts` -- Context and Identity classes
86
+ - `src/config.ts` -- Configuration accessor
87
+ - `src/errors.ts` -- Error hierarchy
88
+ - [Core Executor Feature Specification](../../features/core-executor.md)
@@ -0,0 +1,76 @@
1
+ {
2
+ "feature": "core-executor",
3
+ "created": "2026-02-16T00:00:00Z",
4
+ "updated": "2026-02-16T00:00:00Z",
5
+ "status": "completed",
6
+ "execution_order": [
7
+ "setup",
8
+ "safety-checks",
9
+ "execution-pipeline",
10
+ "async-support",
11
+ "redaction"
12
+ ],
13
+ "progress": {
14
+ "total_tasks": 5,
15
+ "completed": 5,
16
+ "in_progress": 0,
17
+ "pending": 0
18
+ },
19
+ "tasks": [
20
+ {
21
+ "id": "setup",
22
+ "file": "tasks/setup.md",
23
+ "title": "Context, Identity, and Config Classes",
24
+ "status": "completed",
25
+ "started_at": "2026-02-16T08:00:00Z",
26
+ "completed_at": "2026-02-16T09:30:00Z",
27
+ "assignee": null,
28
+ "commits": []
29
+ },
30
+ {
31
+ "id": "safety-checks",
32
+ "file": "tasks/safety-checks.md",
33
+ "title": "Call Depth Limits, Circular Detection, Frequency Throttling",
34
+ "status": "completed",
35
+ "started_at": "2026-02-16T09:30:00Z",
36
+ "completed_at": "2026-02-16T11:00:00Z",
37
+ "assignee": null,
38
+ "commits": []
39
+ },
40
+ {
41
+ "id": "execution-pipeline",
42
+ "file": "tasks/execution-pipeline.md",
43
+ "title": "10-Step Async Execution Pipeline",
44
+ "status": "completed",
45
+ "started_at": "2026-02-16T11:00:00Z",
46
+ "completed_at": "2026-02-16T15:00:00Z",
47
+ "assignee": null,
48
+ "commits": []
49
+ },
50
+ {
51
+ "id": "async-support",
52
+ "file": "tasks/async-support.md",
53
+ "title": "Unified Async Execution Path",
54
+ "status": "completed",
55
+ "started_at": "2026-02-16T15:00:00Z",
56
+ "completed_at": "2026-02-16T18:00:00Z",
57
+ "assignee": null,
58
+ "commits": []
59
+ },
60
+ {
61
+ "id": "redaction",
62
+ "file": "tasks/redaction.md",
63
+ "title": "Sensitive Field Redaction Utility",
64
+ "status": "completed",
65
+ "started_at": "2026-02-16T18:00:00Z",
66
+ "completed_at": "2026-02-16T19:00:00Z",
67
+ "assignee": null,
68
+ "commits": []
69
+ }
70
+ ],
71
+ "metadata": {
72
+ "source_doc": "planning/features/core-executor.md",
73
+ "created_by": "code-forge",
74
+ "version": "1.0"
75
+ }
76
+ }