Reference implementation for the Phoenix Architecture. Work in progress. aicoding.leaflet.pub/
ai coding crazy

fix: write shared files before codegen so typecheck resolves imports

Root cause: the typecheck-retry loop couldn't resolve ../../db.js
because shared files were written by scaffold AFTER code generation.
The LLM would "fix" the import error by creating its own Database.

Now: shared files + package.json + npm install happen BEFORE codegen.
Also added mandatory import block at top of user prompt and multi-
resource code example with JOINs, filtering, cascade protection.

Imports now correct (db from ../../db.js). Score 32% on hard spec —
remaining failures are logic issues (JOINs, stats, filtering), not
import issues. Ready for autoresearch prompt optimization.

+338 -9
+43
experiments/config.d.ts
··· 1 + /** 2 + * Experiment Configuration — Single source of truth for all tunable parameters. 3 + * 4 + * The AI agent edits ONLY this file during experiment loops. 5 + * Default values match the original hardcoded constants exactly. 6 + */ 7 + export declare const CONFIG: { 8 + MAX_DEGREE: number; 9 + MIN_SHARED_TAGS: number; 10 + JACCARD_DEDUP_THRESHOLD: number; 11 + FINGERPRINT_PREFIX_COUNT: number; 12 + DOC_FREQ_CUTOFF: number; 13 + CONSTRAINT_NEGATION_WEIGHT: number; 14 + CONSTRAINT_LIMIT_WEIGHT: number; 15 + CONSTRAINT_NUMERIC_WEIGHT: number; 16 + INVARIANT_SIGNAL_WEIGHT: number; 17 + REQUIREMENT_MODAL_WEIGHT: number; 18 + REQUIREMENT_KEYWORD_WEIGHT: number; 19 + REQUIREMENT_VERB_WEIGHT: number; 20 + DEFINITION_EXPLICIT_WEIGHT: number; 21 + DEFINITION_COLON_WEIGHT: number; 22 + CONTEXT_NO_MODAL_WEIGHT: number; 23 + CONTEXT_SHORT_WEIGHT: number; 24 + HEADING_CONTEXT_BONUS: number; 25 + CONSTRAINT_MUST_BONUS: number; 26 + MIN_CONFIDENCE: number; 27 + MAX_CONFIDENCE: number; 28 + DEFINITION_MAX_LENGTH: number; 29 + MIN_EXTRACTION_LENGTH: number; 30 + MIN_TERM_LENGTH: number; 31 + MIN_WORD_LENGTH: number; 32 + MIN_LIST_ITEM_LENGTH: number; 33 + MIN_PROSE_SENTENCE_LENGTH: number; 34 + PROSE_SPLIT_THRESHOLD: number; 35 + MIN_SPLIT_PART_LENGTH: number; 36 + WARM_MIN_CONFIDENCE: number; 37 + CLASS_A_NORM_DIFF: number; 38 + CLASS_A_TERM_DELTA: number; 39 + CLASS_B_NORM_DIFF: number; 40 + CLASS_B_TERM_DELTA: number; 41 + CLASS_D_HIGH_CHANGE: number; 42 + ANCHOR_MATCH_THRESHOLD: number; 43 + };
+49
experiments/config.js
··· 1 + /** 2 + * Experiment Configuration — Single source of truth for all tunable parameters. 3 + * 4 + * The AI agent edits ONLY this file during experiment loops. 5 + * Default values match the original hardcoded constants exactly. 6 + */ 7 + export const CONFIG = { 8 + // ─── resolution.ts ──────────────────────────────────────────────────────── 9 + MAX_DEGREE: 8, 10 + MIN_SHARED_TAGS: 2, 11 + JACCARD_DEDUP_THRESHOLD: 0.7, 12 + FINGERPRINT_PREFIX_COUNT: 8, 13 + DOC_FREQ_CUTOFF: 0.4, 14 + // ─── canonicalizer.ts — scoring weights ─────────────────────────────────── 15 + CONSTRAINT_NEGATION_WEIGHT: 4, 16 + CONSTRAINT_LIMIT_WEIGHT: 3, 17 + CONSTRAINT_NUMERIC_WEIGHT: 2, 18 + INVARIANT_SIGNAL_WEIGHT: 4, 19 + REQUIREMENT_MODAL_WEIGHT: 2, 20 + REQUIREMENT_KEYWORD_WEIGHT: 2, 21 + REQUIREMENT_VERB_WEIGHT: 1, 22 + DEFINITION_EXPLICIT_WEIGHT: 4, 23 + DEFINITION_COLON_WEIGHT: 3, 24 + CONTEXT_NO_MODAL_WEIGHT: 2, 25 + CONTEXT_SHORT_WEIGHT: 1, 26 + HEADING_CONTEXT_BONUS: 2, 27 + CONSTRAINT_MUST_BONUS: 1, 28 + MIN_CONFIDENCE: 0.3, 29 + MAX_CONFIDENCE: 1.0, 30 + DEFINITION_MAX_LENGTH: 200, 31 + MIN_EXTRACTION_LENGTH: 5, 32 + MIN_TERM_LENGTH: 3, 33 + MIN_WORD_LENGTH: 2, 34 + // ─── sentence-segmenter.ts ──────────────────────────────────────────────── 35 + MIN_LIST_ITEM_LENGTH: 3, 36 + MIN_PROSE_SENTENCE_LENGTH: 3, 37 + PROSE_SPLIT_THRESHOLD: 80, 38 + MIN_SPLIT_PART_LENGTH: 3, 39 + // ─── warm-hasher.ts ─────────────────────────────────────────────────────── 40 + WARM_MIN_CONFIDENCE: 0.3, 41 + // ─── classifier.ts ──────────────────────────────────────────────────────── 42 + CLASS_A_NORM_DIFF: 0.1, 43 + CLASS_A_TERM_DELTA: 0.2, 44 + CLASS_B_NORM_DIFF: 0.5, 45 + CLASS_B_TERM_DELTA: 0.5, 46 + CLASS_D_HIGH_CHANGE: 0.7, 47 + ANCHOR_MATCH_THRESHOLD: 0.5, 48 + }; 49 + //# sourceMappingURL=config.js.map
+1
experiments/config.js.map
··· 1 + {"version":3,"file":"config.js","sourceRoot":"","sources":["config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,6EAA6E;IAC7E,UAAU,EAAE,CAAC;IACb,eAAe,EAAE,CAAC;IAClB,uBAAuB,EAAE,GAAG;IAC5B,wBAAwB,EAAE,CAAC;IAC3B,eAAe,EAAE,GAAG;IAEpB,6EAA6E;IAC7E,0BAA0B,EAAE,CAAC;IAC7B,uBAAuB,EAAE,CAAC;IAC1B,yBAAyB,EAAE,CAAC;IAC5B,uBAAuB,EAAE,CAAC;IAC1B,wBAAwB,EAAE,CAAC;IAC3B,0BAA0B,EAAE,CAAC;IAC7B,uBAAuB,EAAE,CAAC;IAC1B,0BAA0B,EAAE,CAAC;IAC7B,uBAAuB,EAAE,CAAC;IAC1B,uBAAuB,EAAE,CAAC;IAC1B,oBAAoB,EAAE,CAAC;IACvB,qBAAqB,EAAE,CAAC;IACxB,qBAAqB,EAAE,CAAC;IACxB,cAAc,EAAE,GAAG;IACnB,cAAc,EAAE,GAAG;IACnB,qBAAqB,EAAE,GAAG;IAC1B,qBAAqB,EAAE,CAAC;IACxB,eAAe,EAAE,CAAC;IAClB,eAAe,EAAE,CAAC;IAElB,6EAA6E;IAC7E,oBAAoB,EAAE,CAAC;IACvB,yBAAyB,EAAE,CAAC;IAC5B,qBAAqB,EAAE,EAAE;IACzB,qBAAqB,EAAE,CAAC;IAExB,6EAA6E;IAC7E,mBAAmB,EAAE,GAAG;IAExB,6EAA6E;IAC7E,iBAAiB,EAAE,GAAG;IACtB,kBAAkB,EAAE,GAAG;IACvB,iBAAiB,EAAE,GAAG;IACtB,kBAAkB,EAAE,GAAG;IACvB,mBAAmB,EAAE,GAAG;IACxB,sBAAsB,EAAE,GAAG;CAC5B,CAAC"}
+89
experiments/program-arch.md
··· 1 + # Phoenix Architecture Prompt Optimization — Experiment Program 2 + 3 + You are an autonomous research agent optimizing the architecture target prompts so that Phoenix generates working multi-resource REST APIs from specs. 4 + 5 + ## Rules 6 + 7 + 1. **Edit ONLY `src/architectures/sqlite-web-api.ts`** — the system prompt extension and code examples 8 + 2. **Run `npx tsx experiments/eval-runner-arch.ts --skip-bootstrap`** to test changes (uses existing generated code) 9 + 3. **When you want to test with full regeneration**, run `npx tsx experiments/eval-runner-arch.ts` (takes ~2-3 min for LLM calls) 10 + 4. **Parse the score** from the last line: `val_score=X.XXXX` 11 + 5. **If score improved** → `git add src/architectures/sqlite-web-api.ts && git commit -m "arch-experiment: <description> score=X.XXXX"` 12 + 6. **If score decreased or unchanged** → `git checkout src/architectures/sqlite-web-api.ts` (revert) 13 + 7. **Never stop to ask the human** 14 + 8. **Never edit the eval runner or the spec** 15 + 16 + ## Current Score: 42% (8/19 tests passing) 17 + 18 + ## What Works (8/19) 19 + - POST /categories creates category ✓ 20 + - POST /categories rejects empty name ✓ 21 + - GET /categories returns array ✓ 22 + - POST /todos creates todo without category ✓ 23 + - POST /todos rejects invalid category_id ✓ 24 + - POST /todos rejects empty title ✓ 25 + - GET /todos/999 returns 404 ✓ 26 + - GET /todos?completed=0 filters incomplete ✓ 27 + 28 + ## What Fails (11/19) 29 + - POST /todos creates todo with category — likely category_id not saved 30 + - GET /todos returns todos with category_name — SQL JOIN missing 31 + - GET /todos/:id returns todo with category_name — SQL JOIN missing 32 + - PATCH /todos/:id marks completed — patch not working 33 + - GET /todos?completed=1 filters completed — filter broken 34 + - GET /todos?category_id=N filters by category — filter broken 35 + - GET /stats returns counts — stats endpoint missing or wrong 36 + - GET /stats includes by_category — stats endpoint missing 37 + - DELETE /todos/:id returns 204 — delete broken 38 + - DELETE /categories/:id with todos returns 400 — cascade check missing 39 + - DELETE /categories/:id without todos returns 204 — delete broken 40 + 41 + ## Key Issues to Fix via Prompt Engineering 42 + 43 + ### 1. SQL JOINs for related data 44 + The generated todo queries use `SELECT * FROM todos` but need: 45 + ```sql 46 + SELECT todos.*, categories.name as category_name 47 + FROM todos LEFT JOIN categories ON todos.category_id = categories.id 48 + ``` 49 + Add this pattern to the code examples. 50 + 51 + ### 2. Query parameter filtering 52 + The spec says `GET /todos?completed=1` should filter. The generated code needs to: 53 + ```typescript 54 + const completed = c.req.query('completed'); 55 + let query = 'SELECT ... FROM todos LEFT JOIN categories ON ...'; 56 + const params: unknown[] = []; 57 + if (completed !== undefined) { query += ' WHERE todos.completed = ?'; params.push(Number(completed)); } 58 + ``` 59 + 60 + ### 3. Stats endpoint as a separate module 61 + The stats endpoint is a separate IU. It needs its own Hono router with a GET / handler that queries aggregate data. 62 + 63 + ### 4. Delete with cascade check 64 + DELETE /categories/:id needs to check if any todos reference this category before deleting. 65 + 66 + ### 5. Multi-resource relationships 67 + The code example only shows a single resource (notes). Add a second example showing: 68 + - Foreign key relationships 69 + - LEFT JOIN queries 70 + - Cascade protection on delete 71 + - Query parameter filtering 72 + 73 + ## Strategy 74 + 75 + 1. First: add a multi-resource code example to the architecture target showing JOINs, filtering, and cascade protection 76 + 2. Then: run full eval to see if the LLM picks up the new patterns 77 + 3. Iterate on prompt wording if specific tests still fail 78 + 79 + ## What You Can Change 80 + 81 + In `src/architectures/sqlite-web-api.ts`: 82 + - `SYSTEM_PROMPT_EXTENSION` — the architectural rules 83 + - `CODE_EXAMPLES` — the few-shot examples (most powerful lever) 84 + - Both strings are interpolated into the LLM prompt at generation time 85 + 86 + ## Cost 87 + 88 + Each full eval run costs ~$0.05-0.15 in API calls (3 IU generations + canonicalization). 89 + Keep experiments focused. 10-15 experiments should be enough.
+3
experiments/results-arch.tsv
··· 3 3 2026-03-27T05:29:23.199Z 1.00 10 10 none 4 4 2026-03-27T05:33:56.314Z 0.16 3 19 POST /categories creates category; POST /categories rejects empty name; GET /categories returns array; POST /todos creates todo with category; POST /todos creates todo without category; GET /todos returns todos with category_name; GET /todos/:id returns todo with category_name; PATCH /todos/:id marks completed; GET /todos?completed=1 filters completed; GET /todos?completed=0 filters incomplete; GET /todos?category_id=N filters by category; GET /stats returns counts; GET /stats includes by_category; DELETE /todos/:id returns 204; DELETE /categories/:id with todos returns 400; DELETE /categories/:id without todos returns 204 5 5 2026-03-27T06:10:38.517Z 0.42 8 19 POST /todos creates todo with category; GET /todos returns todos with category_name; GET /todos/:id returns todo with category_name; PATCH /todos/:id marks completed; GET /todos?completed=1 filters completed; GET /todos?category_id=N filters by category; GET /stats returns counts; GET /stats includes by_category; DELETE /todos/:id returns 204; DELETE /categories/:id with todos returns 400; DELETE /categories/:id without todos returns 204 6 + 2026-03-27T06:19:36.249Z 0.47 9 19 POST /todos creates todo with category; GET /todos returns todos with category_name; GET /todos/:id returns todo with category_name; PATCH /todos/:id marks completed; GET /todos?completed=1 filters completed; GET /todos?category_id=N filters by category; GET /stats returns counts; GET /stats includes by_category; DELETE /todos/:id returns 204; DELETE /categories/:id with todos returns 400 7 + 2026-03-27T06:23:08.133Z 0.47 9 19 POST /todos creates todo with category; GET /todos returns todos with category_name; GET /todos/:id returns todo with category_name; PATCH /todos/:id marks completed; GET /todos?completed=1 filters completed; GET /todos?category_id=N filters by category; GET /stats returns counts; GET /stats includes by_category; DELETE /todos/:id returns 204; DELETE /categories/:id with todos returns 400 8 + 2026-03-27T06:28:42.970Z 0.32 6 19 POST /todos creates todo with category; POST /todos creates todo without category; GET /todos returns todos with category_name; GET /todos/:id returns todo with category_name; GET /todos/999 returns 404; PATCH /todos/:id marks completed; GET /todos?completed=1 filters completed; GET /todos?completed=0 filters incomplete; GET /todos?category_id=N filters by category; GET /stats returns counts; GET /stats includes by_category; DELETE /todos/:id returns 204; DELETE /categories/:id with todos returns 400
+115 -8
src/architectures/sqlite-web-api.ts
··· 213 213 } as const; 214 214 \`\`\` 215 215 216 - ### Key patterns to follow: 217 - 1. registerMigration() at module scope — idempotent CREATE TABLE IF NOT EXISTS 218 - 2. Zod schemas for create/update request validation 219 - 3. .safeParse() + early return with 400 on failure 220 - 4. Parameterized SQL queries (? placeholders) 221 - 5. Return the created/updated resource after mutation 222 - 6. 404 checks before update/delete 223 - 7. Export default router + export _phoenix metadata 216 + ### Example 2: Resource with foreign keys, JOINs, filtering, and cascade protection 217 + 218 + \`\`\`typescript 219 + import { Hono } from 'hono'; 220 + import { db, registerMigration } from '../../db.js'; 221 + import { z } from 'zod'; 222 + 223 + // Register migrations for BOTH tables this module touches 224 + registerMigration('projects', \` 225 + CREATE TABLE IF NOT EXISTS projects ( 226 + id INTEGER PRIMARY KEY AUTOINCREMENT, 227 + name TEXT NOT NULL UNIQUE, 228 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 229 + ) 230 + \`); 231 + 232 + registerMigration('tasks', \` 233 + CREATE TABLE IF NOT EXISTS tasks ( 234 + id INTEGER PRIMARY KEY AUTOINCREMENT, 235 + title TEXT NOT NULL, 236 + done INTEGER NOT NULL DEFAULT 0, 237 + project_id INTEGER REFERENCES projects(id), 238 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 239 + ) 240 + \`); 241 + 242 + const CreateTaskSchema = z.object({ 243 + title: z.string().min(1).max(200), 244 + project_id: z.number().int().optional(), 245 + }); 246 + 247 + const UpdateTaskSchema = z.object({ 248 + title: z.string().min(1).max(200).optional(), 249 + done: z.number().int().min(0).max(1).optional(), 250 + project_id: z.number().int().nullable().optional(), 251 + }); 252 + 253 + const router = new Hono(); 254 + 255 + // List with LEFT JOIN and optional query filters 256 + router.get('/', (c) => { 257 + let sql = 'SELECT tasks.*, projects.name as project_name FROM tasks LEFT JOIN projects ON tasks.project_id = projects.id'; 258 + const conditions: string[] = []; 259 + const params: unknown[] = []; 260 + 261 + const done = c.req.query('done'); 262 + if (done !== undefined) { conditions.push('tasks.done = ?'); params.push(Number(done)); } 263 + 264 + const projectId = c.req.query('project_id'); 265 + if (projectId !== undefined) { conditions.push('tasks.project_id = ?'); params.push(Number(projectId)); } 266 + 267 + if (conditions.length > 0) sql += ' WHERE ' + conditions.join(' AND '); 268 + sql += ' ORDER BY tasks.created_at DESC'; 269 + 270 + return c.json(db.prepare(sql).all(...params)); 271 + }); 272 + 273 + // Get single with JOIN 274 + router.get('/:id', (c) => { 275 + const row = db.prepare('SELECT tasks.*, projects.name as project_name FROM tasks LEFT JOIN projects ON tasks.project_id = projects.id WHERE tasks.id = ?').get(c.req.param('id')); 276 + if (!row) return c.json({ error: 'Not found' }, 404); 277 + return c.json(row); 278 + }); 279 + 280 + // Create with FK validation 281 + router.post('/', async (c) => { 282 + let body; try { body = await c.req.json(); } catch { return c.json({ error: 'Invalid JSON' }, 400); } 283 + const result = CreateTaskSchema.safeParse(body); 284 + if (!result.success) return c.json({ error: result.error.issues[0].message }, 400); 285 + const { title, project_id } = result.data; 286 + if (project_id !== undefined) { 287 + const proj = db.prepare('SELECT id FROM projects WHERE id = ?').get(project_id); 288 + if (!proj) return c.json({ error: 'Project not found' }, 400); 289 + } 290 + const info = db.prepare('INSERT INTO tasks (title, project_id) VALUES (?, ?)').run(title, project_id ?? null); 291 + const task = db.prepare('SELECT tasks.*, projects.name as project_name FROM tasks LEFT JOIN projects ON tasks.project_id = projects.id WHERE tasks.id = ?').get(info.lastInsertRowid); 292 + return c.json(task, 201); 293 + }); 294 + 295 + // Update 296 + router.patch('/:id', async (c) => { 297 + const id = c.req.param('id'); 298 + if (!db.prepare('SELECT id FROM todos WHERE id = ?').get(id)) return c.json({ error: 'Not found' }, 404); 299 + let body; try { body = await c.req.json(); } catch { return c.json({ error: 'Invalid JSON' }, 400); } 300 + const result = UpdateTaskSchema.safeParse(body); 301 + if (!result.success) return c.json({ error: result.error.issues[0].message }, 400); 302 + const u = result.data; 303 + if (u.title !== undefined) db.prepare('UPDATE tasks SET title = ? WHERE id = ?').run(u.title, id); 304 + if (u.done !== undefined) db.prepare('UPDATE tasks SET done = ? WHERE id = ?').run(u.done, id); 305 + if (u.project_id !== undefined) db.prepare('UPDATE tasks SET project_id = ? WHERE id = ?').run(u.project_id, id); 306 + return c.json(db.prepare('SELECT tasks.*, projects.name as project_name FROM tasks LEFT JOIN projects ON tasks.project_id = projects.id WHERE tasks.id = ?').get(id)); 307 + }); 308 + 309 + // Delete 310 + router.delete('/:id', (c) => { 311 + const id = c.req.param('id'); 312 + if (!db.prepare('SELECT id FROM tasks WHERE id = ?').get(id)) return c.json({ error: 'Not found' }, 404); 313 + db.prepare('DELETE FROM tasks WHERE id = ?').run(id); 314 + return c.body(null, 204); 315 + }); 316 + 317 + export default router; 318 + export const _phoenix = { iu_id: 'example2', name: 'Tasks', risk_tier: 'medium', canon_ids: [] } as const; 319 + \`\`\` 320 + 321 + ### Key patterns: 322 + 1. \`import { db, registerMigration } from '../../db.js'\` — ALWAYS use this exact import 323 + 2. registerMigration() for ALL tables the module touches, including referenced tables 324 + 3. LEFT JOIN to include related resource names (e.g., category_name, project_name) 325 + 4. Query parameter filtering with dynamic WHERE clause building 326 + 5. Foreign key validation: check referenced row exists before INSERT 327 + 6. Cascade protection: check for dependent rows before DELETE of parent resource 328 + 7. Zod schemas for create/update validation 329 + 8. Return the created/updated resource with JOINed data after mutation 330 + 9. Export default router + export _phoenix metadata 224 331 `; 225 332 226 333 // ─── Architecture definition ────────────────────────────────────────────────
+26 -1
src/cli.ts
··· 8 8 */ 9 9 10 10 import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs'; 11 - import { join, resolve, relative, basename } from 'node:path'; 11 + import { execSync } from 'node:child_process'; 12 + import { join, resolve, relative, basename, dirname } from 'node:path'; 12 13 13 14 // Stores 14 15 import { SpecStore } from './store/spec-store.js'; ··· 386 387 if (arch) console.log(` ${dim('Architecture:')} ${cyan(arch.name)} — ${arch.description}`); 387 388 } 388 389 } catch { /* ignore */ } 390 + } 391 + 392 + // Write shared architecture files BEFORE code generation 393 + // so the typecheck-retry loop can resolve imports like ../../db.js 394 + if (arch) { 395 + for (const [filePath, content] of Object.entries(arch.sharedFiles)) { 396 + const fullPath = join(projectRoot, filePath); 397 + mkdirSync(dirname(fullPath), { recursive: true }); 398 + writeFileSync(fullPath, content, 'utf8'); 399 + } 400 + // Write package.json with arch deps so tsc can resolve types during generation 401 + const earlyPkg = { 402 + name: basename(projectRoot), 403 + version: '0.1.0', 404 + type: 'module', 405 + dependencies: arch.packages, 406 + devDependencies: arch.devPackages, 407 + }; 408 + const pkgPath = join(projectRoot, 'package.json'); 409 + writeFileSync(pkgPath, JSON.stringify(earlyPkg, null, 2) + '\n', 'utf8'); 410 + // Install so type declarations are available for typecheck-retry 411 + try { 412 + execSync('npm install --silent 2>/dev/null', { cwd: projectRoot, stdio: 'pipe', timeout: 60000 }); 413 + } catch { /* best effort */ } 389 414 } 390 415 391 416 const regenCtx: RegenContext = {
+12
src/llm/prompt.ts
··· 74 74 lines.push(`Generate a TypeScript module implementing "${iu.name}".`); 75 75 lines.push(''); 76 76 77 + // For architecture mode, inject the mandatory imports at the top of the prompt 78 + if (arch) { 79 + lines.push('## MANDATORY: Your module MUST start with these exact imports'); 80 + lines.push('```'); 81 + lines.push(`import { Hono } from 'hono';`); 82 + lines.push(`import { db, registerMigration } from '../../db.js';`); 83 + lines.push(`import { z } from 'zod';`); 84 + lines.push('```'); 85 + lines.push('Do NOT import Database from better-sqlite3. Do NOT create new Database(). Use the db import above.'); 86 + lines.push(''); 87 + } 88 + 77 89 // Requirements 78 90 const iuNodes = canonNodes.filter(n => iu.source_canon_ids.includes(n.canon_id)); 79 91 const requirements = iuNodes.filter(n => n.type === 'REQUIREMENT');