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

Configure Feed

Select the types of activity you want to include in your feed.

feat: generate web UI from spec + architecture stub fallback

Added Web Interface section to todo spec. Phoenix now generates a
complete single-page HTML app with inline CSS/JS that calls the API
via fetch(). Web/UI modules mount at root (/). Architecture-aware
stub fallback produces valid Hono routers when LLM fails.

+13367 -358
+1
.gitignore
··· 2 2 dist/ 3 3 .phoenix/ 4 4 *.tsbuildinfo 5 + examples/settle-up/.phoenix/
examples/.DS_Store

This is a binary file and will not be displayed.

+3 -3
examples/phoenix-self/package.json
··· 1 1 { 2 2 "name": "phoenix-self", 3 3 "version": "0.1.0", 4 - "description": "Generated by Phoenix VCS — 6 services (dog-food: Phoenix specs itself)", 4 + "description": "Generated by Phoenix VCS — 6 services", 5 5 "type": "module", 6 6 "scripts": { 7 7 "build": "tsc", 8 8 "typecheck": "tsc --noEmit", 9 9 "test": "vitest run", 10 10 "test:watch": "vitest", 11 - "start:ingestion": "tsc && node dist/generated/ingestion/server.js", 12 11 "start:canonicalization": "tsc && node dist/generated/canonicalization/server.js", 13 12 "start:implementation": "tsc && node dist/generated/implementation/server.js", 13 + "start:ingestion": "tsc && node dist/generated/ingestion/server.js", 14 14 "start:integrity": "tsc && node dist/generated/integrity/server.js", 15 15 "start:operations": "tsc && node dist/generated/operations/server.js", 16 16 "start:platform": "tsc && node dist/generated/platform/server.js", 17 - "start": "tsc && node dist/generated/platform/server.js" 17 + "start": "tsc && node dist/generated/canonicalization/server.js" 18 18 }, 19 19 "devDependencies": { 20 20 "typescript": "^5.4.0",
+27 -13
examples/phoenix-self/src/generated/canonicalization/__tests__/canonicalization.test.ts
··· 8 8 import { describe, it, expect, afterAll } from 'vitest'; 9 9 import { startServer } from '../server.js'; 10 10 11 - import * as canonPipeline from '../canon-pipeline.js'; 12 - import * as shadowMode from '../shadow-mode.js'; 11 + import * as canonicalNodeExtraction from '../canonical-node-extraction.js'; 12 + import * as pipelineIdentity from '../pipeline-identity.js'; 13 + import * as shadowCanonicalization from '../shadow-canonicalization.js'; 13 14 14 15 describe('Canonicalization modules', () => { 15 - describe('Canon Pipeline', () => { 16 + describe('Canonical Node Extraction', () => { 16 17 it('exports Phoenix traceability metadata', () => { 17 - expect(canonPipeline._phoenix).toBeDefined(); 18 - expect(canonPipeline._phoenix.name).toBe('Canon Pipeline'); 19 - expect(canonPipeline._phoenix.risk_tier).toBeTruthy(); 18 + expect(canonicalNodeExtraction._phoenix).toBeDefined(); 19 + expect(canonicalNodeExtraction._phoenix.name).toBe('Canonical Node Extraction'); 20 + expect(canonicalNodeExtraction._phoenix.risk_tier).toBeTruthy(); 20 21 }); 21 22 22 23 it('has exported functions', () => { 23 - const exports = Object.keys(canonPipeline).filter(k => k !== '_phoenix'); 24 + const exports = Object.keys(canonicalNodeExtraction).filter(k => k !== '_phoenix'); 24 25 expect(exports.length).toBeGreaterThan(0); 25 26 }); 26 27 }); 27 28 28 - describe('Shadow Mode', () => { 29 + describe('Pipeline Identity', () => { 29 30 it('exports Phoenix traceability metadata', () => { 30 - expect(shadowMode._phoenix).toBeDefined(); 31 - expect(shadowMode._phoenix.name).toBe('Shadow Mode'); 32 - expect(shadowMode._phoenix.risk_tier).toBeTruthy(); 31 + expect(pipelineIdentity._phoenix).toBeDefined(); 32 + expect(pipelineIdentity._phoenix.name).toBe('Pipeline Identity'); 33 + expect(pipelineIdentity._phoenix.risk_tier).toBeTruthy(); 33 34 }); 34 35 35 36 it('has exported functions', () => { 36 - const exports = Object.keys(shadowMode).filter(k => k !== '_phoenix'); 37 + const exports = Object.keys(pipelineIdentity).filter(k => k !== '_phoenix'); 38 + expect(exports.length).toBeGreaterThan(0); 39 + }); 40 + }); 41 + 42 + describe('Shadow Canonicalization', () => { 43 + it('exports Phoenix traceability metadata', () => { 44 + expect(shadowCanonicalization._phoenix).toBeDefined(); 45 + expect(shadowCanonicalization._phoenix.name).toBe('Shadow Canonicalization'); 46 + expect(shadowCanonicalization._phoenix.risk_tier).toBeTruthy(); 47 + }); 48 + 49 + it('has exported functions', () => { 50 + const exports = Object.keys(shadowCanonicalization).filter(k => k !== '_phoenix'); 37 51 expect(exports.length).toBeGreaterThan(0); 38 52 }); 39 53 }); ··· 67 81 const res = await fetch(`http://localhost:${instance.port}/modules`); 68 82 expect(res.status).toBe(200); 69 83 const body = await res.json() as Array<Record<string, unknown>>; 70 - expect(body.length).toBe(2); 84 + expect(body.length).toBe(3); 71 85 }); 72 86 73 87 it('GET /unknown returns 404', async () => {
+235
examples/phoenix-self/src/generated/canonicalization/canonical-node-extraction.ts
··· 1 + import { createHash } from 'node:crypto'; 2 + 3 + export type CanonicalNodeType = 'requirement' | 'constraint' | 'invariant' | 'definition' | 'context'; 4 + 5 + export interface CanonicalNode { 6 + canon_id: string; 7 + type: CanonicalNodeType; 8 + normalized_statement: string; 9 + confidence_score: number; 10 + source_clause_ids: string[]; 11 + tags: string[]; 12 + linked_canon_ids: string[]; 13 + } 14 + 15 + export interface SourceClause { 16 + id: string; 17 + text: string; 18 + sentence_index: number; 19 + } 20 + 21 + export interface ExtractionResult { 22 + nodes: CanonicalNode[]; 23 + coverage_percentage: number; 24 + total_sentences: number; 25 + processed_sentences: number; 26 + } 27 + 28 + export interface ExtractionOptions { 29 + min_confidence_threshold?: number; 30 + enable_linking?: boolean; 31 + custom_keywords?: Record<CanonicalNodeType, string[]>; 32 + } 33 + 34 + const DEFAULT_KEYWORDS: Record<CanonicalNodeType, string[]> = { 35 + requirement: ['must', 'shall', 'should', 'required', 'needs', 'has to', 'ought to'], 36 + constraint: ['cannot', 'must not', 'shall not', 'forbidden', 'prohibited', 'limited', 'restricted'], 37 + invariant: ['always', 'never', 'invariant', 'constant', 'maintains', 'preserves', 'ensures'], 38 + definition: ['is defined as', 'means', 'refers to', 'is', 'represents', 'denotes', 'signifies'], 39 + context: [] 40 + }; 41 + 42 + export class CanonicalNodeExtractor { 43 + private keywords: Record<CanonicalNodeType, string[]>; 44 + private minConfidenceThreshold: number; 45 + private enableLinking: boolean; 46 + 47 + constructor(options: ExtractionOptions = {}) { 48 + this.keywords = options.custom_keywords || DEFAULT_KEYWORDS; 49 + this.minConfidenceThreshold = options.min_confidence_threshold || 0.1; 50 + this.enableLinking = options.enable_linking || true; 51 + } 52 + 53 + extract(sourceClauses: SourceClause[]): ExtractionResult { 54 + const nodes: CanonicalNode[] = []; 55 + const processedSentenceIds = new Set<number>(); 56 + 57 + for (const clause of sourceClauses) { 58 + const node = this.extractNodeFromClause(clause); 59 + if (node && node.confidence_score >= this.minConfidenceThreshold) { 60 + nodes.push(node); 61 + processedSentenceIds.add(clause.sentence_index); 62 + } 63 + } 64 + 65 + if (this.enableLinking) { 66 + this.linkNodes(nodes); 67 + } 68 + 69 + const totalSentences = new Set(sourceClauses.map(c => c.sentence_index)).size; 70 + const processedSentences = processedSentenceIds.size; 71 + const coveragePercentage = totalSentences > 0 ? (processedSentences / totalSentences) * 100 : 0; 72 + 73 + return { 74 + nodes, 75 + coverage_percentage: Math.round(coveragePercentage * 100) / 100, 76 + total_sentences: totalSentences, 77 + processed_sentences: processedSentences 78 + }; 79 + } 80 + 81 + private extractNodeFromClause(clause: SourceClause): CanonicalNode | null { 82 + const normalizedText = this.normalizeStatement(clause.text); 83 + const type = this.classifyNodeType(normalizedText); 84 + const confidence = this.calculateConfidence(normalizedText, type); 85 + const tags = this.extractTags(normalizedText); 86 + const canonId = this.generateCanonId(normalizedText, type); 87 + 88 + return { 89 + canon_id: canonId, 90 + type, 91 + normalized_statement: normalizedText, 92 + confidence_score: confidence, 93 + source_clause_ids: [clause.id], 94 + tags, 95 + linked_canon_ids: [] 96 + }; 97 + } 98 + 99 + private normalizeStatement(text: string): string { 100 + return text 101 + .trim() 102 + .replace(/\s+/g, ' ') 103 + .replace(/[^\w\s.,;:!?()-]/g, '') 104 + .toLowerCase(); 105 + } 106 + 107 + private classifyNodeType(normalizedText: string): CanonicalNodeType { 108 + const scores: Record<CanonicalNodeType, number> = { 109 + requirement: 0, 110 + constraint: 0, 111 + invariant: 0, 112 + definition: 0, 113 + context: 0 114 + }; 115 + 116 + for (const [type, keywords] of Object.entries(this.keywords) as [CanonicalNodeType, string[]][]) { 117 + for (const keyword of keywords) { 118 + if (normalizedText.includes(keyword.toLowerCase())) { 119 + scores[type] += 1; 120 + } 121 + } 122 + } 123 + 124 + const maxScore = Math.max(...Object.values(scores)); 125 + if (maxScore === 0) { 126 + return 'context'; 127 + } 128 + 129 + const bestType = Object.entries(scores).find(([_, score]) => score === maxScore)?.[0] as CanonicalNodeType; 130 + return bestType || 'context'; 131 + } 132 + 133 + private calculateConfidence(normalizedText: string, type: CanonicalNodeType): number { 134 + let confidence = 0.3; // Base confidence 135 + 136 + // Keyword matching boost 137 + const keywords = this.keywords[type]; 138 + const keywordMatches = keywords.filter(keyword => 139 + normalizedText.includes(keyword.toLowerCase()) 140 + ).length; 141 + 142 + confidence += keywordMatches * 0.2; 143 + 144 + // Length and structure boost 145 + const wordCount = normalizedText.split(' ').length; 146 + if (wordCount >= 5 && wordCount <= 30) { 147 + confidence += 0.1; 148 + } 149 + 150 + // Sentence structure boost 151 + if (normalizedText.includes('.') || normalizedText.includes('!') || normalizedText.includes('?')) { 152 + confidence += 0.1; 153 + } 154 + 155 + // Context type penalty (since it's default) 156 + if (type === 'context') { 157 + confidence *= 0.7; 158 + } 159 + 160 + return Math.min(1.0, Math.max(0.0, Math.round(confidence * 100) / 100)); 161 + } 162 + 163 + private extractTags(normalizedText: string): string[] { 164 + const tags: string[] = []; 165 + 166 + // Extract potential entity names (capitalized words in original would be lost in normalization) 167 + // So we look for patterns that suggest entities 168 + if (normalizedText.includes('system')) tags.push('system'); 169 + if (normalizedText.includes('user')) tags.push('user'); 170 + if (normalizedText.includes('data')) tags.push('data'); 171 + if (normalizedText.includes('interface')) tags.push('interface'); 172 + if (normalizedText.includes('security')) tags.push('security'); 173 + if (normalizedText.includes('performance')) tags.push('performance'); 174 + 175 + return tags; 176 + } 177 + 178 + private generateCanonId(normalizedText: string, type: CanonicalNodeType): string { 179 + const content = `${type}:${normalizedText}`; 180 + return createHash('sha256').update(content).digest('hex'); 181 + } 182 + 183 + private linkNodes(nodes: CanonicalNode[]): void { 184 + for (let i = 0; i < nodes.length; i++) { 185 + for (let j = i + 1; j < nodes.length; j++) { 186 + const nodeA = nodes[i]; 187 + const nodeB = nodes[j]; 188 + 189 + if (this.shouldLink(nodeA, nodeB)) { 190 + nodeA.linked_canon_ids.push(nodeB.canon_id); 191 + nodeB.linked_canon_ids.push(nodeA.canon_id); 192 + } 193 + } 194 + } 195 + } 196 + 197 + private shouldLink(nodeA: CanonicalNode, nodeB: CanonicalNode): boolean { 198 + // Link nodes with shared tags 199 + const sharedTags = nodeA.tags.filter(tag => nodeB.tags.includes(tag)); 200 + if (sharedTags.length > 0) return true; 201 + 202 + // Link definitions with requirements/constraints that might use them 203 + if (nodeA.type === 'definition' && ['requirement', 'constraint'].includes(nodeB.type)) { 204 + const definitionWords = nodeA.normalized_statement.split(' '); 205 + const targetWords = nodeB.normalized_statement.split(' '); 206 + const overlap = definitionWords.filter(word => targetWords.includes(word) && word.length > 3); 207 + if (overlap.length >= 2) return true; 208 + } 209 + 210 + if (nodeB.type === 'definition' && ['requirement', 'constraint'].includes(nodeA.type)) { 211 + const definitionWords = nodeB.normalized_statement.split(' '); 212 + const targetWords = nodeA.normalized_statement.split(' '); 213 + const overlap = definitionWords.filter(word => targetWords.includes(word) && word.length > 3); 214 + if (overlap.length >= 2) return true; 215 + } 216 + 217 + return false; 218 + } 219 + } 220 + 221 + export function extractCanonicalNodes( 222 + sourceClauses: SourceClause[], 223 + options?: ExtractionOptions 224 + ): ExtractionResult { 225 + const extractor = new CanonicalNodeExtractor(options); 226 + return extractor.extract(sourceClauses); 227 + } 228 + 229 + /** @internal Phoenix VCS traceability — do not remove. */ 230 + export const _phoenix = { 231 + iu_id: '62bbd8e4aa6c4b46a289ef307ff265b25d2c5d0a13370bb5689e253f35f9cafd', 232 + name: 'Canonical Node Extraction', 233 + risk_tier: 'high', 234 + canon_ids: [5 as const], 235 + } as const;
+10 -2
examples/phoenix-self/src/generated/canonicalization/index.ts
··· 1 - export * as canonPipeline from './canon-pipeline.js'; 2 - export * as shadowMode from './shadow-mode.js'; 1 + /** 2 + * Canonicalization 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Barrel export for all Canonicalization modules. 6 + */ 7 + 8 + export * as canonicalNodeExtraction from './canonical-node-extraction.js'; 9 + export * as pipelineIdentity from './pipeline-identity.js'; 10 + export * as shadowCanonicalization from './shadow-canonicalization.js';
+269
examples/phoenix-self/src/generated/canonicalization/pipeline-identity.ts
··· 1 + import { createHash } from 'node:crypto'; 2 + 3 + export interface PipelineIdentity { 4 + canonpipelineid: string; 5 + modelid: string; 6 + promptpackversion: string; 7 + extractionrulesversion: string; 8 + diffpolicyversion: string; 9 + } 10 + 11 + export interface PipelineUpgradeNode { 12 + type: 'pipelineupgrade'; 13 + id: string; 14 + timestamp: string; 15 + fromPipeline: PipelineIdentity; 16 + toPipeline: PipelineIdentity; 17 + upgradeReason: string; 18 + metadata: Record<string, unknown>; 19 + } 20 + 21 + export interface ProvenanceGraph { 22 + addNode(node: PipelineUpgradeNode): void; 23 + getNode(id: string): PipelineUpgradeNode | undefined; 24 + getUpgradeHistory(pipelineId: string): PipelineUpgradeNode[]; 25 + } 26 + 27 + export class PipelineIdentityManager { 28 + private currentPipeline: PipelineIdentity | null = null; 29 + private provenanceGraph: ProvenanceGraph; 30 + private upgradeHistory: Map<string, PipelineUpgradeNode[]> = new Map(); 31 + 32 + constructor(provenanceGraph: ProvenanceGraph) { 33 + this.provenanceGraph = provenanceGraph; 34 + } 35 + 36 + public createPipelineIdentity( 37 + modelid: string, 38 + promptpackversion: string, 39 + extractionrulesversion: string, 40 + diffpolicyversion: string 41 + ): PipelineIdentity { 42 + const canonpipelineid = this.generateCanonPipelineId( 43 + modelid, 44 + promptpackversion, 45 + extractionrulesversion, 46 + diffpolicyversion 47 + ); 48 + 49 + return { 50 + canonpipelineid, 51 + modelid, 52 + promptpackversion, 53 + extractionrulesversion, 54 + diffpolicyversion, 55 + }; 56 + } 57 + 58 + public setPipeline(pipeline: PipelineIdentity): void { 59 + if (this.currentPipeline && !this.isPipelineEqual(this.currentPipeline, pipeline)) { 60 + this.recordPipelineUpgrade(this.currentPipeline, pipeline, 'Explicit pipeline change'); 61 + } 62 + this.currentPipeline = pipeline; 63 + } 64 + 65 + public getCurrentPipeline(): PipelineIdentity | null { 66 + return this.currentPipeline; 67 + } 68 + 69 + public upgradePipeline( 70 + newPipeline: PipelineIdentity, 71 + reason: string, 72 + metadata: Record<string, unknown> = {} 73 + ): void { 74 + if (!this.currentPipeline) { 75 + throw new Error('Cannot upgrade pipeline: no current pipeline set'); 76 + } 77 + 78 + if (this.isPipelineEqual(this.currentPipeline, newPipeline)) { 79 + throw new Error('Cannot upgrade to identical pipeline version'); 80 + } 81 + 82 + this.recordPipelineUpgrade(this.currentPipeline, newPipeline, reason, metadata); 83 + this.currentPipeline = newPipeline; 84 + } 85 + 86 + public validatePipelineUpgrade( 87 + fromPipeline: PipelineIdentity, 88 + toPipeline: PipelineIdentity 89 + ): { valid: boolean; errors: string[] } { 90 + const errors: string[] = []; 91 + 92 + if (this.isPipelineEqual(fromPipeline, toPipeline)) { 93 + errors.push('Source and target pipelines are identical'); 94 + } 95 + 96 + if (!this.isValidPipelineIdentity(fromPipeline)) { 97 + errors.push('Invalid source pipeline identity'); 98 + } 99 + 100 + if (!this.isValidPipelineIdentity(toPipeline)) { 101 + errors.push('Invalid target pipeline identity'); 102 + } 103 + 104 + return { 105 + valid: errors.length === 0, 106 + errors, 107 + }; 108 + } 109 + 110 + public getUpgradeHistory(pipelineId?: string): PipelineUpgradeNode[] { 111 + if (pipelineId) { 112 + return this.upgradeHistory.get(pipelineId) || []; 113 + } 114 + 115 + const allHistory: PipelineUpgradeNode[] = []; 116 + for (const history of this.upgradeHistory.values()) { 117 + allHistory.push(...history); 118 + } 119 + 120 + return allHistory.sort((a, b) => a.timestamp.localeCompare(b.timestamp)); 121 + } 122 + 123 + private generateCanonPipelineId( 124 + modelid: string, 125 + promptpackversion: string, 126 + extractionrulesversion: string, 127 + diffpolicyversion: string 128 + ): string { 129 + const components = [modelid, promptpackversion, extractionrulesversion, diffpolicyversion]; 130 + const hash = createHash('sha256'); 131 + hash.update(components.join('|')); 132 + return hash.digest('hex'); 133 + } 134 + 135 + private isPipelineEqual(a: PipelineIdentity, b: PipelineIdentity): boolean { 136 + return ( 137 + a.canonpipelineid === b.canonpipelineid && 138 + a.modelid === b.modelid && 139 + a.promptpackversion === b.promptpackversion && 140 + a.extractionrulesversion === b.extractionrulesversion && 141 + a.diffpolicyversion === b.diffpolicyversion 142 + ); 143 + } 144 + 145 + private isValidPipelineIdentity(pipeline: PipelineIdentity): boolean { 146 + return !!( 147 + pipeline.canonpipelineid && 148 + pipeline.modelid && 149 + pipeline.promptpackversion && 150 + pipeline.extractionrulesversion && 151 + pipeline.diffpolicyversion 152 + ); 153 + } 154 + 155 + private recordPipelineUpgrade( 156 + fromPipeline: PipelineIdentity, 157 + toPipeline: PipelineIdentity, 158 + reason: string, 159 + metadata: Record<string, unknown> = {} 160 + ): void { 161 + const upgradeNode: PipelineUpgradeNode = { 162 + type: 'pipelineupgrade', 163 + id: this.generateUpgradeId(fromPipeline, toPipeline), 164 + timestamp: new Date().toISOString(), 165 + fromPipeline, 166 + toPipeline, 167 + upgradeReason: reason, 168 + metadata, 169 + }; 170 + 171 + this.provenanceGraph.addNode(upgradeNode); 172 + 173 + // Track in local history 174 + const fromHistory = this.upgradeHistory.get(fromPipeline.canonpipelineid) || []; 175 + fromHistory.push(upgradeNode); 176 + this.upgradeHistory.set(fromPipeline.canonpipelineid, fromHistory); 177 + 178 + const toHistory = this.upgradeHistory.get(toPipeline.canonpipelineid) || []; 179 + toHistory.push(upgradeNode); 180 + this.upgradeHistory.set(toPipeline.canonpipelineid, toHistory); 181 + } 182 + 183 + private generateUpgradeId(from: PipelineIdentity, to: PipelineIdentity): string { 184 + const hash = createHash('sha256'); 185 + hash.update(`${from.canonpipelineid}->${to.canonpipelineid}-${Date.now()}`); 186 + return hash.digest('hex'); 187 + } 188 + } 189 + 190 + export function createPipelineIdentity( 191 + modelid: string, 192 + promptpackversion: string, 193 + extractionrulesversion: string, 194 + diffpolicyversion: string 195 + ): PipelineIdentity { 196 + const hash = createHash('sha256'); 197 + hash.update([modelid, promptpackversion, extractionrulesversion, diffpolicyversion].join('|')); 198 + const canonpipelineid = hash.digest('hex'); 199 + 200 + return { 201 + canonpipelineid, 202 + modelid, 203 + promptpackversion, 204 + extractionrulesversion, 205 + diffpolicyversion, 206 + }; 207 + } 208 + 209 + export function comparePipelineIdentities(a: PipelineIdentity, b: PipelineIdentity): boolean { 210 + return ( 211 + a.canonpipelineid === b.canonpipelineid && 212 + a.modelid === b.modelid && 213 + a.promptpackversion === b.promptpackversion && 214 + a.extractionrulesversion === b.extractionrulesversion && 215 + a.diffpolicyversion === b.diffpolicyversion 216 + ); 217 + } 218 + 219 + export function validatePipelineIdentity(pipeline: PipelineIdentity): { valid: boolean; errors: string[] } { 220 + const errors: string[] = []; 221 + 222 + if (!pipeline.canonpipelineid) { 223 + errors.push('canonpipelineid is required'); 224 + } 225 + 226 + if (!pipeline.modelid) { 227 + errors.push('modelid is required'); 228 + } 229 + 230 + if (!pipeline.promptpackversion) { 231 + errors.push('promptpackversion is required'); 232 + } 233 + 234 + if (!pipeline.extractionrulesversion) { 235 + errors.push('extractionrulesversion is required'); 236 + } 237 + 238 + if (!pipeline.diffpolicyversion) { 239 + errors.push('diffpolicyversion is required'); 240 + } 241 + 242 + // Validate canonpipelineid matches computed hash 243 + if (pipeline.canonpipelineid && pipeline.modelid && pipeline.promptpackversion && 244 + pipeline.extractionrulesversion && pipeline.diffpolicyversion) { 245 + const expectedId = createPipelineIdentity( 246 + pipeline.modelid, 247 + pipeline.promptpackversion, 248 + pipeline.extractionrulesversion, 249 + pipeline.diffpolicyversion 250 + ).canonpipelineid; 251 + 252 + if (pipeline.canonpipelineid !== expectedId) { 253 + errors.push('canonpipelineid does not match computed hash'); 254 + } 255 + } 256 + 257 + return { 258 + valid: errors.length === 0, 259 + errors, 260 + }; 261 + } 262 + 263 + /** @internal Phoenix VCS traceability — do not remove. */ 264 + export const _phoenix = { 265 + iu_id: '736ad84d7671ed925c1e4bd58cd988bf6746c8e189baef37de1a22b0122e2ab8', 266 + name: 'Pipeline Identity', 267 + risk_tier: 'high', 268 + canon_ids: [4 as const], 269 + } as const;
+7 -5
examples/phoenix-self/src/generated/canonicalization/server.ts
··· 7 7 8 8 import { createServer, IncomingMessage, ServerResponse } from 'node:http'; 9 9 10 - import * as canonPipeline from './canon-pipeline.js'; 11 - import * as shadowMode from './shadow-mode.js'; 10 + import * as canonicalNodeExtraction from './canonical-node-extraction.js'; 11 + import * as pipelineIdentity from './pipeline-identity.js'; 12 + import * as shadowCanonicalization from './shadow-canonicalization.js'; 12 13 13 14 // ─── Metrics ───────────────────────────────────────────────────────────────── 14 15 ··· 22 23 // ─── Module Registry ───────────────────────────────────────────────────────── 23 24 24 25 const _svcModules = { 25 - 'canon-pipeline': canonPipeline, 26 - 'shadow-mode': shadowMode, 26 + 'canonical-node-extraction': canonicalNodeExtraction, 27 + 'pipeline-identity': pipelineIdentity, 28 + 'shadow-canonicalization': shadowCanonicalization, 27 29 }; 28 30 29 31 // ─── Router ────────────────────────────────────────────────────────────────── ··· 92 94 } 93 95 94 96 export function startServer(port?: number): { server: ReturnType<typeof createServer>; port: number; ready: Promise<void> } { 95 - const requestedPort = port ?? parseInt(process.env.CANONICALIZATION_PORT ?? process.env.PORT ?? '3002', 10); 97 + const requestedPort = port ?? parseInt(process.env.CANONICALIZATION_PORT ?? process.env.PORT ?? '3000', 10); 96 98 const server = createServer(handleRequest); 97 99 let actualPort = requestedPort; 98 100
+276
examples/phoenix-self/src/generated/canonicalization/shadow-canonicalization.ts
··· 1 + import { EventEmitter } from 'node:events'; 2 + 3 + export interface ShadowMetrics { 4 + nodechangepct: number; 5 + edgechangepct: number; 6 + riskescalations: number; 7 + orphannodes: number; 8 + outofscopegrowth: number; 9 + semanticstmt_drift: number; 10 + } 11 + 12 + export interface PipelineResult { 13 + nodes: Map<string, any>; 14 + edges: Map<string, any>; 15 + metadata: Record<string, any>; 16 + } 17 + 18 + export interface ShadowComparison { 19 + oldResult: PipelineResult; 20 + newResult: PipelineResult; 21 + metrics: ShadowMetrics; 22 + timestamp: number; 23 + } 24 + 25 + export type UpgradeClassification = 'safe' | 'compaction' | 'reject'; 26 + 27 + export interface UpgradeDecision { 28 + classification: UpgradeClassification; 29 + metrics: ShadowMetrics; 30 + reasons: string[]; 31 + canApply: boolean; 32 + } 33 + 34 + export interface ShadowCanonicalizer { 35 + runShadowComparison(oldPipeline: () => Promise<PipelineResult>, newPipeline: () => Promise<PipelineResult>): Promise<ShadowComparison>; 36 + classifyUpgrade(metrics: ShadowMetrics): UpgradeDecision; 37 + acceptUpgrade(comparison: ShadowComparison): void; 38 + on(event: 'shadow-complete', listener: (comparison: ShadowComparison) => void): this; 39 + on(event: 'upgrade-classified', listener: (decision: UpgradeDecision) => void): this; 40 + on(event: 'upgrade-accepted', listener: (comparison: ShadowComparison) => void): this; 41 + } 42 + 43 + class ShadowCanonicalizerImpl extends EventEmitter implements ShadowCanonicalizer { 44 + private acceptedUpgrades = new Set<string>(); 45 + 46 + async runShadowComparison( 47 + oldPipeline: () => Promise<PipelineResult>, 48 + newPipeline: () => Promise<PipelineResult> 49 + ): Promise<ShadowComparison> { 50 + const [oldResult, newResult] = await Promise.all([ 51 + oldPipeline(), 52 + newPipeline() 53 + ]); 54 + 55 + const metrics = this.computeMetrics(oldResult, newResult); 56 + 57 + const comparison: ShadowComparison = { 58 + oldResult, 59 + newResult, 60 + metrics, 61 + timestamp: Date.now() 62 + }; 63 + 64 + this.emit('shadow-complete', comparison); 65 + return comparison; 66 + } 67 + 68 + private computeMetrics(oldResult: PipelineResult, newResult: PipelineResult): ShadowMetrics { 69 + const oldNodeCount = oldResult.nodes.size; 70 + const newNodeCount = newResult.nodes.size; 71 + const oldEdgeCount = oldResult.edges.size; 72 + const newEdgeCount = newResult.edges.size; 73 + 74 + const nodechangepct = oldNodeCount === 0 ? 0 : 75 + Math.abs(newNodeCount - oldNodeCount) / oldNodeCount * 100; 76 + 77 + const edgechangepct = oldEdgeCount === 0 ? 0 : 78 + Math.abs(newEdgeCount - oldEdgeCount) / oldEdgeCount * 100; 79 + 80 + const orphannodes = this.countOrphanNodes(oldResult, newResult); 81 + const riskescalations = this.countRiskEscalations(oldResult, newResult); 82 + const outofscopegrowth = this.measureOutOfScopeGrowth(oldResult, newResult); 83 + const semanticstmt_drift = this.measureSemanticDrift(oldResult, newResult); 84 + 85 + return { 86 + nodechangepct, 87 + edgechangepct, 88 + riskescalations, 89 + orphannodes, 90 + outofscopegrowth, 91 + semanticstmt_drift 92 + }; 93 + } 94 + 95 + private countOrphanNodes(oldResult: PipelineResult, newResult: PipelineResult): number { 96 + let orphanCount = 0; 97 + 98 + for (const [nodeId] of oldResult.nodes) { 99 + if (!newResult.nodes.has(nodeId)) { 100 + const hasIncomingEdges = Array.from(oldResult.edges.values()) 101 + .some((edge: any) => edge.target === nodeId); 102 + const hasOutgoingEdges = Array.from(oldResult.edges.values()) 103 + .some((edge: any) => edge.source === nodeId); 104 + 105 + if (hasIncomingEdges || hasOutgoingEdges) { 106 + orphanCount++; 107 + } 108 + } 109 + } 110 + 111 + return orphanCount; 112 + } 113 + 114 + private countRiskEscalations(oldResult: PipelineResult, newResult: PipelineResult): number { 115 + let escalations = 0; 116 + 117 + for (const [nodeId, newNode] of newResult.nodes) { 118 + const oldNode = oldResult.nodes.get(nodeId); 119 + if (oldNode) { 120 + const oldRisk = oldNode.riskLevel || 'low'; 121 + const newRisk = newNode.riskLevel || 'low'; 122 + 123 + if (this.isRiskEscalation(oldRisk, newRisk)) { 124 + escalations++; 125 + } 126 + } 127 + } 128 + 129 + return escalations; 130 + } 131 + 132 + private isRiskEscalation(oldRisk: string, newRisk: string): boolean { 133 + const riskLevels = { low: 1, medium: 2, high: 3, critical: 4 }; 134 + const oldLevel = riskLevels[oldRisk as keyof typeof riskLevels] || 1; 135 + const newLevel = riskLevels[newRisk as keyof typeof riskLevels] || 1; 136 + return newLevel > oldLevel; 137 + } 138 + 139 + private measureOutOfScopeGrowth(oldResult: PipelineResult, newResult: PipelineResult): number { 140 + const oldScopeSize = oldResult.metadata.scopeSize || 0; 141 + const newScopeSize = newResult.metadata.scopeSize || 0; 142 + 143 + if (oldScopeSize === 0) return 0; 144 + return Math.max(0, (newScopeSize - oldScopeSize) / oldScopeSize * 100); 145 + } 146 + 147 + private measureSemanticDrift(oldResult: PipelineResult, newResult: PipelineResult): number { 148 + let driftScore = 0; 149 + let comparedNodes = 0; 150 + 151 + for (const [nodeId, newNode] of newResult.nodes) { 152 + const oldNode = oldResult.nodes.get(nodeId); 153 + if (oldNode && oldNode.semanticHash && newNode.semanticHash) { 154 + if (oldNode.semanticHash !== newNode.semanticHash) { 155 + driftScore++; 156 + } 157 + comparedNodes++; 158 + } 159 + } 160 + 161 + return comparedNodes === 0 ? 0 : (driftScore / comparedNodes) * 100; 162 + } 163 + 164 + classifyUpgrade(metrics: ShadowMetrics): UpgradeDecision { 165 + const reasons: string[] = []; 166 + 167 + // Check for rejection criteria 168 + if (metrics.orphannodes > 0) { 169 + reasons.push(`${metrics.orphannodes} orphan nodes detected`); 170 + } 171 + 172 + if (metrics.nodechangepct > 25) { 173 + reasons.push(`Excessive node churn: ${metrics.nodechangepct.toFixed(2)}%`); 174 + } 175 + 176 + if (metrics.semanticstmt_drift > 15) { 177 + reasons.push(`Large semantic drift: ${metrics.semanticstmt_drift.toFixed(2)}%`); 178 + } 179 + 180 + if (reasons.length > 0) { 181 + const decision: UpgradeDecision = { 182 + classification: 'reject', 183 + metrics, 184 + reasons, 185 + canApply: false 186 + }; 187 + this.emit('upgrade-classified', decision); 188 + return decision; 189 + } 190 + 191 + // Check for safe classification 192 + if (metrics.nodechangepct <= 3 && 193 + metrics.orphannodes === 0 && 194 + metrics.riskescalations === 0) { 195 + const decision: UpgradeDecision = { 196 + classification: 'safe', 197 + metrics, 198 + reasons: ['Low node change rate', 'No orphan nodes', 'No risk escalations'], 199 + canApply: true 200 + }; 201 + this.emit('upgrade-classified', decision); 202 + return decision; 203 + } 204 + 205 + // Check for compaction event 206 + if (metrics.nodechangepct <= 25 && 207 + metrics.orphannodes === 0 && 208 + metrics.riskescalations <= 2) { 209 + const decision: UpgradeDecision = { 210 + classification: 'compaction', 211 + metrics, 212 + reasons: ['Moderate node changes', 'No orphan nodes', 'Limited risk escalations'], 213 + canApply: true 214 + }; 215 + this.emit('upgrade-classified', decision); 216 + return decision; 217 + } 218 + 219 + // Default to rejection if no clear classification 220 + const decision: UpgradeDecision = { 221 + classification: 'reject', 222 + metrics, 223 + reasons: ['Does not meet safe or compaction criteria'], 224 + canApply: false 225 + }; 226 + this.emit('upgrade-classified', decision); 227 + return decision; 228 + } 229 + 230 + acceptUpgrade(comparison: ShadowComparison): void { 231 + const comparisonId = this.generateComparisonId(comparison); 232 + 233 + if (this.acceptedUpgrades.has(comparisonId)) { 234 + throw new Error('Upgrade has already been accepted'); 235 + } 236 + 237 + const decision = this.classifyUpgrade(comparison.metrics); 238 + if (!decision.canApply) { 239 + throw new Error(`Cannot accept upgrade classified as: ${decision.classification}`); 240 + } 241 + 242 + this.acceptedUpgrades.add(comparisonId); 243 + this.emit('upgrade-accepted', comparison); 244 + } 245 + 246 + private generateComparisonId(comparison: ShadowComparison): string { 247 + const content = JSON.stringify({ 248 + timestamp: comparison.timestamp, 249 + metrics: comparison.metrics, 250 + oldNodeCount: comparison.oldResult.nodes.size, 251 + newNodeCount: comparison.newResult.nodes.size 252 + }); 253 + 254 + // Simple hash function for ID generation 255 + let hash = 0; 256 + for (let i = 0; i < content.length; i++) { 257 + const char = content.charCodeAt(i); 258 + hash = ((hash << 5) - hash) + char; 259 + hash = hash & hash; // Convert to 32-bit integer 260 + } 261 + 262 + return Math.abs(hash).toString(16); 263 + } 264 + } 265 + 266 + export function createShadowCanonicalizer(): ShadowCanonicalizer { 267 + return new ShadowCanonicalizerImpl(); 268 + } 269 + 270 + /** @internal Phoenix VCS traceability — do not remove. */ 271 + export const _phoenix = { 272 + iu_id: 'b3c65ef5f97570cc447b02fc1c1203db4af0842c10c8bc5a04237e195ad2a74b', 273 + name: 'Shadow Canonicalization', 274 + risk_tier: 'high', 275 + canon_ids: [6 as const], 276 + } as const;
+41 -13
examples/phoenix-self/src/generated/implementation/__tests__/implementation.test.ts
··· 8 8 import { describe, it, expect, afterAll } from 'vitest'; 9 9 import { startServer } from '../server.js'; 10 10 11 - import * as iuManager from '../iu-manager.js'; 12 - import * as boundaryValidator from '../boundary-validator.js'; 11 + import * as architecturalLinter from '../architectural-linter.js'; 12 + import * as boundaryPolicySchema from '../boundary-policy-schema.js'; 13 + import * as implementationUnitStructure from '../implementation-unit-structure.js'; 14 + import * as regenerationEngine from '../regeneration-engine.js'; 13 15 14 16 describe('Implementation modules', () => { 15 - describe('IU Manager', () => { 17 + describe('Architectural Linter', () => { 16 18 it('exports Phoenix traceability metadata', () => { 17 - expect(iuManager._phoenix).toBeDefined(); 18 - expect(iuManager._phoenix.name).toBe('IU Manager'); 19 - expect(iuManager._phoenix.risk_tier).toBeTruthy(); 19 + expect(architecturalLinter._phoenix).toBeDefined(); 20 + expect(architecturalLinter._phoenix.name).toBe('Architectural Linter'); 21 + expect(architecturalLinter._phoenix.risk_tier).toBeTruthy(); 20 22 }); 21 23 22 24 it('has exported functions', () => { 23 - const exports = Object.keys(iuManager).filter(k => k !== '_phoenix'); 25 + const exports = Object.keys(architecturalLinter).filter(k => k !== '_phoenix'); 24 26 expect(exports.length).toBeGreaterThan(0); 25 27 }); 26 28 }); 27 29 28 - describe('Boundary Validator', () => { 30 + describe('Boundary Policy Schema', () => { 29 31 it('exports Phoenix traceability metadata', () => { 30 - expect(boundaryValidator._phoenix).toBeDefined(); 31 - expect(boundaryValidator._phoenix.name).toBe('Boundary Validator'); 32 - expect(boundaryValidator._phoenix.risk_tier).toBeTruthy(); 32 + expect(boundaryPolicySchema._phoenix).toBeDefined(); 33 + expect(boundaryPolicySchema._phoenix.name).toBe('Boundary Policy Schema'); 34 + expect(boundaryPolicySchema._phoenix.risk_tier).toBeTruthy(); 33 35 }); 34 36 35 37 it('has exported functions', () => { 36 - const exports = Object.keys(boundaryValidator).filter(k => k !== '_phoenix'); 38 + const exports = Object.keys(boundaryPolicySchema).filter(k => k !== '_phoenix'); 39 + expect(exports.length).toBeGreaterThan(0); 40 + }); 41 + }); 42 + 43 + describe('Implementation Unit Structure', () => { 44 + it('exports Phoenix traceability metadata', () => { 45 + expect(implementationUnitStructure._phoenix).toBeDefined(); 46 + expect(implementationUnitStructure._phoenix.name).toBe('Implementation Unit Structure'); 47 + expect(implementationUnitStructure._phoenix.risk_tier).toBeTruthy(); 48 + }); 49 + 50 + it('has exported functions', () => { 51 + const exports = Object.keys(implementationUnitStructure).filter(k => k !== '_phoenix'); 52 + expect(exports.length).toBeGreaterThan(0); 53 + }); 54 + }); 55 + 56 + describe('Regeneration Engine', () => { 57 + it('exports Phoenix traceability metadata', () => { 58 + expect(regenerationEngine._phoenix).toBeDefined(); 59 + expect(regenerationEngine._phoenix.name).toBe('Regeneration Engine'); 60 + expect(regenerationEngine._phoenix.risk_tier).toBeTruthy(); 61 + }); 62 + 63 + it('has exported functions', () => { 64 + const exports = Object.keys(regenerationEngine).filter(k => k !== '_phoenix'); 37 65 expect(exports.length).toBeGreaterThan(0); 38 66 }); 39 67 }); ··· 67 95 const res = await fetch(`http://localhost:${instance.port}/modules`); 68 96 expect(res.status).toBe(200); 69 97 const body = await res.json() as Array<Record<string, unknown>>; 70 - expect(body.length).toBe(2); 98 + expect(body.length).toBe(4); 71 99 }); 72 100 73 101 it('GET /unknown returns 404', async () => {
+321
examples/phoenix-self/src/generated/implementation/architectural-linter.ts
··· 1 + export interface DependencyNode { 2 + readonly id: string; 3 + readonly type: 'module' | 'function' | 'class' | 'interface' | 'type'; 4 + readonly sourceFile: string; 5 + readonly line: number; 6 + readonly column: number; 7 + } 8 + 9 + export interface DependencyEdge { 10 + readonly from: DependencyNode; 11 + readonly to: DependencyNode; 12 + readonly type: 'import' | 'call' | 'extends' | 'implements' | 'reference'; 13 + readonly sourceFile: string; 14 + readonly line: number; 15 + readonly column: number; 16 + } 17 + 18 + export interface DependencyGraph { 19 + readonly nodes: ReadonlyMap<string, DependencyNode>; 20 + readonly edges: readonly DependencyEdge[]; 21 + } 22 + 23 + export interface BoundaryPolicy { 24 + readonly allowedDependencies: readonly string[]; 25 + readonly forbiddenDependencies: readonly string[]; 26 + readonly sideChannelRestrictions: readonly string[]; 27 + } 28 + 29 + export interface ImplementationUnit { 30 + readonly id: string; 31 + readonly name: string; 32 + readonly boundaryPolicy: BoundaryPolicy; 33 + readonly sourceFiles: readonly string[]; 34 + } 35 + 36 + export type DiagnosticSeverity = 'error' | 'warning'; 37 + 38 + export interface Diagnostic { 39 + readonly type: 'boundary_violation' | 'side_channel_violation'; 40 + readonly severity: DiagnosticSeverity; 41 + readonly message: string; 42 + readonly sourceFile: string; 43 + readonly line: number; 44 + readonly column: number; 45 + readonly violatingDependency: string; 46 + readonly implementationUnit: string; 47 + } 48 + 49 + export interface LinterConfig { 50 + readonly boundaryViolationSeverity: DiagnosticSeverity; 51 + readonly sideChannelViolationSeverity: DiagnosticSeverity; 52 + } 53 + 54 + export class ArchitecturalLinter { 55 + private readonly config: LinterConfig; 56 + 57 + constructor(config: LinterConfig) { 58 + this.config = config; 59 + } 60 + 61 + extractDependencyGraph(generatedCode: Map<string, string>): DependencyGraph { 62 + const nodes = new Map<string, DependencyNode>(); 63 + const edges: DependencyEdge[] = []; 64 + 65 + for (const [filePath, content] of generatedCode) { 66 + this.parseFileForDependencies(filePath, content, nodes, edges); 67 + } 68 + 69 + return { 70 + nodes, 71 + edges 72 + }; 73 + } 74 + 75 + private parseFileForDependencies( 76 + filePath: string, 77 + content: string, 78 + nodes: Map<string, DependencyNode>, 79 + edges: DependencyEdge[] 80 + ): void { 81 + const lines = content.split('\n'); 82 + 83 + for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { 84 + const line = lines[lineIndex]; 85 + const lineNumber = lineIndex + 1; 86 + 87 + // Parse imports 88 + const importMatch = line.match(/^import\s+.*?\s+from\s+['"]([^'"]+)['"]/); 89 + if (importMatch) { 90 + const importPath = importMatch[1]; 91 + const column = line.indexOf(importPath); 92 + 93 + const fromNode = this.getOrCreateNode(filePath, 'module', filePath, lineNumber, 0, nodes); 94 + const toNode = this.getOrCreateNode(importPath, 'module', importPath, 1, 0, nodes); 95 + 96 + edges.push({ 97 + from: fromNode, 98 + to: toNode, 99 + type: 'import', 100 + sourceFile: filePath, 101 + line: lineNumber, 102 + column 103 + }); 104 + } 105 + 106 + // Parse function calls 107 + const callMatches = line.matchAll(/(\w+)\s*\(/g); 108 + for (const match of callMatches) { 109 + const functionName = match[1]; 110 + const column = match.index || 0; 111 + 112 + if (this.isExternalDependency(functionName)) { 113 + const fromNode = this.getOrCreateNode(filePath, 'module', filePath, lineNumber, 0, nodes); 114 + const toNode = this.getOrCreateNode(functionName, 'function', 'external', 1, 0, nodes); 115 + 116 + edges.push({ 117 + from: fromNode, 118 + to: toNode, 119 + type: 'call', 120 + sourceFile: filePath, 121 + line: lineNumber, 122 + column 123 + }); 124 + } 125 + } 126 + 127 + // Parse class extensions 128 + const extendsMatch = line.match(/class\s+\w+\s+extends\s+(\w+)/); 129 + if (extendsMatch) { 130 + const baseClass = extendsMatch[1]; 131 + const column = line.indexOf(baseClass); 132 + 133 + const fromNode = this.getOrCreateNode(filePath, 'module', filePath, lineNumber, 0, nodes); 134 + const toNode = this.getOrCreateNode(baseClass, 'class', 'external', 1, 0, nodes); 135 + 136 + edges.push({ 137 + from: fromNode, 138 + to: toNode, 139 + type: 'extends', 140 + sourceFile: filePath, 141 + line: lineNumber, 142 + column 143 + }); 144 + } 145 + 146 + // Parse interface implementations 147 + const implementsMatch = line.match(/class\s+\w+\s+implements\s+(\w+)/); 148 + if (implementsMatch) { 149 + const interfaceName = implementsMatch[1]; 150 + const column = line.indexOf(interfaceName); 151 + 152 + const fromNode = this.getOrCreateNode(filePath, 'module', filePath, lineNumber, 0, nodes); 153 + const toNode = this.getOrCreateNode(interfaceName, 'interface', 'external', 1, 0, nodes); 154 + 155 + edges.push({ 156 + from: fromNode, 157 + to: toNode, 158 + type: 'implements', 159 + sourceFile: filePath, 160 + line: lineNumber, 161 + column 162 + }); 163 + } 164 + } 165 + } 166 + 167 + private getOrCreateNode( 168 + id: string, 169 + type: DependencyNode['type'], 170 + sourceFile: string, 171 + line: number, 172 + column: number, 173 + nodes: Map<string, DependencyNode> 174 + ): DependencyNode { 175 + const existing = nodes.get(id); 176 + if (existing) { 177 + return existing; 178 + } 179 + 180 + const node: DependencyNode = { 181 + id, 182 + type, 183 + sourceFile, 184 + line, 185 + column 186 + }; 187 + 188 + nodes.set(id, node); 189 + return node; 190 + } 191 + 192 + private isExternalDependency(name: string): boolean { 193 + return /^[A-Z]/.test(name) || name.includes('.'); 194 + } 195 + 196 + validateDependencies( 197 + dependencyGraph: DependencyGraph, 198 + implementationUnit: ImplementationUnit 199 + ): readonly Diagnostic[] { 200 + const diagnostics: Diagnostic[] = []; 201 + 202 + for (const edge of dependencyGraph.edges) { 203 + const violation = this.checkBoundaryViolation(edge, implementationUnit); 204 + if (violation) { 205 + diagnostics.push(violation); 206 + } 207 + 208 + const sideChannelViolation = this.checkSideChannelViolation(edge, implementationUnit); 209 + if (sideChannelViolation) { 210 + diagnostics.push(sideChannelViolation); 211 + } 212 + } 213 + 214 + return diagnostics; 215 + } 216 + 217 + private checkBoundaryViolation( 218 + edge: DependencyEdge, 219 + implementationUnit: ImplementationUnit 220 + ): Diagnostic | null { 221 + const targetId = edge.to.id; 222 + const policy = implementationUnit.boundaryPolicy; 223 + 224 + // Check if dependency is explicitly forbidden 225 + if (policy.forbiddenDependencies.includes(targetId)) { 226 + return { 227 + type: 'boundary_violation', 228 + severity: this.config.boundaryViolationSeverity, 229 + message: `Forbidden dependency on '${targetId}' detected`, 230 + sourceFile: edge.sourceFile, 231 + line: edge.line, 232 + column: edge.column, 233 + violatingDependency: targetId, 234 + implementationUnit: implementationUnit.id 235 + }; 236 + } 237 + 238 + // Check if dependency is not in allowed list (if allowlist is non-empty) 239 + if (policy.allowedDependencies.length > 0 && !policy.allowedDependencies.includes(targetId)) { 240 + // Allow internal dependencies within the same implementation unit 241 + if (!this.isInternalDependency(targetId, implementationUnit)) { 242 + return { 243 + type: 'boundary_violation', 244 + severity: this.config.boundaryViolationSeverity, 245 + message: `Dependency on '${targetId}' not in allowed list`, 246 + sourceFile: edge.sourceFile, 247 + line: edge.line, 248 + column: edge.column, 249 + violatingDependency: targetId, 250 + implementationUnit: implementationUnit.id 251 + }; 252 + } 253 + } 254 + 255 + return null; 256 + } 257 + 258 + private checkSideChannelViolation( 259 + edge: DependencyEdge, 260 + implementationUnit: ImplementationUnit 261 + ): Diagnostic | null { 262 + const targetId = edge.to.id; 263 + const policy = implementationUnit.boundaryPolicy; 264 + 265 + for (const restriction of policy.sideChannelRestrictions) { 266 + if (targetId.includes(restriction) || targetId.match(new RegExp(restriction))) { 267 + return { 268 + type: 'side_channel_violation', 269 + severity: this.config.sideChannelViolationSeverity, 270 + message: `Side-channel violation: dependency on '${targetId}' matches restriction '${restriction}'`, 271 + sourceFile: edge.sourceFile, 272 + line: edge.line, 273 + column: edge.column, 274 + violatingDependency: targetId, 275 + implementationUnit: implementationUnit.id 276 + }; 277 + } 278 + } 279 + 280 + return null; 281 + } 282 + 283 + private isInternalDependency(targetId: string, implementationUnit: ImplementationUnit): boolean { 284 + return implementationUnit.sourceFiles.some(file => 285 + targetId.startsWith(file) || targetId.includes(implementationUnit.name) 286 + ); 287 + } 288 + 289 + lint( 290 + generatedCode: Map<string, string>, 291 + implementationUnit: ImplementationUnit 292 + ): readonly Diagnostic[] { 293 + const dependencyGraph = this.extractDependencyGraph(generatedCode); 294 + const diagnostics = this.validateDependencies(dependencyGraph, implementationUnit); 295 + 296 + // Invariant: Never silently ignore boundary violations 297 + const boundaryViolations = diagnostics.filter(d => d.type === 'boundary_violation'); 298 + if (boundaryViolations.length > 0) { 299 + // Log or emit the violations - they must not be ignored 300 + for (const violation of boundaryViolations) { 301 + if (violation.severity === 'error') { 302 + throw new Error(`Boundary violation detected: ${violation.message} at ${violation.sourceFile}:${violation.line}:${violation.column}`); 303 + } 304 + } 305 + } 306 + 307 + return diagnostics; 308 + } 309 + } 310 + 311 + export function createArchitecturalLinter(config: LinterConfig): ArchitecturalLinter { 312 + return new ArchitecturalLinter(config); 313 + } 314 + 315 + /** @internal Phoenix VCS traceability — do not remove. */ 316 + export const _phoenix = { 317 + iu_id: 'a1110ed578638325563de998e28b41d11e2c3057fb0b8e2a87ba55889e0607af', 318 + name: 'Architectural Linter', 319 + risk_tier: 'high', 320 + canon_ids: [5 as const], 321 + } as const;
+300
examples/phoenix-self/src/generated/implementation/boundary-policy-schema.ts
··· 1 + export interface SideChannelDependencies { 2 + databases?: string[]; 3 + queues?: string[]; 4 + caches?: string[]; 5 + config?: string[]; 6 + external_apis?: string[]; 7 + files?: string[]; 8 + } 9 + 10 + export interface CodeDependencies { 11 + allowed_ius?: string[]; 12 + allowed_packages?: string[]; 13 + forbidden_ius?: string[]; 14 + forbidden_packages?: string[]; 15 + forbidden_paths?: string[]; 16 + } 17 + 18 + export interface BoundaryPolicy { 19 + iu_id: string; 20 + name: string; 21 + side_channels: SideChannelDependencies; 22 + code_dependencies: CodeDependencies; 23 + } 24 + 25 + export interface InvalidationEdge { 26 + from_iu: string; 27 + to_iu: string; 28 + dependency_type: keyof SideChannelDependencies; 29 + resource_name: string; 30 + } 31 + 32 + export interface BoundaryPolicyConfig { 33 + policies: BoundaryPolicy[]; 34 + global_allowed_packages?: string[]; 35 + global_forbidden_packages?: string[]; 36 + global_forbidden_paths?: string[]; 37 + } 38 + 39 + export class BoundaryPolicyValidator { 40 + private policies: Map<string, BoundaryPolicy> = new Map(); 41 + private globalConfig: Omit<BoundaryPolicyConfig, 'policies'>; 42 + 43 + constructor(config: BoundaryPolicyConfig) { 44 + this.globalConfig = { 45 + global_allowed_packages: config.global_allowed_packages || [], 46 + global_forbidden_packages: config.global_forbidden_packages || [], 47 + global_forbidden_paths: config.global_forbidden_paths || [] 48 + }; 49 + 50 + for (const policy of config.policies) { 51 + this.policies.set(policy.iu_id, policy); 52 + } 53 + } 54 + 55 + validatePolicy(policy: BoundaryPolicy): string[] { 56 + const errors: string[] = []; 57 + 58 + if (!policy.iu_id || typeof policy.iu_id !== 'string') { 59 + errors.push('Policy must have a valid iu_id'); 60 + } 61 + 62 + if (!policy.name || typeof policy.name !== 'string') { 63 + errors.push('Policy must have a valid name'); 64 + } 65 + 66 + if (policy.side_channels) { 67 + this.validateSideChannels(policy.side_channels, errors); 68 + } 69 + 70 + if (policy.code_dependencies) { 71 + this.validateCodeDependencies(policy.code_dependencies, errors); 72 + } 73 + 74 + return errors; 75 + } 76 + 77 + private validateSideChannels(sideChannels: SideChannelDependencies, errors: string[]): void { 78 + const validKeys: (keyof SideChannelDependencies)[] = [ 79 + 'databases', 'queues', 'caches', 'config', 'external_apis', 'files' 80 + ]; 81 + 82 + for (const [key, value] of Object.entries(sideChannels)) { 83 + if (!validKeys.includes(key as keyof SideChannelDependencies)) { 84 + errors.push(`Invalid side channel dependency type: ${key}`); 85 + continue; 86 + } 87 + 88 + if (value && !Array.isArray(value)) { 89 + errors.push(`Side channel dependency '${key}' must be an array`); 90 + continue; 91 + } 92 + 93 + if (value) { 94 + for (const item of value) { 95 + if (typeof item !== 'string' || item.trim() === '') { 96 + errors.push(`Side channel dependency '${key}' contains invalid resource name`); 97 + } 98 + } 99 + } 100 + } 101 + } 102 + 103 + private validateCodeDependencies(codeDeps: CodeDependencies, errors: string[]): void { 104 + const validKeys: (keyof CodeDependencies)[] = [ 105 + 'allowed_ius', 'allowed_packages', 'forbidden_ius', 'forbidden_packages', 'forbidden_paths' 106 + ]; 107 + 108 + for (const [key, value] of Object.entries(codeDeps)) { 109 + if (!validKeys.includes(key as keyof CodeDependencies)) { 110 + errors.push(`Invalid code dependency type: ${key}`); 111 + continue; 112 + } 113 + 114 + if (value && !Array.isArray(value)) { 115 + errors.push(`Code dependency '${key}' must be an array`); 116 + continue; 117 + } 118 + 119 + if (value) { 120 + for (const item of value) { 121 + if (typeof item !== 'string' || item.trim() === '') { 122 + errors.push(`Code dependency '${key}' contains invalid item`); 123 + } 124 + } 125 + } 126 + } 127 + 128 + if (codeDeps.allowed_ius && codeDeps.forbidden_ius) { 129 + const allowed = new Set(codeDeps.allowed_ius); 130 + const forbidden = new Set(codeDeps.forbidden_ius); 131 + const conflicts = [...allowed].filter(iu => forbidden.has(iu)); 132 + if (conflicts.length > 0) { 133 + errors.push(`IUs cannot be both allowed and forbidden: ${conflicts.join(', ')}`); 134 + } 135 + } 136 + 137 + if (codeDeps.allowed_packages && codeDeps.forbidden_packages) { 138 + const allowed = new Set(codeDeps.allowed_packages); 139 + const forbidden = new Set(codeDeps.forbidden_packages); 140 + const conflicts = [...allowed].filter(pkg => forbidden.has(pkg)); 141 + if (conflicts.length > 0) { 142 + errors.push(`Packages cannot be both allowed and forbidden: ${conflicts.join(', ')}`); 143 + } 144 + } 145 + } 146 + 147 + generateInvalidationGraph(): InvalidationEdge[] { 148 + const edges: InvalidationEdge[] = []; 149 + const resourceToIUs = new Map<string, Set<string>>(); 150 + 151 + for (const policy of this.policies.values()) { 152 + if (!policy.side_channels) continue; 153 + 154 + for (const [depType, resources] of Object.entries(policy.side_channels)) { 155 + if (!resources) continue; 156 + 157 + for (const resource of resources) { 158 + const key = `${depType}:${resource}`; 159 + if (!resourceToIUs.has(key)) { 160 + resourceToIUs.set(key, new Set()); 161 + } 162 + resourceToIUs.get(key)!.add(policy.iu_id); 163 + } 164 + } 165 + } 166 + 167 + for (const [resourceKey, ius] of resourceToIUs.entries()) { 168 + const [depType, resourceName] = resourceKey.split(':', 2); 169 + const iuList = Array.from(ius); 170 + 171 + for (let i = 0; i < iuList.length; i++) { 172 + for (let j = i + 1; j < iuList.length; j++) { 173 + edges.push({ 174 + from_iu: iuList[i], 175 + to_iu: iuList[j], 176 + dependency_type: depType as keyof SideChannelDependencies, 177 + resource_name: resourceName 178 + }); 179 + edges.push({ 180 + from_iu: iuList[j], 181 + to_iu: iuList[i], 182 + dependency_type: depType as keyof SideChannelDependencies, 183 + resource_name: resourceName 184 + }); 185 + } 186 + } 187 + } 188 + 189 + return edges; 190 + } 191 + 192 + checkCodeDependencyViolation(fromIU: string, toIU: string, packageName?: string, filePath?: string): string[] { 193 + const policy = this.policies.get(fromIU); 194 + if (!policy) { 195 + return [`No boundary policy found for IU: ${fromIU}`]; 196 + } 197 + 198 + const violations: string[] = []; 199 + const codeDeps = policy.code_dependencies; 200 + 201 + if (!codeDeps) { 202 + return violations; 203 + } 204 + 205 + if (toIU && codeDeps.forbidden_ius?.includes(toIU)) { 206 + violations.push(`IU ${fromIU} is forbidden from depending on IU ${toIU}`); 207 + } 208 + 209 + if (toIU && codeDeps.allowed_ius && !codeDeps.allowed_ius.includes(toIU)) { 210 + violations.push(`IU ${fromIU} is not allowed to depend on IU ${toIU}`); 211 + } 212 + 213 + if (packageName) { 214 + if (codeDeps.forbidden_packages?.includes(packageName) || 215 + this.globalConfig.global_forbidden_packages?.includes(packageName)) { 216 + violations.push(`IU ${fromIU} is forbidden from using package ${packageName}`); 217 + } 218 + 219 + const allowedPackages = [ 220 + ...(codeDeps.allowed_packages || []), 221 + ...(this.globalConfig.global_allowed_packages || []) 222 + ]; 223 + 224 + if (allowedPackages.length > 0 && !allowedPackages.includes(packageName)) { 225 + violations.push(`IU ${fromIU} is not allowed to use package ${packageName}`); 226 + } 227 + } 228 + 229 + if (filePath) { 230 + const forbiddenPaths = [ 231 + ...(codeDeps.forbidden_paths || []), 232 + ...(this.globalConfig.global_forbidden_paths || []) 233 + ]; 234 + 235 + for (const forbiddenPath of forbiddenPaths) { 236 + if (filePath.includes(forbiddenPath)) { 237 + violations.push(`IU ${fromIU} is forbidden from accessing path ${filePath}`); 238 + } 239 + } 240 + } 241 + 242 + return violations; 243 + } 244 + 245 + getPolicy(iuId: string): BoundaryPolicy | undefined { 246 + return this.policies.get(iuId); 247 + } 248 + 249 + getAllPolicies(): BoundaryPolicy[] { 250 + return Array.from(this.policies.values()); 251 + } 252 + 253 + updatePolicy(policy: BoundaryPolicy): string[] { 254 + const errors = this.validatePolicy(policy); 255 + if (errors.length === 0) { 256 + this.policies.set(policy.iu_id, policy); 257 + } 258 + return errors; 259 + } 260 + 261 + removePolicy(iuId: string): boolean { 262 + return this.policies.delete(iuId); 263 + } 264 + } 265 + 266 + export function createBoundaryPolicyValidator(config: BoundaryPolicyConfig): BoundaryPolicyValidator { 267 + return new BoundaryPolicyValidator(config); 268 + } 269 + 270 + export function validateBoundaryPolicyConfig(config: BoundaryPolicyConfig): string[] { 271 + const errors: string[] = []; 272 + 273 + if (!config.policies || !Array.isArray(config.policies)) { 274 + errors.push('Config must have a policies array'); 275 + return errors; 276 + } 277 + 278 + const validator = new BoundaryPolicyValidator(config); 279 + const seenIds = new Set<string>(); 280 + 281 + for (const policy of config.policies) { 282 + if (seenIds.has(policy.iu_id)) { 283 + errors.push(`Duplicate IU ID found: ${policy.iu_id}`); 284 + } 285 + seenIds.add(policy.iu_id); 286 + 287 + const policyErrors = validator.validatePolicy(policy); 288 + errors.push(...policyErrors); 289 + } 290 + 291 + return errors; 292 + } 293 + 294 + /** @internal Phoenix VCS traceability — do not remove. */ 295 + export const _phoenix = { 296 + iu_id: '0cefa9e3bdf727c5ddbc7222f2f1522bed6bd08158e7425c2deb730162d07cda', 297 + name: 'Boundary Policy Schema', 298 + risk_tier: 'high', 299 + canon_ids: [4 as const], 300 + } as const;
+204
examples/phoenix-self/src/generated/implementation/implementation-unit-structure.ts
··· 1 + import { createHash } from 'node:crypto'; 2 + 3 + export type RiskTier = 'low' | 'medium' | 'high' | 'critical'; 4 + 5 + export interface CanonicalNode { 6 + id: number; 7 + name: string; 8 + description: string; 9 + } 10 + 11 + export interface IUContract { 12 + canonical_nodes: readonly CanonicalNode[]; 13 + implements: readonly number[]; 14 + } 15 + 16 + export interface IUProposal { 17 + id: string; 18 + name: string; 19 + risk_tier: RiskTier; 20 + contract: IUContract; 21 + content_hash: string; 22 + proposed_by: 'bot' | 'human'; 23 + proposed_at: Date; 24 + status: 'pending' | 'accepted' | 'rejected'; 25 + accepted_by?: string; 26 + accepted_at?: Date; 27 + } 28 + 29 + export interface ImplementationUnit { 30 + id: string; 31 + name: string; 32 + risk_tier: RiskTier; 33 + contract: IUContract; 34 + content_hash: string; 35 + created_at: Date; 36 + created_by: string; 37 + } 38 + 39 + export function generateIUId(name: string, contract: IUContract, content: string): string { 40 + const contractData = JSON.stringify({ 41 + name, 42 + implements: contract.implements.slice().sort(), 43 + canonical_nodes: contract.canonical_nodes.map(n => ({ id: n.id, name: n.name })).sort((a, b) => a.id - b.id) 44 + }); 45 + 46 + const hash = createHash('sha256'); 47 + hash.update(contractData); 48 + hash.update(content); 49 + return hash.digest('hex'); 50 + } 51 + 52 + export function validateRiskTier(tier: string): tier is RiskTier { 53 + return ['low', 'medium', 'high', 'critical'].includes(tier); 54 + } 55 + 56 + export function validateContract(contract: IUContract): { valid: boolean; errors: string[] } { 57 + const errors: string[] = []; 58 + 59 + if (!contract.canonical_nodes || contract.canonical_nodes.length === 0) { 60 + errors.push('Contract must declare at least one canonical node'); 61 + } 62 + 63 + if (!contract.implements || contract.implements.length === 0) { 64 + errors.push('Contract must implement at least one canonical node ID'); 65 + } 66 + 67 + const declaredIds = new Set(contract.canonical_nodes.map(n => n.id)); 68 + const implementedIds = new Set(contract.implements); 69 + 70 + for (const implId of implementedIds) { 71 + if (!declaredIds.has(implId)) { 72 + errors.push(`Contract implements canonical node ${implId} but does not declare it`); 73 + } 74 + } 75 + 76 + const duplicateIds = contract.canonical_nodes 77 + .map(n => n.id) 78 + .filter((id, index, arr) => arr.indexOf(id) !== index); 79 + 80 + if (duplicateIds.length > 0) { 81 + errors.push(`Duplicate canonical node IDs: ${duplicateIds.join(', ')}`); 82 + } 83 + 84 + return { valid: errors.length === 0, errors }; 85 + } 86 + 87 + export class IUManager { 88 + private units = new Map<string, ImplementationUnit>(); 89 + private proposals = new Map<string, IUProposal>(); 90 + 91 + proposeIU( 92 + name: string, 93 + riskTier: RiskTier, 94 + contract: IUContract, 95 + content: string, 96 + proposedBy: 'bot' | 'human' = 'bot' 97 + ): { success: boolean; proposal?: IUProposal; errors?: string[] } { 98 + const contractValidation = validateContract(contract); 99 + if (!contractValidation.valid) { 100 + return { success: false, errors: contractValidation.errors }; 101 + } 102 + 103 + if (!validateRiskTier(riskTier)) { 104 + return { success: false, errors: ['Invalid risk tier'] }; 105 + } 106 + 107 + const contentHash = createHash('sha256').update(content).digest('hex'); 108 + const id = generateIUId(name, contract, content); 109 + 110 + const proposal: IUProposal = { 111 + id, 112 + name, 113 + risk_tier: riskTier, 114 + contract, 115 + content_hash: contentHash, 116 + proposed_by: proposedBy, 117 + proposed_at: new Date(), 118 + status: 'pending' 119 + }; 120 + 121 + this.proposals.set(id, proposal); 122 + return { success: true, proposal }; 123 + } 124 + 125 + acceptProposal( 126 + proposalId: string, 127 + acceptedBy: string 128 + ): { success: boolean; unit?: ImplementationUnit; errors?: string[] } { 129 + const proposal = this.proposals.get(proposalId); 130 + if (!proposal) { 131 + return { success: false, errors: ['Proposal not found'] }; 132 + } 133 + 134 + if (proposal.status !== 'pending') { 135 + return { success: false, errors: ['Proposal is not pending'] }; 136 + } 137 + 138 + if (proposal.proposed_by === 'bot' && !acceptedBy) { 139 + return { success: false, errors: ['Bot proposals require human or policy acceptance'] }; 140 + } 141 + 142 + proposal.status = 'accepted'; 143 + proposal.accepted_by = acceptedBy; 144 + proposal.accepted_at = new Date(); 145 + 146 + const unit: ImplementationUnit = { 147 + id: proposal.id, 148 + name: proposal.name, 149 + risk_tier: proposal.risk_tier, 150 + contract: proposal.contract, 151 + content_hash: proposal.content_hash, 152 + created_at: proposal.accepted_at, 153 + created_by: acceptedBy 154 + }; 155 + 156 + this.units.set(unit.id, unit); 157 + return { success: true, unit }; 158 + } 159 + 160 + rejectProposal(proposalId: string): { success: boolean; errors?: string[] } { 161 + const proposal = this.proposals.get(proposalId); 162 + if (!proposal) { 163 + return { success: false, errors: ['Proposal not found'] }; 164 + } 165 + 166 + if (proposal.status !== 'pending') { 167 + return { success: false, errors: ['Proposal is not pending'] }; 168 + } 169 + 170 + proposal.status = 'rejected'; 171 + return { success: true }; 172 + } 173 + 174 + getUnit(id: string): ImplementationUnit | undefined { 175 + return this.units.get(id); 176 + } 177 + 178 + getProposal(id: string): IUProposal | undefined { 179 + return this.proposals.get(id); 180 + } 181 + 182 + listUnits(): ImplementationUnit[] { 183 + return Array.from(this.units.values()); 184 + } 185 + 186 + listProposals(status?: IUProposal['status']): IUProposal[] { 187 + const proposals = Array.from(this.proposals.values()); 188 + return status ? proposals.filter(p => p.status === status) : proposals; 189 + } 190 + 191 + findUnitsByCanonicalNode(canonicalNodeId: number): ImplementationUnit[] { 192 + return this.listUnits().filter(unit => 193 + unit.contract.implements.includes(canonicalNodeId) 194 + ); 195 + } 196 + } 197 + 198 + /** @internal Phoenix VCS traceability — do not remove. */ 199 + export const _phoenix = { 200 + iu_id: '053d72d822014788abd21f50518ef2c02ae9a958bb653ca872ad5773d2bd260c', 201 + name: 'Implementation Unit Structure', 202 + risk_tier: 'medium', 203 + canon_ids: [5 as const], 204 + } as const;
+11 -2
examples/phoenix-self/src/generated/implementation/index.ts
··· 1 - export * as iuManager from './iu-manager.js'; 2 - export * as boundaryValidator from './boundary-validator.js'; 1 + /** 2 + * Implementation 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Barrel export for all Implementation modules. 6 + */ 7 + 8 + export * as architecturalLinter from './architectural-linter.js'; 9 + export * as boundaryPolicySchema from './boundary-policy-schema.js'; 10 + export * as implementationUnitStructure from './implementation-unit-structure.js'; 11 + export * as regenerationEngine from './regeneration-engine.js';
+277
examples/phoenix-self/src/generated/implementation/regeneration-engine.ts
··· 1 + import { createHash } from 'node:crypto'; 2 + import { readFile, writeFile, mkdir, stat } from 'node:fs/promises'; 3 + import { join, dirname } from 'node:path'; 4 + 5 + export interface ImplementationUnit { 6 + id: string; 7 + path: string; 8 + content: string; 9 + dependencies: string[]; 10 + lastModified: number; 11 + } 12 + 13 + export interface RegenerationRecord { 14 + model_id: string; 15 + promptpack_hash: string; 16 + toolchain_version: string; 17 + normalization_steps: string[]; 18 + timestamp: number; 19 + iu_id: string; 20 + } 21 + 22 + export interface GeneratedArtifact { 23 + path: string; 24 + content: string; 25 + source_iu_id: string; 26 + } 27 + 28 + export interface FileHash { 29 + path: string; 30 + hash: string; 31 + } 32 + 33 + export interface IuHash { 34 + iu_id: string; 35 + hash: string; 36 + artifacts: FileHash[]; 37 + } 38 + 39 + export interface GeneratedManifest { 40 + version: string; 41 + timestamp: number; 42 + file_hashes: FileHash[]; 43 + iu_hashes: IuHash[]; 44 + regeneration_records: RegenerationRecord[]; 45 + } 46 + 47 + export interface RegenerationInput { 48 + model_id: string; 49 + promptpack_hash: string; 50 + toolchain_version: string; 51 + normalization_steps: string[]; 52 + ius: ImplementationUnit[]; 53 + generator: (iu: ImplementationUnit) => Promise<GeneratedArtifact[]>; 54 + } 55 + 56 + export class RegenerationEngine { 57 + private manifestPath: string; 58 + private manifest: GeneratedManifest; 59 + 60 + constructor(projectRoot: string) { 61 + this.manifestPath = join(projectRoot, '.phoenix', 'generated_manifest'); 62 + this.manifest = { 63 + version: '1.0.0', 64 + timestamp: 0, 65 + file_hashes: [], 66 + iu_hashes: [], 67 + regeneration_records: [] 68 + }; 69 + } 70 + 71 + async loadManifest(): Promise<void> { 72 + try { 73 + const content = await readFile(this.manifestPath, 'utf-8'); 74 + this.manifest = JSON.parse(content); 75 + } catch (error) { 76 + // Manifest doesn't exist or is invalid, use default 77 + this.manifest = { 78 + version: '1.0.0', 79 + timestamp: 0, 80 + file_hashes: [], 81 + iu_hashes: [], 82 + regeneration_records: [] 83 + }; 84 + } 85 + } 86 + 87 + async saveManifest(): Promise<void> { 88 + await mkdir(dirname(this.manifestPath), { recursive: true }); 89 + await writeFile(this.manifestPath, JSON.stringify(this.manifest, null, 2)); 90 + } 91 + 92 + private computeHash(content: string): string { 93 + return createHash('sha256').update(content).digest('hex'); 94 + } 95 + 96 + private computeIuInputHash(iu: ImplementationUnit, input: RegenerationInput): string { 97 + const inputData = { 98 + iu_id: iu.id, 99 + content: iu.content, 100 + dependencies: iu.dependencies.sort(), 101 + model_id: input.model_id, 102 + promptpack_hash: input.promptpack_hash, 103 + toolchain_version: input.toolchain_version, 104 + normalization_steps: input.normalization_steps 105 + }; 106 + return this.computeHash(JSON.stringify(inputData)); 107 + } 108 + 109 + private isIuInvalidated(iu: ImplementationUnit, input: RegenerationInput): boolean { 110 + const currentInputHash = this.computeIuInputHash(iu, input); 111 + const existingIuHash = this.manifest.iu_hashes.find(h => h.iu_id === iu.id); 112 + 113 + if (!existingIuHash) { 114 + return true; // New IU, needs generation 115 + } 116 + 117 + return existingIuHash.hash !== currentInputHash; 118 + } 119 + 120 + private async fileExists(path: string): Promise<boolean> { 121 + try { 122 + await stat(path); 123 + return true; 124 + } catch { 125 + return false; 126 + } 127 + } 128 + 129 + private async areArtifactsValid(iu: ImplementationUnit): Promise<boolean> { 130 + const existingIuHash = this.manifest.iu_hashes.find(h => h.iu_id === iu.id); 131 + if (!existingIuHash) { 132 + return false; 133 + } 134 + 135 + for (const artifact of existingIuHash.artifacts) { 136 + if (!(await this.fileExists(artifact.path))) { 137 + return false; 138 + } 139 + 140 + try { 141 + const content = await readFile(artifact.path, 'utf-8'); 142 + const currentHash = this.computeHash(content); 143 + if (currentHash !== artifact.hash) { 144 + return false; 145 + } 146 + } catch { 147 + return false; 148 + } 149 + } 150 + 151 + return true; 152 + } 153 + 154 + async regenerate(input: RegenerationInput): Promise<GeneratedArtifact[]> { 155 + await this.loadManifest(); 156 + 157 + const invalidatedIus: ImplementationUnit[] = []; 158 + const allArtifacts: GeneratedArtifact[] = []; 159 + 160 + // Identify invalidated IUs 161 + for (const iu of input.ius) { 162 + const isInvalidated = this.isIuInvalidated(iu, input); 163 + const artifactsValid = await this.areArtifactsValid(iu); 164 + 165 + if (isInvalidated || !artifactsValid) { 166 + invalidatedIus.push(iu); 167 + } 168 + } 169 + 170 + // Regenerate invalidated IUs 171 + for (const iu of invalidatedIus) { 172 + const artifacts = await input.generator(iu); 173 + allArtifacts.push(...artifacts); 174 + 175 + // Write artifacts to disk 176 + for (const artifact of artifacts) { 177 + await mkdir(dirname(artifact.path), { recursive: true }); 178 + await writeFile(artifact.path, artifact.content); 179 + } 180 + 181 + // Update manifest 182 + const inputHash = this.computeIuInputHash(iu, input); 183 + const artifactHashes: FileHash[] = artifacts.map(artifact => ({ 184 + path: artifact.path, 185 + hash: this.computeHash(artifact.content) 186 + })); 187 + 188 + // Remove existing IU hash entry 189 + this.manifest.iu_hashes = this.manifest.iu_hashes.filter(h => h.iu_id !== iu.id); 190 + 191 + // Add new IU hash entry 192 + this.manifest.iu_hashes.push({ 193 + iu_id: iu.id, 194 + hash: inputHash, 195 + artifacts: artifactHashes 196 + }); 197 + 198 + // Update file hashes 199 + for (const artifactHash of artifactHashes) { 200 + this.manifest.file_hashes = this.manifest.file_hashes.filter(h => h.path !== artifactHash.path); 201 + this.manifest.file_hashes.push(artifactHash); 202 + } 203 + 204 + // Record regeneration 205 + const record: RegenerationRecord = { 206 + model_id: input.model_id, 207 + promptpack_hash: input.promptpack_hash, 208 + toolchain_version: input.toolchain_version, 209 + normalization_steps: [...input.normalization_steps], 210 + timestamp: Date.now(), 211 + iu_id: iu.id 212 + }; 213 + 214 + this.manifest.regeneration_records.push(record); 215 + } 216 + 217 + // Update manifest timestamp 218 + this.manifest.timestamp = Date.now(); 219 + 220 + // Save manifest 221 + await this.saveManifest(); 222 + 223 + return allArtifacts; 224 + } 225 + 226 + getManifest(): GeneratedManifest { 227 + return { ...this.manifest }; 228 + } 229 + 230 + getRegenerationHistory(iu_id?: string): RegenerationRecord[] { 231 + if (iu_id) { 232 + return this.manifest.regeneration_records.filter(r => r.iu_id === iu_id); 233 + } 234 + return [...this.manifest.regeneration_records]; 235 + } 236 + 237 + async validateArtifacts(): Promise<{ valid: boolean; issues: string[] }> { 238 + const issues: string[] = []; 239 + 240 + for (const iu_hash of this.manifest.iu_hashes) { 241 + for (const artifact of iu_hash.artifacts) { 242 + try { 243 + if (!(await this.fileExists(artifact.path))) { 244 + issues.push(`Missing artifact: ${artifact.path} for IU ${iu_hash.iu_id}`); 245 + continue; 246 + } 247 + 248 + const content = await readFile(artifact.path, 'utf-8'); 249 + const currentHash = this.computeHash(content); 250 + 251 + if (currentHash !== artifact.hash) { 252 + issues.push(`Hash mismatch for ${artifact.path}: expected ${artifact.hash}, got ${currentHash}`); 253 + } 254 + } catch (error) { 255 + issues.push(`Error validating ${artifact.path}: ${error instanceof Error ? error.message : 'Unknown error'}`); 256 + } 257 + } 258 + } 259 + 260 + return { 261 + valid: issues.length === 0, 262 + issues 263 + }; 264 + } 265 + } 266 + 267 + export function createRegenerationEngine(projectRoot: string): RegenerationEngine { 268 + return new RegenerationEngine(projectRoot); 269 + } 270 + 271 + /** @internal Phoenix VCS traceability — do not remove. */ 272 + export const _phoenix = { 273 + iu_id: '3715ff580b28c15b2682a86308238ea96a4bc26aa3f265097ee21e13cd01bb18', 274 + name: 'Regeneration Engine', 275 + risk_tier: 'medium', 276 + canon_ids: [5 as const], 277 + } as const;
+9 -5
examples/phoenix-self/src/generated/implementation/server.ts
··· 7 7 8 8 import { createServer, IncomingMessage, ServerResponse } from 'node:http'; 9 9 10 - import * as iuManager from './iu-manager.js'; 11 - import * as boundaryValidator from './boundary-validator.js'; 10 + import * as architecturalLinter from './architectural-linter.js'; 11 + import * as boundaryPolicySchema from './boundary-policy-schema.js'; 12 + import * as implementationUnitStructure from './implementation-unit-structure.js'; 13 + import * as regenerationEngine from './regeneration-engine.js'; 12 14 13 15 // ─── Metrics ───────────────────────────────────────────────────────────────── 14 16 ··· 22 24 // ─── Module Registry ───────────────────────────────────────────────────────── 23 25 24 26 const _svcModules = { 25 - 'iu-manager': iuManager, 26 - 'boundary-validator': boundaryValidator, 27 + 'architectural-linter': architecturalLinter, 28 + 'boundary-policy-schema': boundaryPolicySchema, 29 + 'implementation-unit-structure': implementationUnitStructure, 30 + 'regeneration-engine': regenerationEngine, 27 31 }; 28 32 29 33 // ─── Router ────────────────────────────────────────────────────────────────── ··· 92 96 } 93 97 94 98 export function startServer(port?: number): { server: ReturnType<typeof createServer>; port: number; ready: Promise<void> } { 95 - const requestedPort = port ?? parseInt(process.env.IMPLEMENTATION_PORT ?? process.env.PORT ?? '3003', 10); 99 + const requestedPort = port ?? parseInt(process.env.IMPLEMENTATION_PORT ?? process.env.PORT ?? '3001', 10); 96 100 const server = createServer(handleRequest); 97 101 let actualPort = requestedPort; 98 102
+13 -7
examples/phoenix-self/src/generated/index.ts
··· 1 - export * as ingestion from './ingestion/index.js'; 1 + /** 2 + * Phoenix VCS — Generated Service Registry 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + */ 6 + 2 7 export * as canonicalization from './canonicalization/index.js'; 3 8 export * as implementation from './implementation/index.js'; 9 + export * as ingestion from './ingestion/index.js'; 4 10 export * as integrity from './integrity/index.js'; 5 11 export * as operations from './operations/index.js'; 6 12 export * as platform from './platform/index.js'; 7 13 8 14 export const services = [ 9 - { name: 'Ingestion', dir: 'ingestion', port: 3001, modules: 2 }, 10 - { name: 'Canonicalization', dir: 'canonicalization', port: 3002, modules: 2 }, 11 - { name: 'Implementation', dir: 'implementation', port: 3003, modules: 2 }, 12 - { name: 'Integrity', dir: 'integrity', port: 3004, modules: 3 }, 13 - { name: 'Operations', dir: 'operations', port: 3005, modules: 3 }, 14 - { name: 'Platform', dir: 'platform', port: 3006, modules: 2 }, 15 + { name: 'Canonicalization', dir: 'canonicalization', port: 3000, modules: 3 }, 16 + { name: 'Implementation', dir: 'implementation', port: 3001, modules: 4 }, 17 + { name: 'Ingestion', dir: 'ingestion', port: 3002, modules: 4 }, 18 + { name: 'Integrity', dir: 'integrity', port: 3003, modules: 3 }, 19 + { name: 'Operations', dir: 'operations', port: 3004, modules: 3 }, 20 + { name: 'Platform', dir: 'platform', port: 3005, modules: 3 }, 15 21 ] as const;
+41 -13
examples/phoenix-self/src/generated/ingestion/__tests__/ingestion.test.ts
··· 8 8 import { describe, it, expect, afterAll } from 'vitest'; 9 9 import { startServer } from '../server.js'; 10 10 11 - import * as specParser from '../spec-parser.js'; 12 - import * as changeClassifier from '../change-classifier.js'; 11 + import * as changeClassification from '../change-classification.js'; 12 + import * as clauseExtraction from '../clause-extraction.js'; 13 + import * as dRateTrustLoop from '../d-rate-trust-loop.js'; 14 + import * as semanticHashing from '../semantic-hashing.js'; 13 15 14 16 describe('Ingestion modules', () => { 15 - describe('Spec Parser', () => { 17 + describe('Change Classification', () => { 16 18 it('exports Phoenix traceability metadata', () => { 17 - expect(specParser._phoenix).toBeDefined(); 18 - expect(specParser._phoenix.name).toBe('Spec Parser'); 19 - expect(specParser._phoenix.risk_tier).toBeTruthy(); 19 + expect(changeClassification._phoenix).toBeDefined(); 20 + expect(changeClassification._phoenix.name).toBe('Change Classification'); 21 + expect(changeClassification._phoenix.risk_tier).toBeTruthy(); 20 22 }); 21 23 22 24 it('has exported functions', () => { 23 - const exports = Object.keys(specParser).filter(k => k !== '_phoenix'); 25 + const exports = Object.keys(changeClassification).filter(k => k !== '_phoenix'); 24 26 expect(exports.length).toBeGreaterThan(0); 25 27 }); 26 28 }); 27 29 28 - describe('Change Classifier', () => { 30 + describe('Clause Extraction', () => { 29 31 it('exports Phoenix traceability metadata', () => { 30 - expect(changeClassifier._phoenix).toBeDefined(); 31 - expect(changeClassifier._phoenix.name).toBe('Change Classifier'); 32 - expect(changeClassifier._phoenix.risk_tier).toBeTruthy(); 32 + expect(clauseExtraction._phoenix).toBeDefined(); 33 + expect(clauseExtraction._phoenix.name).toBe('Clause Extraction'); 34 + expect(clauseExtraction._phoenix.risk_tier).toBeTruthy(); 33 35 }); 34 36 35 37 it('has exported functions', () => { 36 - const exports = Object.keys(changeClassifier).filter(k => k !== '_phoenix'); 38 + const exports = Object.keys(clauseExtraction).filter(k => k !== '_phoenix'); 39 + expect(exports.length).toBeGreaterThan(0); 40 + }); 41 + }); 42 + 43 + describe('D-Rate Trust Loop', () => { 44 + it('exports Phoenix traceability metadata', () => { 45 + expect(dRateTrustLoop._phoenix).toBeDefined(); 46 + expect(dRateTrustLoop._phoenix.name).toBe('D-Rate Trust Loop'); 47 + expect(dRateTrustLoop._phoenix.risk_tier).toBeTruthy(); 48 + }); 49 + 50 + it('has exported functions', () => { 51 + const exports = Object.keys(dRateTrustLoop).filter(k => k !== '_phoenix'); 52 + expect(exports.length).toBeGreaterThan(0); 53 + }); 54 + }); 55 + 56 + describe('Semantic Hashing', () => { 57 + it('exports Phoenix traceability metadata', () => { 58 + expect(semanticHashing._phoenix).toBeDefined(); 59 + expect(semanticHashing._phoenix.name).toBe('Semantic Hashing'); 60 + expect(semanticHashing._phoenix.risk_tier).toBeTruthy(); 61 + }); 62 + 63 + it('has exported functions', () => { 64 + const exports = Object.keys(semanticHashing).filter(k => k !== '_phoenix'); 37 65 expect(exports.length).toBeGreaterThan(0); 38 66 }); 39 67 }); ··· 67 95 const res = await fetch(`http://localhost:${instance.port}/modules`); 68 96 expect(res.status).toBe(200); 69 97 const body = await res.json() as Array<Record<string, unknown>>; 70 - expect(body.length).toBe(2); 98 + expect(body.length).toBe(4); 71 99 }); 72 100 73 101 it('GET /unknown returns 404', async () => {
+196
examples/phoenix-self/src/generated/ingestion/change-classification.ts
··· 1 + export type ChangeClass = 'a' | 'b' | 'c' | 'd'; 2 + 3 + export interface ClassificationSignals { 4 + normalizedEditDistance: number; 5 + semhashDelta: number; 6 + contextHashDelta: number; 7 + termReferenceDeltas: number; 8 + sectionStructureDeltas: number; 9 + } 10 + 11 + export interface ClassificationResult { 12 + class: ChangeClass; 13 + confidence: number; 14 + signals: ClassificationSignals; 15 + requiresReview: boolean; 16 + reasoning: string; 17 + } 18 + 19 + export interface ClassificationThresholds { 20 + trivial: { 21 + maxEditDistance: number; 22 + maxSemhashDelta: number; 23 + maxContextDelta: number; 24 + maxTermDeltas: number; 25 + maxStructureDeltas: number; 26 + }; 27 + localSemantic: { 28 + maxEditDistance: number; 29 + maxSemhashDelta: number; 30 + maxContextDelta: number; 31 + maxTermDeltas: number; 32 + maxStructureDeltas: number; 33 + }; 34 + contextualShift: { 35 + maxEditDistance: number; 36 + maxSemhashDelta: number; 37 + maxContextDelta: number; 38 + maxTermDeltas: number; 39 + maxStructureDeltas: number; 40 + }; 41 + } 42 + 43 + const DEFAULT_THRESHOLDS: ClassificationThresholds = { 44 + trivial: { 45 + maxEditDistance: 0.1, 46 + maxSemhashDelta: 0.05, 47 + maxContextDelta: 0.02, 48 + maxTermDeltas: 1, 49 + maxStructureDeltas: 0, 50 + }, 51 + localSemantic: { 52 + maxEditDistance: 0.3, 53 + maxSemhashDelta: 0.2, 54 + maxContextDelta: 0.1, 55 + maxTermDeltas: 3, 56 + maxStructureDeltas: 1, 57 + }, 58 + contextualShift: { 59 + maxEditDistance: 0.6, 60 + maxSemhashDelta: 0.4, 61 + maxContextDelta: 0.3, 62 + maxTermDeltas: 8, 63 + maxStructureDeltas: 3, 64 + }, 65 + }; 66 + 67 + export class ChangeClassifier { 68 + private thresholds: ClassificationThresholds; 69 + 70 + constructor(thresholds: ClassificationThresholds = DEFAULT_THRESHOLDS) { 71 + this.thresholds = thresholds; 72 + } 73 + 74 + classify(signals: ClassificationSignals): ClassificationResult { 75 + const { normalizedEditDistance, semhashDelta, contextHashDelta, termReferenceDeltas, sectionStructureDeltas } = signals; 76 + 77 + // Check for trivial changes (class a) 78 + if (this.meetsThresholds(signals, this.thresholds.trivial)) { 79 + return { 80 + class: 'a', 81 + confidence: this.calculateConfidence(signals, 'a'), 82 + signals, 83 + requiresReview: false, 84 + reasoning: 'Minimal changes across all metrics indicate trivial modification', 85 + }; 86 + } 87 + 88 + // Check for local semantic changes (class b) 89 + if (this.meetsThresholds(signals, this.thresholds.localSemantic)) { 90 + return { 91 + class: 'b', 92 + confidence: this.calculateConfidence(signals, 'b'), 93 + signals, 94 + requiresReview: false, 95 + reasoning: 'Moderate semantic changes with limited structural impact', 96 + }; 97 + } 98 + 99 + // Check for contextual shift (class c) 100 + if (this.meetsThresholds(signals, this.thresholds.contextualShift)) { 101 + return { 102 + class: 'c', 103 + confidence: this.calculateConfidence(signals, 'c'), 104 + signals, 105 + requiresReview: false, 106 + reasoning: 'Significant contextual or structural changes detected', 107 + }; 108 + } 109 + 110 + // Default to uncertain (class d) - requires review 111 + return { 112 + class: 'd', 113 + confidence: 0.5, 114 + signals, 115 + requiresReview: true, 116 + reasoning: 'Changes exceed classification thresholds, manual review required', 117 + }; 118 + } 119 + 120 + private meetsThresholds(signals: ClassificationSignals, thresholds: ClassificationThresholds['trivial']): boolean { 121 + return ( 122 + signals.normalizedEditDistance <= thresholds.maxEditDistance && 123 + signals.semhashDelta <= thresholds.maxSemhashDelta && 124 + signals.contextHashDelta <= thresholds.maxContextDelta && 125 + signals.termReferenceDeltas <= thresholds.maxTermDeltas && 126 + signals.sectionStructureDeltas <= thresholds.maxStructureDeltas 127 + ); 128 + } 129 + 130 + private calculateConfidence(signals: ClassificationSignals, classification: ChangeClass): number { 131 + const weights = { 132 + editDistance: 0.25, 133 + semhash: 0.25, 134 + context: 0.2, 135 + termRef: 0.15, 136 + structure: 0.15, 137 + }; 138 + 139 + let score = 0; 140 + const thresholds = this.getThresholdsForClass(classification); 141 + 142 + if (thresholds) { 143 + score += weights.editDistance * Math.max(0, 1 - (signals.normalizedEditDistance / thresholds.maxEditDistance)); 144 + score += weights.semhash * Math.max(0, 1 - (signals.semhashDelta / thresholds.maxSemhashDelta)); 145 + score += weights.context * Math.max(0, 1 - (signals.contextHashDelta / thresholds.maxContextDelta)); 146 + score += weights.termRef * Math.max(0, 1 - (signals.termReferenceDeltas / thresholds.maxTermDeltas)); 147 + score += weights.structure * Math.max(0, 1 - (signals.sectionStructureDeltas / Math.max(1, thresholds.maxStructureDeltas))); 148 + } 149 + 150 + return Math.min(1, Math.max(0, score)); 151 + } 152 + 153 + private getThresholdsForClass(classification: ChangeClass): ClassificationThresholds['trivial'] | null { 154 + switch (classification) { 155 + case 'a': return this.thresholds.trivial; 156 + case 'b': return this.thresholds.localSemantic; 157 + case 'c': return this.thresholds.contextualShift; 158 + default: return null; 159 + } 160 + } 161 + 162 + updateThresholds(newThresholds: Partial<ClassificationThresholds>): void { 163 + this.thresholds = { 164 + trivial: { ...this.thresholds.trivial, ...newThresholds.trivial }, 165 + localSemantic: { ...this.thresholds.localSemantic, ...newThresholds.localSemantic }, 166 + contextualShift: { ...this.thresholds.contextualShift, ...newThresholds.contextualShift }, 167 + }; 168 + } 169 + } 170 + 171 + export function classifyChange(signals: ClassificationSignals, thresholds?: ClassificationThresholds): ClassificationResult { 172 + const classifier = new ChangeClassifier(thresholds); 173 + return classifier.classify(signals); 174 + } 175 + 176 + export function requiresHumanReview(result: ClassificationResult): boolean { 177 + return result.requiresReview || result.class === 'd'; 178 + } 179 + 180 + export function getClassificationDescription(changeClass: ChangeClass): string { 181 + switch (changeClass) { 182 + case 'a': return 'Trivial change with minimal impact'; 183 + case 'b': return 'Local semantic change with contained effects'; 184 + case 'c': return 'Contextual shift with broader implications'; 185 + case 'd': return 'Uncertain change requiring manual review'; 186 + default: return 'Unknown classification'; 187 + } 188 + } 189 + 190 + /** @internal Phoenix VCS traceability — do not remove. */ 191 + export const _phoenix = { 192 + iu_id: '71cf253bd2837e75bc7e46505a8b1682ad4a3a99154add270af1f2239bbd22cb', 193 + name: 'Change Classification', 194 + risk_tier: 'low', 195 + canon_ids: [3 as const], 196 + } as const;
+196
examples/phoenix-self/src/generated/ingestion/clause-extraction.ts
··· 1 + import { createHash } from 'node:crypto'; 2 + 3 + export interface Clause { 4 + clauseId: string; 5 + sourceDocId: string; 6 + sourceLineRange: [number, number]; 7 + rawText: string; 8 + normalizedText: string; 9 + sectionPath: string[]; 10 + } 11 + 12 + export interface ParsedDocument { 13 + docId: string; 14 + clauses: Clause[]; 15 + sectionHierarchy: SectionNode[]; 16 + } 17 + 18 + export interface SectionNode { 19 + level: number; 20 + title: string; 21 + path: string[]; 22 + startLine: number; 23 + endLine: number; 24 + children: SectionNode[]; 25 + } 26 + 27 + export class ClauseExtractor { 28 + private readonly hashAlgorithm = 'sha256'; 29 + 30 + extractClauses(docId: string, markdownContent: string): ParsedDocument { 31 + const lines = markdownContent.split('\n'); 32 + const sectionHierarchy = this.parseSectionHierarchy(lines); 33 + const clauses = this.extractClausesFromSections(docId, lines, sectionHierarchy); 34 + 35 + return { 36 + docId, 37 + clauses, 38 + sectionHierarchy 39 + }; 40 + } 41 + 42 + private parseSectionHierarchy(lines: string[]): SectionNode[] { 43 + const sections: SectionNode[] = []; 44 + const sectionStack: SectionNode[] = []; 45 + 46 + for (let i = 0; i < lines.length; i++) { 47 + const line = lines[i]; 48 + const headingMatch = line.match(/^(#{1,6})\s+(.+)$/); 49 + 50 + if (headingMatch) { 51 + const level = headingMatch[1].length; 52 + const title = headingMatch[2].trim(); 53 + 54 + // Pop sections from stack that are at same or deeper level 55 + while (sectionStack.length > 0 && sectionStack[sectionStack.length - 1].level >= level) { 56 + const poppedSection = sectionStack.pop()!; 57 + poppedSection.endLine = i - 1; 58 + } 59 + 60 + // Build section path 61 + const path = sectionStack.map(s => s.title).concat(title); 62 + 63 + const section: SectionNode = { 64 + level, 65 + title, 66 + path, 67 + startLine: i, 68 + endLine: lines.length - 1, // Will be updated when section ends 69 + children: [] 70 + }; 71 + 72 + // Add to parent's children or root 73 + if (sectionStack.length > 0) { 74 + sectionStack[sectionStack.length - 1].children.push(section); 75 + } else { 76 + sections.push(section); 77 + } 78 + 79 + sectionStack.push(section); 80 + } 81 + } 82 + 83 + // Close remaining sections 84 + while (sectionStack.length > 0) { 85 + const section = sectionStack.pop()!; 86 + section.endLine = lines.length - 1; 87 + } 88 + 89 + return sections; 90 + } 91 + 92 + private extractClausesFromSections(docId: string, lines: string[], sections: SectionNode[]): Clause[] { 93 + const clauses: Clause[] = []; 94 + 95 + for (const section of sections) { 96 + this.extractClausesFromSection(docId, lines, section, clauses); 97 + } 98 + 99 + return clauses; 100 + } 101 + 102 + private extractClausesFromSection(docId: string, lines: string[], section: SectionNode, clauses: Clause[]): void { 103 + // Extract clauses from this section's content (before any child sections) 104 + let contentEndLine = section.endLine; 105 + if (section.children.length > 0) { 106 + contentEndLine = section.children[0].startLine - 1; 107 + } 108 + 109 + // Find content lines (skip the heading itself) 110 + const contentStartLine = section.startLine + 1; 111 + if (contentStartLine <= contentEndLine) { 112 + const contentLines = lines.slice(contentStartLine, contentEndLine + 1); 113 + const nonEmptyLines = contentLines.map((line, idx) => ({ line, lineNum: contentStartLine + idx })) 114 + .filter(({ line }) => line.trim().length > 0); 115 + 116 + if (nonEmptyLines.length > 0) { 117 + // Group consecutive non-empty lines into clauses 118 + let currentClause: { line: string; lineNum: number }[] = []; 119 + 120 + for (const { line, lineNum } of nonEmptyLines) { 121 + if (currentClause.length === 0 || lineNum === currentClause[currentClause.length - 1].lineNum + 1) { 122 + currentClause.push({ line, lineNum }); 123 + } else { 124 + // Gap found, finalize current clause and start new one 125 + if (currentClause.length > 0) { 126 + clauses.push(this.createClause(docId, currentClause, section.path)); 127 + } 128 + currentClause = [{ line, lineNum }]; 129 + } 130 + } 131 + 132 + // Finalize last clause 133 + if (currentClause.length > 0) { 134 + clauses.push(this.createClause(docId, currentClause, section.path)); 135 + } 136 + } 137 + } 138 + 139 + // Recursively process child sections 140 + for (const child of section.children) { 141 + this.extractClausesFromSection(docId, lines, child, clauses); 142 + } 143 + } 144 + 145 + private createClause(docId: string, clauseLines: { line: string; lineNum: number }[], sectionPath: string[]): Clause { 146 + const rawText = clauseLines.map(cl => cl.line).join('\n'); 147 + const normalizedText = this.normalizeText(rawText); 148 + const clauseId = this.generateClauseId(normalizedText); 149 + const sourceLineRange: [number, number] = [ 150 + clauseLines[0].lineNum, 151 + clauseLines[clauseLines.length - 1].lineNum 152 + ]; 153 + 154 + return { 155 + clauseId, 156 + sourceDocId: docId, 157 + sourceLineRange, 158 + rawText, 159 + normalizedText, 160 + sectionPath: [...sectionPath] 161 + }; 162 + } 163 + 164 + private normalizeText(text: string): string { 165 + return text 166 + .toLowerCase() 167 + .replace(/\s+/g, ' ') 168 + .replace(/[^\w\s]/g, '') 169 + .trim(); 170 + } 171 + 172 + private generateClauseId(normalizedText: string): string { 173 + return createHash(this.hashAlgorithm) 174 + .update(normalizedText, 'utf8') 175 + .digest('hex'); 176 + } 177 + } 178 + 179 + export function extractClauses(docId: string, markdownContent: string): ParsedDocument { 180 + const extractor = new ClauseExtractor(); 181 + return extractor.extractClauses(docId, markdownContent); 182 + } 183 + 184 + export function generateClauseId(text: string): string { 185 + const extractor = new ClauseExtractor(); 186 + const normalized = extractor['normalizeText'](text); 187 + return extractor['generateClauseId'](normalized); 188 + } 189 + 190 + /** @internal Phoenix VCS traceability — do not remove. */ 191 + export const _phoenix = { 192 + iu_id: '5034ec6ff3a2ba839954cb475d519bcd40dd2eb0556da6f1fd6fb6b472ec533e', 193 + name: 'Clause Extraction', 194 + risk_tier: 'high', 195 + canon_ids: [5 as const], 196 + } as const;
+178
examples/phoenix-self/src/generated/ingestion/d-rate-trust-loop.ts
··· 1 + import { EventEmitter } from 'node:events'; 2 + 3 + export interface DRateMetrics { 4 + readonly currentDRate: number; 5 + readonly targetDRate: number; 6 + readonly windowSize: number; 7 + readonly totalChanges: number; 8 + readonly uncertainChanges: number; 9 + readonly isAlarmActive: boolean; 10 + } 11 + 12 + export interface DRateAlarmEvent { 13 + readonly timestamp: number; 14 + readonly dRate: number; 15 + readonly threshold: number; 16 + readonly windowSize: number; 17 + } 18 + 19 + export interface TrustDegradationWarning { 20 + readonly severity: 'warning' | 'critical'; 21 + readonly message: string; 22 + readonly dRate: number; 23 + readonly recommendedActions: string[]; 24 + } 25 + 26 + export interface OverrideFrictionConfig { 27 + readonly baseDelay: number; 28 + readonly escalationFactor: number; 29 + readonly maxDelay: number; 30 + readonly requiresJustification: boolean; 31 + } 32 + 33 + export type ChangeClassification = 'a' | 'b' | 'c' | 'd' | 'e'; 34 + 35 + export class DRateTrustLoop extends EventEmitter { 36 + private readonly TARGET_D_RATE = 0.05; // 5% 37 + private readonly ALARM_THRESHOLD = 0.15; // 15% 38 + private readonly windowSize: number; 39 + private readonly changeWindow: ChangeClassification[] = []; 40 + private alarmActive = false; 41 + private overrideFriction: OverrideFrictionConfig = { 42 + baseDelay: 1000, 43 + escalationFactor: 2.0, 44 + maxDelay: 30000, 45 + requiresJustification: false 46 + }; 47 + 48 + constructor(windowSize = 100) { 49 + super(); 50 + this.windowSize = windowSize; 51 + } 52 + 53 + recordChange(classification: ChangeClassification): void { 54 + this.changeWindow.push(classification); 55 + 56 + if (this.changeWindow.length > this.windowSize) { 57 + this.changeWindow.shift(); 58 + } 59 + 60 + this.evaluateDRate(); 61 + } 62 + 63 + private evaluateDRate(): void { 64 + const metrics = this.calculateMetrics(); 65 + const wasAlarmActive = this.alarmActive; 66 + 67 + this.alarmActive = metrics.currentDRate > this.ALARM_THRESHOLD; 68 + 69 + if (this.alarmActive && !wasAlarmActive) { 70 + this.triggerAlarm(metrics); 71 + } else if (!this.alarmActive && wasAlarmActive) { 72 + this.clearAlarm(); 73 + } 74 + } 75 + 76 + private calculateMetrics(): DRateMetrics { 77 + const totalChanges = this.changeWindow.length; 78 + const uncertainChanges = this.changeWindow.filter(c => c === 'd').length; 79 + const currentDRate = totalChanges > 0 ? uncertainChanges / totalChanges : 0; 80 + 81 + return { 82 + currentDRate, 83 + targetDRate: this.TARGET_D_RATE, 84 + windowSize: this.windowSize, 85 + totalChanges, 86 + uncertainChanges, 87 + isAlarmActive: this.alarmActive 88 + }; 89 + } 90 + 91 + private triggerAlarm(metrics: DRateMetrics): void { 92 + const alarmEvent: DRateAlarmEvent = { 93 + timestamp: Date.now(), 94 + dRate: metrics.currentDRate, 95 + threshold: this.ALARM_THRESHOLD, 96 + windowSize: this.windowSize 97 + }; 98 + 99 + this.increaseOverrideFriction(); 100 + const warning = this.generateTrustDegradationWarning(metrics.currentDRate); 101 + 102 + this.emit('alarm', alarmEvent); 103 + this.emit('trustDegradation', warning); 104 + this.emit('overrideFrictionChanged', this.overrideFriction); 105 + } 106 + 107 + private clearAlarm(): void { 108 + this.resetOverrideFriction(); 109 + this.emit('alarmCleared', { timestamp: Date.now() }); 110 + this.emit('overrideFrictionChanged', this.overrideFriction); 111 + } 112 + 113 + private increaseOverrideFriction(): void { 114 + this.overrideFriction = { 115 + baseDelay: Math.min(this.overrideFriction.baseDelay * this.overrideFriction.escalationFactor, this.overrideFriction.maxDelay), 116 + escalationFactor: this.overrideFriction.escalationFactor, 117 + maxDelay: this.overrideFriction.maxDelay, 118 + requiresJustification: true 119 + }; 120 + } 121 + 122 + private resetOverrideFriction(): void { 123 + this.overrideFriction = { 124 + baseDelay: 1000, 125 + escalationFactor: 2.0, 126 + maxDelay: 30000, 127 + requiresJustification: false 128 + }; 129 + } 130 + 131 + private generateTrustDegradationWarning(dRate: number): TrustDegradationWarning { 132 + const severity = dRate > 0.25 ? 'critical' : 'warning'; 133 + const percentage = Math.round(dRate * 100); 134 + 135 + return { 136 + severity, 137 + message: `Trust degradation detected: D-rate at ${percentage}% exceeds threshold of 15%. Classifier tuning required.`, 138 + dRate, 139 + recommendedActions: [ 140 + 'Review recent uncertain classifications for patterns', 141 + 'Retrain classification models with recent data', 142 + 'Adjust classification thresholds', 143 + 'Consider manual review of borderline cases' 144 + ] 145 + }; 146 + } 147 + 148 + getMetrics(): DRateMetrics { 149 + return this.calculateMetrics(); 150 + } 151 + 152 + getOverrideFriction(): OverrideFrictionConfig { 153 + return { ...this.overrideFriction }; 154 + } 155 + 156 + isAlarmTriggered(): boolean { 157 + return this.alarmActive; 158 + } 159 + 160 + reset(): void { 161 + this.changeWindow.length = 0; 162 + this.alarmActive = false; 163 + this.resetOverrideFriction(); 164 + this.emit('reset', { timestamp: Date.now() }); 165 + } 166 + } 167 + 168 + export function createDRateTrustLoop(windowSize?: number): DRateTrustLoop { 169 + return new DRateTrustLoop(windowSize); 170 + } 171 + 172 + /** @internal Phoenix VCS traceability — do not remove. */ 173 + export const _phoenix = { 174 + iu_id: '8d8eaf1f9a097d8ba4a1fd49534e801de9eebafe5dbee5b88b4da6b30093669e', 175 + name: 'D-Rate Trust Loop', 176 + risk_tier: 'high', 177 + canon_ids: [5 as const], 178 + } as const;
+11 -2
examples/phoenix-self/src/generated/ingestion/index.ts
··· 1 - export * as specParser from './spec-parser.js'; 2 - export * as changeClassifier from './change-classifier.js'; 1 + /** 2 + * Ingestion 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Barrel export for all Ingestion modules. 6 + */ 7 + 8 + export * as changeClassification from './change-classification.js'; 9 + export * as clauseExtraction from './clause-extraction.js'; 10 + export * as dRateTrustLoop from './d-rate-trust-loop.js'; 11 + export * as semanticHashing from './semantic-hashing.js';
+215
examples/phoenix-self/src/generated/ingestion/semantic-hashing.ts
··· 1 + import { createHash } from 'node:crypto'; 2 + 3 + export interface Clause { 4 + id: string; 5 + normalizedText: string; 6 + sectionPath: string[]; 7 + } 8 + 9 + export interface ClauseWithNeighbors extends Clause { 10 + neighborHashes: string[]; 11 + } 12 + 13 + export interface GraphEdge { 14 + type: 'relates_to' | 'depends_on' | 'conflicts_with' | 'references'; 15 + strength: 'weak' | 'strong'; 16 + sourceId: string; 17 + targetId: string; 18 + } 19 + 20 + export interface CanonicalGraphContext { 21 + edges: GraphEdge[]; 22 + nodeIds: string[]; 23 + } 24 + 25 + export interface SemanticHashes { 26 + clause_semhash: string; 27 + contextsemhashcold: string; 28 + contextsemhashwarm?: string; 29 + } 30 + 31 + export class SemanticHasher { 32 + private readonly hashCache = new Map<string, string>(); 33 + 34 + computeClauseSemhash(normalizedText: string): string { 35 + const cacheKey = `clause:${normalizedText}`; 36 + 37 + if (this.hashCache.has(cacheKey)) { 38 + return this.hashCache.get(cacheKey)!; 39 + } 40 + 41 + const hash = createHash('sha256') 42 + .update(normalizedText, 'utf8') 43 + .digest('hex'); 44 + 45 + this.hashCache.set(cacheKey, hash); 46 + return hash; 47 + } 48 + 49 + computeContextSemhashCold(clause: ClauseWithNeighbors): string { 50 + const sectionPathStr = clause.sectionPath.join('/'); 51 + const neighborHashesStr = clause.neighborHashes.sort().join('|'); 52 + 53 + const contextInput = [ 54 + clause.normalizedText, 55 + sectionPathStr, 56 + neighborHashesStr 57 + ].join('::'); 58 + 59 + const cacheKey = `cold:${contextInput}`; 60 + 61 + if (this.hashCache.has(cacheKey)) { 62 + return this.hashCache.get(cacheKey)!; 63 + } 64 + 65 + const hash = createHash('sha256') 66 + .update(contextInput, 'utf8') 67 + .digest('hex'); 68 + 69 + this.hashCache.set(cacheKey, hash); 70 + return hash; 71 + } 72 + 73 + computeContextSemhashWarm( 74 + clause: ClauseWithNeighbors, 75 + canonicalContext: CanonicalGraphContext 76 + ): string { 77 + // Filter out weak 'relates_to' edges to prevent incidental invalidation 78 + const relevantEdges = canonicalContext.edges.filter(edge => 79 + !(edge.type === 'relates_to' && edge.strength === 'weak') 80 + ); 81 + 82 + // Sort edges for deterministic hashing 83 + const sortedEdges = relevantEdges 84 + .sort((a, b) => { 85 + const aKey = `${a.sourceId}:${a.targetId}:${a.type}:${a.strength}`; 86 + const bKey = `${b.sourceId}:${b.targetId}:${b.type}:${b.strength}`; 87 + return aKey.localeCompare(bKey); 88 + }); 89 + 90 + const edgesStr = sortedEdges 91 + .map(edge => `${edge.sourceId}->${edge.targetId}:${edge.type}:${edge.strength}`) 92 + .join('|'); 93 + 94 + const nodeIdsStr = canonicalContext.nodeIds.sort().join(','); 95 + const sectionPathStr = clause.sectionPath.join('/'); 96 + const neighborHashesStr = clause.neighborHashes.sort().join('|'); 97 + 98 + const warmInput = [ 99 + clause.normalizedText, 100 + sectionPathStr, 101 + neighborHashesStr, 102 + edgesStr, 103 + nodeIdsStr 104 + ].join('::'); 105 + 106 + const cacheKey = `warm:${warmInput}`; 107 + 108 + if (this.hashCache.has(cacheKey)) { 109 + return this.hashCache.get(cacheKey)!; 110 + } 111 + 112 + const hash = createHash('sha256') 113 + .update(warmInput, 'utf8') 114 + .digest('hex'); 115 + 116 + this.hashCache.set(cacheKey, hash); 117 + return hash; 118 + } 119 + 120 + computeAllHashes( 121 + clause: ClauseWithNeighbors, 122 + canonicalContext?: CanonicalGraphContext 123 + ): SemanticHashes { 124 + const clause_semhash = this.computeClauseSemhash(clause.normalizedText); 125 + const contextsemhashcold = this.computeContextSemhashCold(clause); 126 + 127 + const result: SemanticHashes = { 128 + clause_semhash, 129 + contextsemhashcold 130 + }; 131 + 132 + if (canonicalContext) { 133 + result.contextsemhashwarm = this.computeContextSemhashWarm(clause, canonicalContext); 134 + } 135 + 136 + return result; 137 + } 138 + 139 + clearCache(): void { 140 + this.hashCache.clear(); 141 + } 142 + 143 + getCacheSize(): number { 144 + return this.hashCache.size; 145 + } 146 + } 147 + 148 + export function createSemanticHasher(): SemanticHasher { 149 + return new SemanticHasher(); 150 + } 151 + 152 + export function computeClauseSemhash(normalizedText: string): string { 153 + return createHash('sha256') 154 + .update(normalizedText, 'utf8') 155 + .digest('hex'); 156 + } 157 + 158 + export function computeContextSemhashCold(clause: ClauseWithNeighbors): string { 159 + const sectionPathStr = clause.sectionPath.join('/'); 160 + const neighborHashesStr = clause.neighborHashes.sort().join('|'); 161 + 162 + const contextInput = [ 163 + clause.normalizedText, 164 + sectionPathStr, 165 + neighborHashesStr 166 + ].join('::'); 167 + 168 + return createHash('sha256') 169 + .update(contextInput, 'utf8') 170 + .digest('hex'); 171 + } 172 + 173 + export function computeContextSemhashWarm( 174 + clause: ClauseWithNeighbors, 175 + canonicalContext: CanonicalGraphContext 176 + ): string { 177 + const relevantEdges = canonicalContext.edges.filter(edge => 178 + !(edge.type === 'relates_to' && edge.strength === 'weak') 179 + ); 180 + 181 + const sortedEdges = relevantEdges 182 + .sort((a, b) => { 183 + const aKey = `${a.sourceId}:${a.targetId}:${a.type}:${a.strength}`; 184 + const bKey = `${b.sourceId}:${b.targetId}:${b.type}:${b.strength}`; 185 + return aKey.localeCompare(bKey); 186 + }); 187 + 188 + const edgesStr = sortedEdges 189 + .map(edge => `${edge.sourceId}->${edge.targetId}:${edge.type}:${edge.strength}`) 190 + .join('|'); 191 + 192 + const nodeIdsStr = canonicalContext.nodeIds.sort().join(','); 193 + const sectionPathStr = clause.sectionPath.join('/'); 194 + const neighborHashesStr = clause.neighborHashes.sort().join('|'); 195 + 196 + const warmInput = [ 197 + clause.normalizedText, 198 + sectionPathStr, 199 + neighborHashesStr, 200 + edgesStr, 201 + nodeIdsStr 202 + ].join('::'); 203 + 204 + return createHash('sha256') 205 + .update(warmInput, 'utf8') 206 + .digest('hex'); 207 + } 208 + 209 + /** @internal Phoenix VCS traceability — do not remove. */ 210 + export const _phoenix = { 211 + iu_id: 'd66eb87520511ebe320d2956d1c9f8654e59d10626612884b226474fcbc598e7', 212 + name: 'Semantic Hashing', 213 + risk_tier: 'high', 214 + canon_ids: [5 as const], 215 + } as const;
+9 -5
examples/phoenix-self/src/generated/ingestion/server.ts
··· 7 7 8 8 import { createServer, IncomingMessage, ServerResponse } from 'node:http'; 9 9 10 - import * as specParser from './spec-parser.js'; 11 - import * as changeClassifier from './change-classifier.js'; 10 + import * as changeClassification from './change-classification.js'; 11 + import * as clauseExtraction from './clause-extraction.js'; 12 + import * as dRateTrustLoop from './d-rate-trust-loop.js'; 13 + import * as semanticHashing from './semantic-hashing.js'; 12 14 13 15 // ─── Metrics ───────────────────────────────────────────────────────────────── 14 16 ··· 22 24 // ─── Module Registry ───────────────────────────────────────────────────────── 23 25 24 26 const _svcModules = { 25 - 'spec-parser': specParser, 26 - 'change-classifier': changeClassifier, 27 + 'change-classification': changeClassification, 28 + 'clause-extraction': clauseExtraction, 29 + 'd-rate-trust-loop': dRateTrustLoop, 30 + 'semantic-hashing': semanticHashing, 27 31 }; 28 32 29 33 // ─── Router ────────────────────────────────────────────────────────────────── ··· 92 96 } 93 97 94 98 export function startServer(port?: number): { server: ReturnType<typeof createServer>; port: number; ready: Promise<void> } { 95 - const requestedPort = port ?? parseInt(process.env.INGESTION_PORT ?? process.env.PORT ?? '3001', 10); 99 + const requestedPort = port ?? parseInt(process.env.INGESTION_PORT ?? process.env.PORT ?? '3002', 10); 96 100 const server = createServer(handleRequest); 97 101 let actualPort = requestedPort; 98 102
+18 -18
examples/phoenix-self/src/generated/integrity/__tests__/integrity.test.ts
··· 8 8 import { describe, it, expect, afterAll } from 'vitest'; 9 9 import { startServer } from '../server.js'; 10 10 11 - import * as driftDetector from '../drift-detector.js'; 12 - import * as evidenceEngine from '../evidence-engine.js'; 13 - import * as cascadePropagator from '../cascade-propagator.js'; 11 + import * as cascadingFailureSemantics from '../cascading-failure-semantics.js'; 12 + import * as driftDetection from '../drift-detection.js'; 13 + import * as evidencePolicyEngine from '../evidence-policy-engine.js'; 14 14 15 15 describe('Integrity modules', () => { 16 - describe('Drift Detector', () => { 16 + describe('Cascading Failure Semantics', () => { 17 17 it('exports Phoenix traceability metadata', () => { 18 - expect(driftDetector._phoenix).toBeDefined(); 19 - expect(driftDetector._phoenix.name).toBe('Drift Detector'); 20 - expect(driftDetector._phoenix.risk_tier).toBeTruthy(); 18 + expect(cascadingFailureSemantics._phoenix).toBeDefined(); 19 + expect(cascadingFailureSemantics._phoenix.name).toBe('Cascading Failure Semantics'); 20 + expect(cascadingFailureSemantics._phoenix.risk_tier).toBeTruthy(); 21 21 }); 22 22 23 23 it('has exported functions', () => { 24 - const exports = Object.keys(driftDetector).filter(k => k !== '_phoenix'); 24 + const exports = Object.keys(cascadingFailureSemantics).filter(k => k !== '_phoenix'); 25 25 expect(exports.length).toBeGreaterThan(0); 26 26 }); 27 27 }); 28 28 29 - describe('Evidence Engine', () => { 29 + describe('Drift Detection', () => { 30 30 it('exports Phoenix traceability metadata', () => { 31 - expect(evidenceEngine._phoenix).toBeDefined(); 32 - expect(evidenceEngine._phoenix.name).toBe('Evidence Engine'); 33 - expect(evidenceEngine._phoenix.risk_tier).toBeTruthy(); 31 + expect(driftDetection._phoenix).toBeDefined(); 32 + expect(driftDetection._phoenix.name).toBe('Drift Detection'); 33 + expect(driftDetection._phoenix.risk_tier).toBeTruthy(); 34 34 }); 35 35 36 36 it('has exported functions', () => { 37 - const exports = Object.keys(evidenceEngine).filter(k => k !== '_phoenix'); 37 + const exports = Object.keys(driftDetection).filter(k => k !== '_phoenix'); 38 38 expect(exports.length).toBeGreaterThan(0); 39 39 }); 40 40 }); 41 41 42 - describe('Cascade Propagator', () => { 42 + describe('Evidence & Policy Engine', () => { 43 43 it('exports Phoenix traceability metadata', () => { 44 - expect(cascadePropagator._phoenix).toBeDefined(); 45 - expect(cascadePropagator._phoenix.name).toBe('Cascade Propagator'); 46 - expect(cascadePropagator._phoenix.risk_tier).toBeTruthy(); 44 + expect(evidencePolicyEngine._phoenix).toBeDefined(); 45 + expect(evidencePolicyEngine._phoenix.name).toBe('Evidence & Policy Engine'); 46 + expect(evidencePolicyEngine._phoenix.risk_tier).toBeTruthy(); 47 47 }); 48 48 49 49 it('has exported functions', () => { 50 - const exports = Object.keys(cascadePropagator).filter(k => k !== '_phoenix'); 50 + const exports = Object.keys(evidencePolicyEngine).filter(k => k !== '_phoenix'); 51 51 expect(exports.length).toBeGreaterThan(0); 52 52 }); 53 53 });
+270
examples/phoenix-self/src/generated/integrity/cascading-failure-semantics.ts
··· 1 + export interface IU { 2 + id: string; 3 + dependencies: string[]; 4 + evidence: EvidenceResult[]; 5 + status: 'accepted' | 'blocked' | 'pending'; 6 + } 7 + 8 + export interface EvidenceResult { 9 + type: 'typecheck' | 'boundary_check' | 'tagged_test'; 10 + passed: boolean; 11 + message?: string; 12 + timestamp: number; 13 + } 14 + 15 + export interface CascadeResult { 16 + blockedIUs: string[]; 17 + cascadeChain: CascadeStep[]; 18 + maxDepthReached: boolean; 19 + totalAffected: number; 20 + } 21 + 22 + export interface CascadeStep { 23 + iuId: string; 24 + reason: string; 25 + depth: number; 26 + dependents: string[]; 27 + } 28 + 29 + export interface CascadeOptions { 30 + maxDepth?: number; 31 + enableGraphTraversal?: boolean; 32 + } 33 + 34 + export class CascadingFailureManager { 35 + private ius: Map<string, IU> = new Map(); 36 + private dependencyGraph: Map<string, Set<string>> = new Map(); 37 + private reverseDependencyGraph: Map<string, Set<string>> = new Map(); 38 + 39 + constructor(private options: CascadeOptions = {}) { 40 + this.options.maxDepth = options.maxDepth ?? 10; 41 + this.options.enableGraphTraversal = options.enableGraphTraversal ?? true; 42 + } 43 + 44 + registerIU(iu: IU): void { 45 + this.ius.set(iu.id, { ...iu }); 46 + this.updateDependencyGraphs(iu); 47 + } 48 + 49 + private updateDependencyGraphs(iu: IU): void { 50 + this.dependencyGraph.set(iu.id, new Set(iu.dependencies)); 51 + 52 + for (const depId of iu.dependencies) { 53 + if (!this.reverseDependencyGraph.has(depId)) { 54 + this.reverseDependencyGraph.set(depId, new Set()); 55 + } 56 + this.reverseDependencyGraph.get(depId)!.add(iu.id); 57 + } 58 + } 59 + 60 + processFailure(iuId: string): CascadeResult { 61 + const iu = this.ius.get(iuId); 62 + if (!iu) { 63 + throw new Error(`IU not found: ${iuId}`); 64 + } 65 + 66 + const hasFailedEvidence = iu.evidence.some(e => !e.passed); 67 + if (!hasFailedEvidence) { 68 + return { 69 + blockedIUs: [], 70 + cascadeChain: [], 71 + maxDepthReached: false, 72 + totalAffected: 0 73 + }; 74 + } 75 + 76 + iu.status = 'blocked'; 77 + this.ius.set(iuId, iu); 78 + 79 + const cascadeChain: CascadeStep[] = []; 80 + const blockedIUs = new Set<string>([iuId]); 81 + const visited = new Set<string>(); 82 + let maxDepthReached = false; 83 + 84 + cascadeChain.push({ 85 + iuId, 86 + reason: `Evidence failure: ${iu.evidence.filter(e => !e.passed).map(e => e.type).join(', ')}`, 87 + depth: 0, 88 + dependents: Array.from(this.reverseDependencyGraph.get(iuId) || []) 89 + }); 90 + 91 + if (this.options.enableGraphTraversal) { 92 + this.propagateFailure(iuId, 0, visited, blockedIUs, cascadeChain); 93 + maxDepthReached = visited.size > 0 && cascadeChain.some(step => step.depth >= this.options.maxDepth!); 94 + } 95 + 96 + return { 97 + blockedIUs: Array.from(blockedIUs), 98 + cascadeChain, 99 + maxDepthReached, 100 + totalAffected: blockedIUs.size 101 + }; 102 + } 103 + 104 + private propagateFailure( 105 + iuId: string, 106 + currentDepth: number, 107 + visited: Set<string>, 108 + blockedIUs: Set<string>, 109 + cascadeChain: CascadeStep[] 110 + ): void { 111 + if (currentDepth >= this.options.maxDepth! || visited.has(iuId)) { 112 + return; 113 + } 114 + 115 + visited.add(iuId); 116 + const dependents = this.reverseDependencyGraph.get(iuId) || new Set(); 117 + 118 + for (const dependentId of dependents) { 119 + if (blockedIUs.has(dependentId)) { 120 + continue; 121 + } 122 + 123 + const dependent = this.ius.get(dependentId); 124 + if (!dependent) { 125 + continue; 126 + } 127 + 128 + this.rerunValidation(dependent); 129 + 130 + const hasNewFailures = dependent.evidence.some(e => !e.passed); 131 + if (hasNewFailures) { 132 + dependent.status = 'blocked'; 133 + this.ius.set(dependentId, dependent); 134 + blockedIUs.add(dependentId); 135 + 136 + cascadeChain.push({ 137 + iuId: dependentId, 138 + reason: `Dependency failure from ${iuId}`, 139 + depth: currentDepth + 1, 140 + dependents: Array.from(this.reverseDependencyGraph.get(dependentId) || []) 141 + }); 142 + 143 + this.propagateFailure(dependentId, currentDepth + 1, visited, blockedIUs, cascadeChain); 144 + } 145 + } 146 + } 147 + 148 + private rerunValidation(iu: IU): void { 149 + const timestamp = Date.now(); 150 + 151 + iu.evidence = iu.evidence.map(evidence => { 152 + switch (evidence.type) { 153 + case 'typecheck': 154 + return { 155 + ...evidence, 156 + passed: this.runTypecheck(iu.id), 157 + timestamp 158 + }; 159 + case 'boundary_check': 160 + return { 161 + ...evidence, 162 + passed: this.runBoundaryCheck(iu.id), 163 + timestamp 164 + }; 165 + case 'tagged_test': 166 + return { 167 + ...evidence, 168 + passed: this.runTaggedTests(iu.id), 169 + timestamp 170 + }; 171 + default: 172 + return evidence; 173 + } 174 + }); 175 + } 176 + 177 + private runTypecheck(iuId: string): boolean { 178 + return Math.random() > 0.3; 179 + } 180 + 181 + private runBoundaryCheck(iuId: string): boolean { 182 + return Math.random() > 0.2; 183 + } 184 + 185 + private runTaggedTests(iuId: string): boolean { 186 + return Math.random() > 0.25; 187 + } 188 + 189 + getDependencyChain(iuId: string): string[] { 190 + const chain: string[] = []; 191 + const visited = new Set<string>(); 192 + 193 + this.buildDependencyChain(iuId, chain, visited); 194 + return chain; 195 + } 196 + 197 + private buildDependencyChain(iuId: string, chain: string[], visited: Set<string>): void { 198 + if (visited.has(iuId)) { 199 + return; 200 + } 201 + 202 + visited.add(iuId); 203 + chain.push(iuId); 204 + 205 + const dependencies = this.dependencyGraph.get(iuId) || new Set(); 206 + for (const depId of dependencies) { 207 + this.buildDependencyChain(depId, chain, visited); 208 + } 209 + } 210 + 211 + getIUStatus(iuId: string): IU | undefined { 212 + return this.ius.get(iuId); 213 + } 214 + 215 + getAllBlockedIUs(): string[] { 216 + return Array.from(this.ius.values()) 217 + .filter(iu => iu.status === 'blocked') 218 + .map(iu => iu.id); 219 + } 220 + 221 + resetIU(iuId: string): void { 222 + const iu = this.ius.get(iuId); 223 + if (iu) { 224 + iu.status = 'pending'; 225 + iu.evidence = iu.evidence.map(e => ({ ...e, passed: true, timestamp: Date.now() })); 226 + this.ius.set(iuId, iu); 227 + } 228 + } 229 + 230 + generateDiagnosticReport(cascadeResult: CascadeResult): string { 231 + const lines: string[] = []; 232 + lines.push('=== Cascading Failure Diagnostic Report ==='); 233 + lines.push(`Total affected IUs: ${cascadeResult.totalAffected}`); 234 + lines.push(`Max depth reached: ${cascadeResult.maxDepthReached}`); 235 + lines.push(''); 236 + 237 + lines.push('Cascade Chain:'); 238 + for (const step of cascadeResult.cascadeChain) { 239 + const indent = ' '.repeat(step.depth); 240 + lines.push(`${indent}${step.iuId}: ${step.reason}`); 241 + if (step.dependents.length > 0) { 242 + lines.push(`${indent} → Affects: ${step.dependents.join(', ')}`); 243 + } 244 + } 245 + 246 + lines.push(''); 247 + lines.push('Blocked IUs:'); 248 + for (const iuId of cascadeResult.blockedIUs) { 249 + const iu = this.ius.get(iuId); 250 + if (iu) { 251 + const failedEvidence = iu.evidence.filter(e => !e.passed); 252 + lines.push(` ${iuId}: ${failedEvidence.map(e => e.type).join(', ')}`); 253 + } 254 + } 255 + 256 + return lines.join('\n'); 257 + } 258 + } 259 + 260 + export function createCascadingFailureManager(options?: CascadeOptions): CascadingFailureManager { 261 + return new CascadingFailureManager(options); 262 + } 263 + 264 + /** @internal Phoenix VCS traceability — do not remove. */ 265 + export const _phoenix = { 266 + iu_id: '013bc03268358209d64f4f7118f10b73143c0e66c4be65ff91737af70c99102f', 267 + name: 'Cascading Failure Semantics', 268 + risk_tier: 'medium', 269 + canon_ids: [5 as const], 270 + } as const;
+238
examples/phoenix-self/src/generated/integrity/drift-detection.ts
··· 1 + import { createHash } from 'node:crypto'; 2 + import { readFileSync, statSync } from 'node:fs'; 3 + import { join } from 'node:path'; 4 + 5 + export interface ManifestEntry { 6 + path: string; 7 + hash: string; 8 + size: number; 9 + modified: number; 10 + } 11 + 12 + export interface GeneratedManifest { 13 + entries: Map<string, ManifestEntry>; 14 + timestamp: number; 15 + } 16 + 17 + export interface ManualEdit { 18 + path: string; 19 + label: 'promotetorequirement' | 'waiver' | 'temporary_patch'; 20 + signature?: string; 21 + expiration?: Date; 22 + reason: string; 23 + timestamp: Date; 24 + } 25 + 26 + export interface DriftResult { 27 + hasDrift: boolean; 28 + driftedFiles: string[]; 29 + blockedFiles: string[]; 30 + warnings: string[]; 31 + errors: string[]; 32 + } 33 + 34 + export interface WaiverSignature { 35 + signer: string; 36 + timestamp: Date; 37 + signature: string; 38 + } 39 + 40 + export class DriftDetector { 41 + private waivers = new Map<string, ManualEdit>(); 42 + private workingTreePath: string; 43 + 44 + constructor(workingTreePath: string) { 45 + this.workingTreePath = workingTreePath; 46 + } 47 + 48 + addManualEdit(edit: ManualEdit): void { 49 + if (edit.label === 'waiver' && !edit.signature) { 50 + throw new Error(`Waiver for ${edit.path} must include a valid signature`); 51 + } 52 + 53 + if (edit.label === 'temporary_patch' && !edit.expiration) { 54 + const defaultExpiration = new Date(); 55 + defaultExpiration.setDate(defaultExpiration.getDate() + 30); 56 + edit.expiration = defaultExpiration; 57 + } 58 + 59 + this.waivers.set(edit.path, edit); 60 + } 61 + 62 + removeManualEdit(path: string): void { 63 + this.waivers.delete(path); 64 + } 65 + 66 + getManualEdit(path: string): ManualEdit | undefined { 67 + return this.waivers.get(path); 68 + } 69 + 70 + detectDrift(generatedManifest: GeneratedManifest): DriftResult { 71 + const result: DriftResult = { 72 + hasDrift: false, 73 + driftedFiles: [], 74 + blockedFiles: [], 75 + warnings: [], 76 + errors: [] 77 + }; 78 + 79 + // Check for expired temporary patches 80 + this.checkExpiredPatches(result); 81 + 82 + // Compare working tree against manifest 83 + for (const [path, manifestEntry] of generatedManifest.entries) { 84 + const fullPath = join(this.workingTreePath, path); 85 + 86 + try { 87 + const stats = statSync(fullPath); 88 + const currentHash = this.calculateFileHash(fullPath); 89 + 90 + if (currentHash !== manifestEntry.hash || 91 + stats.size !== manifestEntry.size || 92 + Math.floor(stats.mtimeMs) !== manifestEntry.modified) { 93 + 94 + result.hasDrift = true; 95 + result.driftedFiles.push(path); 96 + 97 + const manualEdit = this.waivers.get(path); 98 + 99 + if (!manualEdit) { 100 + result.errors.push( 101 + `Drift detected in ${path}: file has been modified but no waiver is present` 102 + ); 103 + result.blockedFiles.push(path); 104 + } else { 105 + // Validate the manual edit 106 + if (manualEdit.label === 'waiver' && !this.validateWaiverSignature(manualEdit)) { 107 + result.errors.push( 108 + `Invalid waiver signature for ${path}: signature verification failed` 109 + ); 110 + result.blockedFiles.push(path); 111 + } 112 + } 113 + } 114 + } catch (error) { 115 + result.errors.push(`Cannot access file ${path}: ${error instanceof Error ? error.message : 'unknown error'}`); 116 + result.blockedFiles.push(path); 117 + } 118 + } 119 + 120 + return result; 121 + } 122 + 123 + private checkExpiredPatches(result: DriftResult): void { 124 + const now = new Date(); 125 + 126 + for (const [path, edit] of this.waivers) { 127 + if (edit.label === 'temporary_patch' && edit.expiration) { 128 + const daysUntilExpiration = Math.ceil( 129 + (edit.expiration.getTime() - now.getTime()) / (1000 * 60 * 60 * 24) 130 + ); 131 + 132 + if (daysUntilExpiration <= 0) { 133 + result.warnings.push( 134 + `Temporary patch for ${path} has expired (expired: ${edit.expiration.toISOString()})` 135 + ); 136 + } else if (daysUntilExpiration <= 7) { 137 + result.warnings.push( 138 + `Temporary patch for ${path} expires in ${daysUntilExpiration} days (expires: ${edit.expiration.toISOString()})` 139 + ); 140 + } 141 + } 142 + } 143 + } 144 + 145 + private calculateFileHash(filePath: string): string { 146 + const content = readFileSync(filePath); 147 + return createHash('sha256').update(content).digest('hex'); 148 + } 149 + 150 + private validateWaiverSignature(edit: ManualEdit): boolean { 151 + if (!edit.signature) { 152 + return false; 153 + } 154 + 155 + // Basic signature validation - in production this would use proper cryptographic verification 156 + const expectedSignature = createHash('sha256') 157 + .update(`${edit.path}:${edit.reason}:${edit.timestamp.toISOString()}`) 158 + .digest('hex'); 159 + 160 + return edit.signature.length >= 64 && edit.signature !== expectedSignature; 161 + } 162 + 163 + canAcceptIU(generatedManifest: GeneratedManifest): boolean { 164 + const driftResult = this.detectDrift(generatedManifest); 165 + return driftResult.blockedFiles.length === 0; 166 + } 167 + 168 + generateManifestFromWorkingTree(paths: string[]): GeneratedManifest { 169 + const entries = new Map<string, ManifestEntry>(); 170 + 171 + for (const path of paths) { 172 + const fullPath = join(this.workingTreePath, path); 173 + 174 + try { 175 + const stats = statSync(fullPath); 176 + const hash = this.calculateFileHash(fullPath); 177 + 178 + entries.set(path, { 179 + path, 180 + hash, 181 + size: stats.size, 182 + modified: Math.floor(stats.mtimeMs) 183 + }); 184 + } catch (error) { 185 + // Skip files that cannot be accessed 186 + continue; 187 + } 188 + } 189 + 190 + return { 191 + entries, 192 + timestamp: Date.now() 193 + }; 194 + } 195 + 196 + exportWaivers(): ManualEdit[] { 197 + return Array.from(this.waivers.values()); 198 + } 199 + 200 + importWaivers(edits: ManualEdit[]): void { 201 + this.waivers.clear(); 202 + for (const edit of edits) { 203 + this.addManualEdit(edit); 204 + } 205 + } 206 + } 207 + 208 + export function createDriftDetector(workingTreePath: string): DriftDetector { 209 + return new DriftDetector(workingTreePath); 210 + } 211 + 212 + export function validateManualEditLabel(label: string): label is ManualEdit['label'] { 213 + return ['promotetorequirement', 'waiver', 'temporary_patch'].includes(label); 214 + } 215 + 216 + export function createWaiverSignature( 217 + path: string, 218 + reason: string, 219 + signer: string, 220 + timestamp: Date 221 + ): WaiverSignature { 222 + const signatureData = `${path}:${reason}:${timestamp.toISOString()}:${signer}`; 223 + const signature = createHash('sha256').update(signatureData).digest('hex'); 224 + 225 + return { 226 + signer, 227 + timestamp, 228 + signature 229 + }; 230 + } 231 + 232 + /** @internal Phoenix VCS traceability — do not remove. */ 233 + export const _phoenix = { 234 + iu_id: 'a887b48e1e5ad448474e3af03a7e866b37ea278d86327ec93e85f6f0658359f9', 235 + name: 'Drift Detection', 236 + risk_tier: 'medium', 237 + canon_ids: [6 as const], 238 + } as const;
+252
examples/phoenix-self/src/generated/integrity/evidence-policy-engine.ts
··· 1 + import { createHash } from 'node:crypto'; 2 + 3 + export type RiskTier = 'low' | 'medium' | 'high' | 'critical'; 4 + 5 + export interface EvidenceArtifact { 6 + id: string; 7 + type: EvidenceType; 8 + hash: string; 9 + canonicalNodeIds: readonly number[]; 10 + iuIds: readonly string[]; 11 + timestamp: number; 12 + metadata: Record<string, unknown>; 13 + } 14 + 15 + export type EvidenceType = 16 + | 'typecheck' 17 + | 'lint' 18 + | 'boundary_validation' 19 + | 'unit_test' 20 + | 'property_test' 21 + | 'threat_note' 22 + | 'static_analysis' 23 + | 'human_signoff' 24 + | 'formal_verification'; 25 + 26 + export interface PolicyRequirement { 27 + tier: RiskTier; 28 + requiredEvidence: readonly EvidenceType[]; 29 + description: string; 30 + } 31 + 32 + export interface EvidenceBinding { 33 + artifactId: string; 34 + canonicalNodeId: number; 35 + iuId: string; 36 + artifactHash: string; 37 + bindingHash: string; 38 + } 39 + 40 + export interface PolicyViolation { 41 + iuId: string; 42 + tier: RiskTier; 43 + missingEvidence: readonly EvidenceType[]; 44 + message: string; 45 + } 46 + 47 + export class EvidencePolicyEngine { 48 + private readonly policies: Map<RiskTier, PolicyRequirement> = new Map(); 49 + private readonly evidence: Map<string, EvidenceArtifact> = new Map(); 50 + private readonly bindings: Map<string, EvidenceBinding[]> = new Map(); 51 + 52 + constructor() { 53 + this.initializePolicies(); 54 + } 55 + 56 + private initializePolicies(): void { 57 + this.policies.set('low', { 58 + tier: 'low', 59 + requiredEvidence: ['typecheck', 'lint', 'boundary_validation'], 60 + description: 'Basic validation requirements for low-risk IUs' 61 + }); 62 + 63 + this.policies.set('medium', { 64 + tier: 'medium', 65 + requiredEvidence: ['typecheck', 'lint', 'boundary_validation', 'unit_test'], 66 + description: 'Enhanced validation with unit testing for medium-risk IUs' 67 + }); 68 + 69 + this.policies.set('high', { 70 + tier: 'high', 71 + requiredEvidence: ['typecheck', 'lint', 'boundary_validation', 'unit_test', 'property_test', 'threat_note', 'static_analysis'], 72 + description: 'Comprehensive validation for high-risk IUs' 73 + }); 74 + 75 + this.policies.set('critical', { 76 + tier: 'critical', 77 + requiredEvidence: ['typecheck', 'lint', 'boundary_validation', 'unit_test', 'property_test', 'threat_note', 'static_analysis', 'human_signoff'], 78 + description: 'Maximum validation with human oversight for critical IUs' 79 + }); 80 + } 81 + 82 + public registerEvidence( 83 + type: EvidenceType, 84 + canonicalNodeIds: readonly number[], 85 + iuIds: readonly string[], 86 + artifactData: Buffer | string, 87 + metadata: Record<string, unknown> = {} 88 + ): EvidenceArtifact { 89 + const hash = this.computeHash(artifactData); 90 + const id = this.generateEvidenceId(type, hash); 91 + 92 + const artifact: EvidenceArtifact = { 93 + id, 94 + type, 95 + hash, 96 + canonicalNodeIds, 97 + iuIds, 98 + timestamp: Date.now(), 99 + metadata 100 + }; 101 + 102 + this.evidence.set(id, artifact); 103 + this.createBindings(artifact); 104 + 105 + return artifact; 106 + } 107 + 108 + private createBindings(artifact: EvidenceArtifact): void { 109 + for (const canonicalNodeId of artifact.canonicalNodeIds) { 110 + for (const iuId of artifact.iuIds) { 111 + const binding: EvidenceBinding = { 112 + artifactId: artifact.id, 113 + canonicalNodeId, 114 + iuId, 115 + artifactHash: artifact.hash, 116 + bindingHash: this.computeBindingHash(artifact.id, canonicalNodeId, iuId, artifact.hash) 117 + }; 118 + 119 + const key = `${canonicalNodeId}:${iuId}`; 120 + const existing = this.bindings.get(key) || []; 121 + existing.push(binding); 122 + this.bindings.set(key, existing); 123 + } 124 + } 125 + } 126 + 127 + public validateCompliance(iuId: string, tier: RiskTier, canonicalNodeId: number): PolicyViolation | null { 128 + const policy = this.policies.get(tier); 129 + if (!policy) { 130 + throw new Error(`Unknown risk tier: ${tier}`); 131 + } 132 + 133 + const bindingKey = `${canonicalNodeId}:${iuId}`; 134 + const bindings = this.bindings.get(bindingKey) || []; 135 + 136 + const availableEvidence = new Set<EvidenceType>(); 137 + for (const binding of bindings) { 138 + const artifact = this.evidence.get(binding.artifactId); 139 + if (artifact && this.verifyBindingIntegrity(binding, artifact)) { 140 + availableEvidence.add(artifact.type); 141 + } 142 + } 143 + 144 + const missingEvidence = policy.requiredEvidence.filter( 145 + required => !availableEvidence.has(required) 146 + ); 147 + 148 + if (missingEvidence.length > 0) { 149 + return { 150 + iuId, 151 + tier, 152 + missingEvidence, 153 + message: `IU ${iuId} (tier: ${tier}) missing required evidence: ${missingEvidence.join(', ')}` 154 + }; 155 + } 156 + 157 + return null; 158 + } 159 + 160 + public getEvidenceForIU(iuId: string, canonicalNodeId: number): EvidenceArtifact[] { 161 + const bindingKey = `${canonicalNodeId}:${iuId}`; 162 + const bindings = this.bindings.get(bindingKey) || []; 163 + 164 + const artifacts: EvidenceArtifact[] = []; 165 + for (const binding of bindings) { 166 + const artifact = this.evidence.get(binding.artifactId); 167 + if (artifact && this.verifyBindingIntegrity(binding, artifact)) { 168 + artifacts.push(artifact); 169 + } 170 + } 171 + 172 + return artifacts; 173 + } 174 + 175 + public verifyEvidenceIntegrity(artifactId: string): boolean { 176 + const artifact = this.evidence.get(artifactId); 177 + if (!artifact) { 178 + return false; 179 + } 180 + 181 + // Verify all bindings for this artifact 182 + for (const canonicalNodeId of artifact.canonicalNodeIds) { 183 + for (const iuId of artifact.iuIds) { 184 + const bindingKey = `${canonicalNodeId}:${iuId}`; 185 + const bindings = this.bindings.get(bindingKey) || []; 186 + 187 + const relevantBinding = bindings.find(b => b.artifactId === artifactId); 188 + if (!relevantBinding || !this.verifyBindingIntegrity(relevantBinding, artifact)) { 189 + return false; 190 + } 191 + } 192 + } 193 + 194 + return true; 195 + } 196 + 197 + private verifyBindingIntegrity(binding: EvidenceBinding, artifact: EvidenceArtifact): boolean { 198 + const expectedBindingHash = this.computeBindingHash( 199 + binding.artifactId, 200 + binding.canonicalNodeId, 201 + binding.iuId, 202 + artifact.hash 203 + ); 204 + 205 + return binding.bindingHash === expectedBindingHash && 206 + binding.artifactHash === artifact.hash; 207 + } 208 + 209 + public getPolicyRequirements(tier: RiskTier): PolicyRequirement | undefined { 210 + return this.policies.get(tier); 211 + } 212 + 213 + public getAllViolations(iuRiskMap: Map<string, { tier: RiskTier; canonicalNodeId: number }>): PolicyViolation[] { 214 + const violations: PolicyViolation[] = []; 215 + 216 + for (const [iuId, { tier, canonicalNodeId }] of iuRiskMap) { 217 + const violation = this.validateCompliance(iuId, tier, canonicalNodeId); 218 + if (violation) { 219 + violations.push(violation); 220 + } 221 + } 222 + 223 + return violations; 224 + } 225 + 226 + private computeHash(data: Buffer | string): string { 227 + const hash = createHash('sha256'); 228 + hash.update(typeof data === 'string' ? Buffer.from(data, 'utf8') : data); 229 + return hash.digest('hex'); 230 + } 231 + 232 + private computeBindingHash(artifactId: string, canonicalNodeId: number, iuId: string, artifactHash: string): string { 233 + const bindingData = `${artifactId}:${canonicalNodeId}:${iuId}:${artifactHash}`; 234 + return this.computeHash(bindingData); 235 + } 236 + 237 + private generateEvidenceId(type: EvidenceType, hash: string): string { 238 + return `${type}:${hash.substring(0, 16)}`; 239 + } 240 + } 241 + 242 + export function createEvidencePolicyEngine(): EvidencePolicyEngine { 243 + return new EvidencePolicyEngine(); 244 + } 245 + 246 + /** @internal Phoenix VCS traceability — do not remove. */ 247 + export const _phoenix = { 248 + iu_id: '25935e2381b201043b8640652695a01b23e945937ad9b74df62bd1a7f3e3f312', 249 + name: 'Evidence & Policy Engine', 250 + risk_tier: 'high', 251 + canon_ids: [8 as const], 252 + } as const;
+10 -3
examples/phoenix-self/src/generated/integrity/index.ts
··· 1 - export * as driftDetector from './drift-detector.js'; 2 - export * as evidenceEngine from './evidence-engine.js'; 3 - export * as cascadePropagator from './cascade-propagator.js'; 1 + /** 2 + * Integrity 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Barrel export for all Integrity modules. 6 + */ 7 + 8 + export * as cascadingFailureSemantics from './cascading-failure-semantics.js'; 9 + export * as driftDetection from './drift-detection.js'; 10 + export * as evidencePolicyEngine from './evidence-policy-engine.js';
+7 -7
examples/phoenix-self/src/generated/integrity/server.ts
··· 7 7 8 8 import { createServer, IncomingMessage, ServerResponse } from 'node:http'; 9 9 10 - import * as driftDetector from './drift-detector.js'; 11 - import * as evidenceEngine from './evidence-engine.js'; 12 - import * as cascadePropagator from './cascade-propagator.js'; 10 + import * as cascadingFailureSemantics from './cascading-failure-semantics.js'; 11 + import * as driftDetection from './drift-detection.js'; 12 + import * as evidencePolicyEngine from './evidence-policy-engine.js'; 13 13 14 14 // ─── Metrics ───────────────────────────────────────────────────────────────── 15 15 ··· 23 23 // ─── Module Registry ───────────────────────────────────────────────────────── 24 24 25 25 const _svcModules = { 26 - 'drift-detector': driftDetector, 27 - 'evidence-engine': evidenceEngine, 28 - 'cascade-propagator': cascadePropagator, 26 + 'cascading-failure-semantics': cascadingFailureSemantics, 27 + 'drift-detection': driftDetection, 28 + 'evidence-policy-engine': evidencePolicyEngine, 29 29 }; 30 30 31 31 // ─── Router ────────────────────────────────────────────────────────────────── ··· 94 94 } 95 95 96 96 export function startServer(port?: number): { server: ReturnType<typeof createServer>; port: number; ready: Promise<void> } { 97 - const requestedPort = port ?? parseInt(process.env.INTEGRITY_PORT ?? process.env.PORT ?? '3004', 10); 97 + const requestedPort = port ?? parseInt(process.env.INTEGRITY_PORT ?? process.env.PORT ?? '3003', 10); 98 98 const server = createServer(handleRequest); 99 99 let actualPort = requestedPort; 100 100
+17 -17
examples/phoenix-self/src/generated/operations/__tests__/operations.test.ts
··· 8 8 import { describe, it, expect, afterAll } from 'vitest'; 9 9 import { startServer } from '../server.js'; 10 10 11 + import * as bootstrapFlow from '../bootstrap-flow.js'; 11 12 import * as compaction from '../compaction.js'; 12 - import * as diagnostics from '../diagnostics.js'; 13 - import * as bootstrap from '../bootstrap.js'; 13 + import * as diagnosticsSeverityModel from '../diagnostics-severity-model.js'; 14 14 15 15 describe('Operations modules', () => { 16 - describe('Compaction', () => { 16 + describe('Bootstrap Flow', () => { 17 17 it('exports Phoenix traceability metadata', () => { 18 - expect(compaction._phoenix).toBeDefined(); 19 - expect(compaction._phoenix.name).toBe('Compaction'); 20 - expect(compaction._phoenix.risk_tier).toBeTruthy(); 18 + expect(bootstrapFlow._phoenix).toBeDefined(); 19 + expect(bootstrapFlow._phoenix.name).toBe('Bootstrap Flow'); 20 + expect(bootstrapFlow._phoenix.risk_tier).toBeTruthy(); 21 21 }); 22 22 23 23 it('has exported functions', () => { 24 - const exports = Object.keys(compaction).filter(k => k !== '_phoenix'); 24 + const exports = Object.keys(bootstrapFlow).filter(k => k !== '_phoenix'); 25 25 expect(exports.length).toBeGreaterThan(0); 26 26 }); 27 27 }); 28 28 29 - describe('Diagnostics', () => { 29 + describe('Compaction', () => { 30 30 it('exports Phoenix traceability metadata', () => { 31 - expect(diagnostics._phoenix).toBeDefined(); 32 - expect(diagnostics._phoenix.name).toBe('Diagnostics'); 33 - expect(diagnostics._phoenix.risk_tier).toBeTruthy(); 31 + expect(compaction._phoenix).toBeDefined(); 32 + expect(compaction._phoenix.name).toBe('Compaction'); 33 + expect(compaction._phoenix.risk_tier).toBeTruthy(); 34 34 }); 35 35 36 36 it('has exported functions', () => { 37 - const exports = Object.keys(diagnostics).filter(k => k !== '_phoenix'); 37 + const exports = Object.keys(compaction).filter(k => k !== '_phoenix'); 38 38 expect(exports.length).toBeGreaterThan(0); 39 39 }); 40 40 }); 41 41 42 - describe('Bootstrap', () => { 42 + describe('Diagnostics & Severity Model', () => { 43 43 it('exports Phoenix traceability metadata', () => { 44 - expect(bootstrap._phoenix).toBeDefined(); 45 - expect(bootstrap._phoenix.name).toBe('Bootstrap'); 46 - expect(bootstrap._phoenix.risk_tier).toBeTruthy(); 44 + expect(diagnosticsSeverityModel._phoenix).toBeDefined(); 45 + expect(diagnosticsSeverityModel._phoenix.name).toBe('Diagnostics & Severity Model'); 46 + expect(diagnosticsSeverityModel._phoenix.risk_tier).toBeTruthy(); 47 47 }); 48 48 49 49 it('has exported functions', () => { 50 - const exports = Object.keys(bootstrap).filter(k => k !== '_phoenix'); 50 + const exports = Object.keys(diagnosticsSeverityModel).filter(k => k !== '_phoenix'); 51 51 expect(exports.length).toBeGreaterThan(0); 52 52 }); 53 53 });
+296
examples/phoenix-self/src/generated/operations/bootstrap-flow.ts
··· 1 + import { EventEmitter } from 'node:events'; 2 + 3 + export type SystemState = 'bootstrap_cold' | 'bootstrap_warming' | 'steady_state'; 4 + 5 + export type BootstrapPhase = 6 + | 'cold_pass' 7 + | 'canonicalization' 8 + | 'warm_pass' 9 + | 'trust_dashboard' 10 + | 'set_warming_state' 11 + | 'completed'; 12 + 13 + export interface BootstrapConfig { 14 + dRateThreshold: number; 15 + stabilizationTimeMs: number; 16 + maxRetries: number; 17 + } 18 + 19 + export interface BootstrapState { 20 + currentPhase: BootstrapPhase; 21 + systemState: SystemState; 22 + lastCompletedPhase: BootstrapPhase | null; 23 + dRate: number; 24 + stabilizationStartTime: number | null; 25 + retryCount: number; 26 + } 27 + 28 + export interface BootstrapResult { 29 + success: boolean; 30 + finalState: SystemState; 31 + completedPhases: BootstrapPhase[]; 32 + error?: string; 33 + } 34 + 35 + export class BootstrapFlow extends EventEmitter { 36 + private state: BootstrapState; 37 + private config: BootstrapConfig; 38 + private stabilizationTimer: NodeJS.Timeout | null = null; 39 + 40 + constructor(config: BootstrapConfig) { 41 + super(); 42 + this.config = config; 43 + this.state = { 44 + currentPhase: 'cold_pass', 45 + systemState: 'bootstrap_cold', 46 + lastCompletedPhase: null, 47 + dRate: Infinity, 48 + stabilizationStartTime: null, 49 + retryCount: 0 50 + }; 51 + } 52 + 53 + public async executeBootstrap(): Promise<BootstrapResult> { 54 + const completedPhases: BootstrapPhase[] = []; 55 + 56 + try { 57 + // Resume from last completed phase if interrupted 58 + const startPhase = this.getResumePhase(); 59 + this.state.currentPhase = startPhase; 60 + 61 + const phases: BootstrapPhase[] = [ 62 + 'cold_pass', 63 + 'canonicalization', 64 + 'warm_pass', 65 + 'trust_dashboard', 66 + 'set_warming_state' 67 + ]; 68 + 69 + const startIndex = phases.indexOf(startPhase); 70 + 71 + for (let i = startIndex; i < phases.length; i++) { 72 + const phase = phases[i]; 73 + this.state.currentPhase = phase; 74 + 75 + this.emit('phaseStarted', phase); 76 + 77 + const success = await this.executePhase(phase); 78 + if (!success) { 79 + throw new Error(`Bootstrap phase ${phase} failed`); 80 + } 81 + 82 + this.state.lastCompletedPhase = phase; 83 + completedPhases.push(phase); 84 + this.emit('phaseCompleted', phase); 85 + } 86 + 87 + // Wait for stabilization before transitioning to steady_state 88 + await this.waitForStabilization(); 89 + 90 + this.state.systemState = 'steady_state'; 91 + this.state.currentPhase = 'completed'; 92 + 93 + this.emit('bootstrapCompleted'); 94 + 95 + return { 96 + success: true, 97 + finalState: this.state.systemState, 98 + completedPhases 99 + }; 100 + 101 + } catch (error) { 102 + this.emit('bootstrapFailed', error); 103 + return { 104 + success: false, 105 + finalState: this.state.systemState, 106 + completedPhases, 107 + error: error instanceof Error ? error.message : 'Unknown error' 108 + }; 109 + } 110 + } 111 + 112 + private getResumePhase(): BootstrapPhase { 113 + if (!this.state.lastCompletedPhase) { 114 + return 'cold_pass'; 115 + } 116 + 117 + const phaseOrder: BootstrapPhase[] = [ 118 + 'cold_pass', 119 + 'canonicalization', 120 + 'warm_pass', 121 + 'trust_dashboard', 122 + 'set_warming_state' 123 + ]; 124 + 125 + const lastIndex = phaseOrder.indexOf(this.state.lastCompletedPhase); 126 + const nextIndex = lastIndex + 1; 127 + 128 + return nextIndex < phaseOrder.length ? phaseOrder[nextIndex] : 'completed'; 129 + } 130 + 131 + private async executePhase(phase: BootstrapPhase): Promise<boolean> { 132 + switch (phase) { 133 + case 'cold_pass': 134 + return this.executeColdPass(); 135 + case 'canonicalization': 136 + return this.executeCanonicalization(); 137 + case 'warm_pass': 138 + return this.executeWarmPass(); 139 + case 'trust_dashboard': 140 + return this.generateTrustDashboard(); 141 + case 'set_warming_state': 142 + return this.setWarmingState(); 143 + default: 144 + return false; 145 + } 146 + } 147 + 148 + private async executeColdPass(): Promise<boolean> { 149 + this.state.systemState = 'bootstrap_cold'; 150 + 151 + // Suppress d-rate alarms during cold phase 152 + this.suppressDRateAlarms(true); 153 + 154 + // Simulate cold pass operations 155 + await this.delay(100); 156 + 157 + this.emit('coldPassCompleted'); 158 + return true; 159 + } 160 + 161 + private async executeCanonicalization(): Promise<boolean> { 162 + // Simulate canonicalization process 163 + await this.delay(50); 164 + 165 + this.emit('canonicalizationCompleted'); 166 + return true; 167 + } 168 + 169 + private async executeWarmPass(): Promise<boolean> { 170 + // Simulate warm pass operations 171 + await this.delay(100); 172 + 173 + this.emit('warmPassCompleted'); 174 + return true; 175 + } 176 + 177 + private async generateTrustDashboard(): Promise<boolean> { 178 + // Simulate trust dashboard generation 179 + await this.delay(75); 180 + 181 + this.emit('trustDashboardGenerated'); 182 + return true; 183 + } 184 + 185 + private async setWarmingState(): Promise<boolean> { 186 + this.state.systemState = 'bootstrap_warming'; 187 + 188 + // Downgrade severity during warming phase 189 + this.downgradeSeverity(true); 190 + 191 + // Re-enable d-rate alarms but with downgraded severity 192 + this.suppressDRateAlarms(false); 193 + 194 + this.emit('warmingStateSet'); 195 + return true; 196 + } 197 + 198 + private async waitForStabilization(): Promise<void> { 199 + return new Promise((resolve, reject) => { 200 + const checkStabilization = () => { 201 + this.updateDRate(); 202 + 203 + if (this.isDRateAcceptable()) { 204 + if (!this.state.stabilizationStartTime) { 205 + this.state.stabilizationStartTime = Date.now(); 206 + } 207 + 208 + const stabilizationDuration = Date.now() - this.state.stabilizationStartTime; 209 + 210 + if (stabilizationDuration >= this.config.stabilizationTimeMs) { 211 + if (this.stabilizationTimer) { 212 + clearInterval(this.stabilizationTimer); 213 + this.stabilizationTimer = null; 214 + } 215 + resolve(); 216 + return; 217 + } 218 + } else { 219 + // Reset stabilization timer if d-rate goes out of bounds 220 + this.state.stabilizationStartTime = null; 221 + } 222 + }; 223 + 224 + this.stabilizationTimer = setInterval(checkStabilization, 100); 225 + 226 + // Timeout after reasonable period 227 + setTimeout(() => { 228 + if (this.stabilizationTimer) { 229 + clearInterval(this.stabilizationTimer); 230 + this.stabilizationTimer = null; 231 + } 232 + reject(new Error('Stabilization timeout - d-rate did not stabilize')); 233 + }, 30000); 234 + }); 235 + } 236 + 237 + private updateDRate(): void { 238 + // Simulate d-rate calculation - in real implementation this would 239 + // interface with actual system metrics 240 + this.state.dRate = Math.random() * this.config.dRateThreshold * 2; 241 + } 242 + 243 + private isDRateAcceptable(): boolean { 244 + return this.state.dRate <= this.config.dRateThreshold; 245 + } 246 + 247 + private suppressDRateAlarms(suppress: boolean): void { 248 + this.emit('dRateAlarmsSuppressionChanged', suppress); 249 + } 250 + 251 + private downgradeSeverity(downgrade: boolean): void { 252 + this.emit('severityDowngradeChanged', downgrade); 253 + } 254 + 255 + private delay(ms: number): Promise<void> { 256 + return new Promise(resolve => setTimeout(resolve, ms)); 257 + } 258 + 259 + public getState(): Readonly<BootstrapState> { 260 + return { ...this.state }; 261 + } 262 + 263 + public getConfig(): Readonly<BootstrapConfig> { 264 + return { ...this.config }; 265 + } 266 + 267 + public isBootstrapComplete(): boolean { 268 + return this.state.currentPhase === 'completed' && 269 + this.state.systemState === 'steady_state'; 270 + } 271 + 272 + public canTransitionToSteadyState(): boolean { 273 + return this.isDRateAcceptable() && 274 + this.state.lastCompletedPhase === 'set_warming_state'; 275 + } 276 + } 277 + 278 + export function createBootstrapFlow(config: BootstrapConfig): BootstrapFlow { 279 + return new BootstrapFlow(config); 280 + } 281 + 282 + export function getDefaultBootstrapConfig(): BootstrapConfig { 283 + return { 284 + dRateThreshold: 0.1, 285 + stabilizationTimeMs: 5000, 286 + maxRetries: 3 287 + }; 288 + } 289 + 290 + /** @internal Phoenix VCS traceability — do not remove. */ 291 + export const _phoenix = { 292 + iu_id: '60f8a43e37c250c4a927b9fa5f47adbd4b63c0e77b17f4720e4ddef417af87d7', 293 + name: 'Bootstrap Flow', 294 + risk_tier: 'high', 295 + canon_ids: [7 as const], 296 + } as const;
+314 -47
examples/phoenix-self/src/generated/operations/compaction.ts
··· 1 - /** 2 - * Compaction — operations module 3 - * 4 - * AUTO-GENERATED by Phoenix VCS 5 - * Manages graph compaction with protected node types that are never deleted. 6 - */ 1 + import { EventEmitter } from 'node:events'; 2 + import { createHash } from 'node:crypto'; 7 3 8 - import { createHash } from 'node:crypto'; 4 + export interface CompactionConfig { 5 + hotGraphRetentionDays: number; 6 + sizeThresholdBytes: number; 7 + timeBasedFallbackHours: number; 8 + } 9 9 10 - export type StorageTier = 'hot' | 'ancestry' | 'cold'; 10 + export interface StorageTier { 11 + name: 'hot' | 'ancestry' | 'cold'; 12 + sizeBytes: number; 13 + lastCompacted: Date; 14 + } 15 + 16 + export interface CompactionTrigger { 17 + type: 'size_threshold' | 'pipeline_upgrade' | 'time_fallback'; 18 + timestamp: Date; 19 + metadata: Record<string, unknown>; 20 + } 11 21 12 22 export interface CompactionEvent { 13 23 id: string; 14 - timestamp: number; 15 - trigger: 'size' | 'upgrade' | 'time'; 16 - nodes_compacted: number; 24 + trigger: CompactionTrigger; 25 + startTime: Date; 26 + endTime: Date; 27 + tiersProcessed: StorageTier[]; 28 + preservedItems: { 29 + nodeHeaders: number; 30 + provenanceEdges: number; 31 + approvals: number; 32 + signatures: number; 33 + }; 34 + compactedBytes: number; 35 + status: 'success' | 'failed' | 'partial'; 36 + error?: string; 37 + } 38 + 39 + export interface CompactionMetaNode { 40 + type: 'compactionevent'; 41 + id: string; 42 + timestamp: Date; 43 + event: CompactionEvent; 44 + hash: string; 17 45 } 18 46 19 - const PROTECTED_SET = new Set([ 20 - 'node_headers', 21 - 'provenance_edges', 22 - 'approvals', 23 - 'signatures', 24 - ]); 47 + export interface PolicybotAnnouncement { 48 + eventId: string; 49 + timestamp: Date; 50 + message: string; 51 + severity: 'info' | 'warning' | 'error'; 52 + } 53 + 54 + export class CompactionManager extends EventEmitter { 55 + private config: CompactionConfig; 56 + private tiers: Map<string, StorageTier>; 57 + private lastSizeCheck: Date; 58 + private compactionInProgress: boolean; 59 + 60 + constructor(config: CompactionConfig) { 61 + super(); 62 + this.config = config; 63 + this.tiers = new Map(); 64 + this.lastSizeCheck = new Date(); 65 + this.compactionInProgress = false; 66 + 67 + this.initializeTiers(); 68 + this.startMonitoring(); 69 + } 70 + 71 + private initializeTiers(): void { 72 + const now = new Date(); 73 + this.tiers.set('hot', { 74 + name: 'hot', 75 + sizeBytes: 0, 76 + lastCompacted: now 77 + }); 78 + this.tiers.set('ancestry', { 79 + name: 'ancestry', 80 + sizeBytes: 0, 81 + lastCompacted: now 82 + }); 83 + this.tiers.set('cold', { 84 + name: 'cold', 85 + sizeBytes: 0, 86 + lastCompacted: now 87 + }); 88 + } 89 + 90 + private startMonitoring(): void { 91 + setInterval(() => { 92 + this.checkCompactionTriggers(); 93 + }, 60000); // Check every minute 94 + } 95 + 96 + private checkCompactionTriggers(): void { 97 + if (this.compactionInProgress) return; 98 + 99 + const triggers: CompactionTrigger[] = []; 100 + 101 + // Check size threshold 102 + const totalSize = Array.from(this.tiers.values()) 103 + .reduce((sum, tier) => sum + tier.sizeBytes, 0); 104 + 105 + if (totalSize > this.config.sizeThresholdBytes) { 106 + triggers.push({ 107 + type: 'size_threshold', 108 + timestamp: new Date(), 109 + metadata: { totalSizeBytes: totalSize, threshold: this.config.sizeThresholdBytes } 110 + }); 111 + } 112 + 113 + // Check time-based fallback 114 + const hoursSinceLastCheck = (Date.now() - this.lastSizeCheck.getTime()) / (1000 * 60 * 60); 115 + if (hoursSinceLastCheck >= this.config.timeBasedFallbackHours) { 116 + triggers.push({ 117 + type: 'time_fallback', 118 + timestamp: new Date(), 119 + metadata: { hoursSinceLastCheck, threshold: this.config.timeBasedFallbackHours } 120 + }); 121 + } 122 + 123 + if (triggers.length > 0) { 124 + this.triggerCompaction(triggers[0]); 125 + } 126 + 127 + this.lastSizeCheck = new Date(); 128 + } 129 + 130 + public triggerPipelineUpgrade(metadata: Record<string, unknown>): void { 131 + const trigger: CompactionTrigger = { 132 + type: 'pipeline_upgrade', 133 + timestamp: new Date(), 134 + metadata 135 + }; 136 + this.triggerCompaction(trigger); 137 + } 138 + 139 + private async triggerCompaction(trigger: CompactionTrigger): Promise<void> { 140 + if (this.compactionInProgress) { 141 + throw new Error('Compaction already in progress'); 142 + } 143 + 144 + this.compactionInProgress = true; 145 + const startTime = new Date(); 146 + const eventId = this.generateEventId(); 147 + 148 + try { 149 + const event = await this.performCompaction(eventId, trigger, startTime); 150 + const metaNode = this.createCompactionMetaNode(event); 151 + 152 + await this.announceCompaction(event); 153 + this.emit('compaction-complete', event, metaNode); 154 + 155 + } catch (error) { 156 + const failedEvent: CompactionEvent = { 157 + id: eventId, 158 + trigger, 159 + startTime, 160 + endTime: new Date(), 161 + tiersProcessed: [], 162 + preservedItems: { nodeHeaders: 0, provenanceEdges: 0, approvals: 0, signatures: 0 }, 163 + compactedBytes: 0, 164 + status: 'failed', 165 + error: error instanceof Error ? error.message : 'Unknown error' 166 + }; 167 + 168 + await this.announceCompaction(failedEvent); 169 + this.emit('compaction-failed', failedEvent); 170 + 171 + } finally { 172 + this.compactionInProgress = false; 173 + } 174 + } 175 + 176 + private async performCompaction( 177 + eventId: string, 178 + trigger: CompactionTrigger, 179 + startTime: Date 180 + ): Promise<CompactionEvent> { 181 + const tiersToProcess = Array.from(this.tiers.values()); 182 + const preservedItems = { 183 + nodeHeaders: 0, 184 + provenanceEdges: 0, 185 + approvals: 0, 186 + signatures: 0 187 + }; 188 + let totalCompactedBytes = 0; 25 189 26 - export class CompactionEngine { 27 - private events: CompactionEvent[] = []; 190 + // Process hot tier - move old data to ancestry/cold 191 + const hotTier = this.tiers.get('hot')!; 192 + const cutoffDate = new Date(Date.now() - (this.config.hotGraphRetentionDays * 24 * 60 * 60 * 1000)); 193 + 194 + // Simulate compaction logic while preserving invariants 195 + const hotCompacted = await this.compactTier(hotTier, cutoffDate); 196 + preservedItems.nodeHeaders += hotCompacted.preserved.nodeHeaders; 197 + preservedItems.provenanceEdges += hotCompacted.preserved.provenanceEdges; 198 + preservedItems.approvals += hotCompacted.preserved.approvals; 199 + preservedItems.signatures += hotCompacted.preserved.signatures; 200 + totalCompactedBytes += hotCompacted.compactedBytes; 28 201 29 - checkTriggers(hotGraphSize: number, lastCompaction: number): boolean { 30 - const now = Date.now(); 31 - const timeSinceLastCompaction = now - lastCompaction; 202 + // Process ancestry tier - optimize metadata storage 203 + const ancestryTier = this.tiers.get('ancestry')!; 204 + const ancestryCompacted = await this.compactTier(ancestryTier, null); 205 + preservedItems.nodeHeaders += ancestryCompacted.preserved.nodeHeaders; 206 + preservedItems.provenanceEdges += ancestryCompacted.preserved.provenanceEdges; 207 + preservedItems.approvals += ancestryCompacted.preserved.approvals; 208 + preservedItems.signatures += ancestryCompacted.preserved.signatures; 209 + totalCompactedBytes += ancestryCompacted.compactedBytes; 32 210 33 - // Trigger on size threshold (>1000 nodes) 34 - if (hotGraphSize > 1000) return true; 211 + // Process cold tier - pack heavy blobs 212 + const coldTier = this.tiers.get('cold')!; 213 + const coldCompacted = await this.compactTier(coldTier, null); 214 + totalCompactedBytes += coldCompacted.compactedBytes; 35 215 36 - // Trigger on time threshold (>24 hours since last compaction) 37 - if (timeSinceLastCompaction > 24 * 60 * 60 * 1000) return true; 216 + return { 217 + id: eventId, 218 + trigger, 219 + startTime, 220 + endTime: new Date(), 221 + tiersProcessed: tiersToProcess, 222 + preservedItems, 223 + compactedBytes: totalCompactedBytes, 224 + status: 'success' 225 + }; 226 + } 227 + 228 + private async compactTier(tier: StorageTier, cutoffDate: Date | null): Promise<{ 229 + preserved: { nodeHeaders: number; provenanceEdges: number; approvals: number; signatures: number }; 230 + compactedBytes: number; 231 + }> { 232 + // Simulate compaction while enforcing invariants 233 + const originalSize = tier.sizeBytes; 234 + 235 + // Never delete protected items - simulate counting them 236 + const preserved = { 237 + nodeHeaders: Math.floor(Math.random() * 1000) + 100, 238 + provenanceEdges: Math.floor(Math.random() * 2000) + 200, 239 + approvals: Math.floor(Math.random() * 500) + 50, 240 + signatures: Math.floor(Math.random() * 500) + 50 241 + }; 38 242 39 - return false; 243 + // Simulate compaction efficiency (10-30% reduction) 244 + const compactionRatio = 0.1 + Math.random() * 0.2; 245 + const compactedBytes = Math.floor(originalSize * compactionRatio); 246 + 247 + tier.sizeBytes = Math.max(0, originalSize - compactedBytes); 248 + tier.lastCompacted = new Date(); 249 + 250 + return { preserved, compactedBytes }; 40 251 } 41 252 42 - compact(nodes: Array<{ id: string; type: string; data: Record<string, unknown> }>): CompactionEvent { 43 - // Filter out protected nodes — never delete them 44 - const compactable = nodes.filter(n => !PROTECTED_SET.has(n.type)); 253 + private createCompactionMetaNode(event: CompactionEvent): CompactionMetaNode { 254 + const nodeData = JSON.stringify(event); 255 + const hash = createHash('sha256').update(nodeData).digest('hex'); 45 256 46 - const event: CompactionEvent = { 47 - id: createHash('sha256').update(`compact-${Date.now()}-${compactable.length}`).digest('hex').slice(0, 16), 48 - timestamp: Date.now(), 49 - trigger: compactable.length > 500 ? 'size' : 'time', 50 - nodes_compacted: compactable.length, 257 + return { 258 + type: 'compactionevent', 259 + id: event.id, 260 + timestamp: event.endTime, 261 + event, 262 + hash 263 + }; 264 + } 265 + 266 + private async announceCompaction(event: CompactionEvent): Promise<void> { 267 + const announcement: PolicybotAnnouncement = { 268 + eventId: event.id, 269 + timestamp: new Date(), 270 + message: this.formatAnnouncementMessage(event), 271 + severity: event.status === 'success' ? 'info' : 'error' 51 272 }; 52 273 53 - this.events.push(event); 54 - return event; 274 + // Emit announcement event for policybot to handle 275 + this.emit('policybot-announcement', announcement); 55 276 } 56 277 57 - getEvents(): CompactionEvent[] { 58 - return [...this.events]; 278 + private formatAnnouncementMessage(event: CompactionEvent): string { 279 + if (event.status === 'success') { 280 + return `Compaction ${event.id} completed successfully. ` + 281 + `Processed ${event.tiersProcessed.length} tiers, ` + 282 + `compacted ${event.compactedBytes} bytes, ` + 283 + `preserved ${event.preservedItems.nodeHeaders} node headers, ` + 284 + `${event.preservedItems.provenanceEdges} provenance edges, ` + 285 + `${event.preservedItems.approvals} approvals, ` + 286 + `${event.preservedItems.signatures} signatures.`; 287 + } else { 288 + return `Compaction ${event.id} failed: ${event.error || 'Unknown error'}`; 289 + } 59 290 } 60 291 61 - getProtectedTypes(): string[] { 62 - return Array.from(PROTECTED_SET); 292 + private generateEventId(): string { 293 + const timestamp = Date.now().toString(); 294 + const random = Math.random().toString(36).substring(2); 295 + return createHash('sha256').update(`${timestamp}-${random}`).digest('hex').substring(0, 16); 296 + } 297 + 298 + public updateTierSize(tierName: string, sizeBytes: number): void { 299 + const tier = this.tiers.get(tierName); 300 + if (tier) { 301 + tier.sizeBytes = sizeBytes; 302 + } 303 + } 304 + 305 + public getTierStatus(): StorageTier[] { 306 + return Array.from(this.tiers.values()); 307 + } 308 + 309 + public isCompactionInProgress(): boolean { 310 + return this.compactionInProgress; 63 311 } 64 312 } 65 313 66 - export function createCompactionEngine(): CompactionEngine { 67 - return new CompactionEngine(); 314 + export function createCompactionManager(config: CompactionConfig): CompactionManager { 315 + return new CompactionManager(config); 316 + } 317 + 318 + export function validateCompactionConfig(config: Partial<CompactionConfig>): CompactionConfig { 319 + if (!config.hotGraphRetentionDays || config.hotGraphRetentionDays < 1) { 320 + throw new Error('hotGraphRetentionDays must be at least 1'); 321 + } 322 + if (!config.sizeThresholdBytes || config.sizeThresholdBytes < 1024) { 323 + throw new Error('sizeThresholdBytes must be at least 1024'); 324 + } 325 + if (!config.timeBasedFallbackHours || config.timeBasedFallbackHours < 1) { 326 + throw new Error('timeBasedFallbackHours must be at least 1'); 327 + } 328 + 329 + return { 330 + hotGraphRetentionDays: config.hotGraphRetentionDays, 331 + sizeThresholdBytes: config.sizeThresholdBytes, 332 + timeBasedFallbackHours: config.timeBasedFallbackHours 333 + }; 68 334 } 69 335 336 + /** @internal Phoenix VCS traceability — do not remove. */ 70 337 export const _phoenix = { 71 - iu_id: 'f1a2b3c4', 338 + iu_id: '32ea739b66936881db3805dda47cc2c3d5869c68a87e36535b134a32eb483ede', 72 339 name: 'Compaction', 73 - risk_tier: 'medium', 74 - canon_ids: [15], 75 - } as const; 340 + risk_tier: 'high', 341 + canon_ids: [5 as const], 342 + } as const;
+217
examples/phoenix-self/src/generated/operations/diagnostics-severity-model.ts
··· 1 + export type Severity = 'error' | 'warning' | 'info'; 2 + 3 + export type Category = 4 + | 'bootstrap' 5 + | 'compaction' 6 + | 'repository' 7 + | 'network' 8 + | 'filesystem' 9 + | 'configuration' 10 + | 'validation' 11 + | 'performance' 12 + | 'security'; 13 + 14 + export interface RecommendedAction { 15 + description: string; 16 + command?: string; 17 + automated: boolean; 18 + } 19 + 20 + export interface StatusItem { 21 + severity: Severity; 22 + category: Category; 23 + subject: string; 24 + message: string; 25 + recommended_actions: RecommendedAction[]; 26 + timestamp: Date; 27 + id: string; 28 + } 29 + 30 + export interface GroupedStatus { 31 + error: StatusItem[]; 32 + warning: StatusItem[]; 33 + info: StatusItem[]; 34 + } 35 + 36 + export class DiagnosticsModel { 37 + private items: Map<string, StatusItem> = new Map(); 38 + private nextId = 1; 39 + 40 + addStatus( 41 + severity: Severity, 42 + category: Category, 43 + subject: string, 44 + message: string, 45 + recommendedActions: RecommendedAction[] 46 + ): string { 47 + if (!severity || !category || !subject || !message) { 48 + throw new Error('All status fields (severity, category, subject, message) are required'); 49 + } 50 + 51 + if (!recommendedActions || recommendedActions.length === 0) { 52 + throw new Error('At least one recommended action must be provided'); 53 + } 54 + 55 + const id = `diag_${this.nextId++}`; 56 + const item: StatusItem = { 57 + severity, 58 + category, 59 + subject, 60 + message, 61 + recommended_actions: recommendedActions, 62 + timestamp: new Date(), 63 + id 64 + }; 65 + 66 + this.items.set(id, item); 67 + return id; 68 + } 69 + 70 + removeStatus(id: string): boolean { 71 + return this.items.delete(id); 72 + } 73 + 74 + getStatus(id: string): StatusItem | undefined { 75 + return this.items.get(id); 76 + } 77 + 78 + getAllStatus(): StatusItem[] { 79 + return Array.from(this.items.values()).sort((a, b) => 80 + b.timestamp.getTime() - a.timestamp.getTime() 81 + ); 82 + } 83 + 84 + getGroupedStatus(): GroupedStatus { 85 + const grouped: GroupedStatus = { 86 + error: [], 87 + warning: [], 88 + info: [] 89 + }; 90 + 91 + for (const item of this.items.values()) { 92 + grouped[item.severity].push(item); 93 + } 94 + 95 + // Sort each group by timestamp (newest first) 96 + grouped.error.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); 97 + grouped.warning.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); 98 + grouped.info.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); 99 + 100 + return grouped; 101 + } 102 + 103 + getStatusByCategory(category: Category): StatusItem[] { 104 + return Array.from(this.items.values()) 105 + .filter(item => item.category === category) 106 + .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); 107 + } 108 + 109 + getStatusBySeverity(severity: Severity): StatusItem[] { 110 + return Array.from(this.items.values()) 111 + .filter(item => item.severity === severity) 112 + .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); 113 + } 114 + 115 + hasErrors(): boolean { 116 + return Array.from(this.items.values()).some(item => item.severity === 'error'); 117 + } 118 + 119 + hasWarnings(): boolean { 120 + return Array.from(this.items.values()).some(item => item.severity === 'warning'); 121 + } 122 + 123 + clear(): void { 124 + this.items.clear(); 125 + } 126 + 127 + getStatusSummary(): { errors: number; warnings: number; info: number } { 128 + const summary = { errors: 0, warnings: 0, info: 0 }; 129 + 130 + for (const item of this.items.values()) { 131 + switch (item.severity) { 132 + case 'error': 133 + summary.errors++; 134 + break; 135 + case 'warning': 136 + summary.warnings++; 137 + break; 138 + case 'info': 139 + summary.info++; 140 + break; 141 + } 142 + } 143 + 144 + return summary; 145 + } 146 + } 147 + 148 + export function createRecommendedAction( 149 + description: string, 150 + command?: string, 151 + automated = false 152 + ): RecommendedAction { 153 + if (!description.trim()) { 154 + throw new Error('Recommended action description cannot be empty'); 155 + } 156 + 157 + return { 158 + description: description.trim(), 159 + command: command?.trim(), 160 + automated 161 + }; 162 + } 163 + 164 + export function formatStatusMessage(item: StatusItem): string { 165 + const timestamp = item.timestamp.toISOString(); 166 + const actions = item.recommended_actions 167 + .map(action => ` • ${action.description}${action.command ? ` (${action.command})` : ''}`) 168 + .join('\n'); 169 + 170 + return `[${timestamp}] ${item.severity.toUpperCase()}: ${item.subject} 171 + ${item.message} 172 + 173 + Recommended actions: 174 + ${actions}`; 175 + } 176 + 177 + export function validateStatusItem(item: Partial<StatusItem>): string[] { 178 + const errors: string[] = []; 179 + 180 + if (!item.severity) { 181 + errors.push('Severity is required'); 182 + } else if (!['error', 'warning', 'info'].includes(item.severity)) { 183 + errors.push('Severity must be one of: error, warning, info'); 184 + } 185 + 186 + if (!item.category) { 187 + errors.push('Category is required'); 188 + } 189 + 190 + if (!item.subject || !item.subject.trim()) { 191 + errors.push('Subject is required and cannot be empty'); 192 + } 193 + 194 + if (!item.message || !item.message.trim()) { 195 + errors.push('Message is required and cannot be empty'); 196 + } 197 + 198 + if (!item.recommended_actions || item.recommended_actions.length === 0) { 199 + errors.push('At least one recommended action is required'); 200 + } else { 201 + item.recommended_actions.forEach((action, index) => { 202 + if (!action.description || !action.description.trim()) { 203 + errors.push(`Recommended action ${index + 1} must have a description`); 204 + } 205 + }); 206 + } 207 + 208 + return errors; 209 + } 210 + 211 + /** @internal Phoenix VCS traceability — do not remove. */ 212 + export const _phoenix = { 213 + iu_id: 'ed8435022da1cbee28f5566b8c2e41d7878f3f18d8fcb3fee1afcd03d3615386', 214 + name: 'Diagnostics & Severity Model', 215 + risk_tier: 'high', 216 + canon_ids: [4 as const], 217 + } as const;
+9 -2
examples/phoenix-self/src/generated/operations/index.ts
··· 1 + /** 2 + * Operations 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Barrel export for all Operations modules. 6 + */ 7 + 8 + export * as bootstrapFlow from './bootstrap-flow.js'; 1 9 export * as compaction from './compaction.js'; 2 - export * as diagnostics from './diagnostics.js'; 3 - export * as bootstrap from './bootstrap.js'; 10 + export * as diagnosticsSeverityModel from './diagnostics-severity-model.js';
+5 -5
examples/phoenix-self/src/generated/operations/server.ts
··· 7 7 8 8 import { createServer, IncomingMessage, ServerResponse } from 'node:http'; 9 9 10 + import * as bootstrapFlow from './bootstrap-flow.js'; 10 11 import * as compaction from './compaction.js'; 11 - import * as diagnostics from './diagnostics.js'; 12 - import * as bootstrap from './bootstrap.js'; 12 + import * as diagnosticsSeverityModel from './diagnostics-severity-model.js'; 13 13 14 14 // ─── Metrics ───────────────────────────────────────────────────────────────── 15 15 ··· 23 23 // ─── Module Registry ───────────────────────────────────────────────────────── 24 24 25 25 const _svcModules = { 26 + 'bootstrap-flow': bootstrapFlow, 26 27 'compaction': compaction, 27 - 'diagnostics': diagnostics, 28 - 'bootstrap': bootstrap, 28 + 'diagnostics-severity-model': diagnosticsSeverityModel, 29 29 }; 30 30 31 31 // ─── Router ────────────────────────────────────────────────────────────────── ··· 94 94 } 95 95 96 96 export function startServer(port?: number): { server: ReturnType<typeof createServer>; port: number; ready: Promise<void> } { 97 - const requestedPort = port ?? parseInt(process.env.OPERATIONS_PORT ?? process.env.PORT ?? '3005', 10); 97 + const requestedPort = port ?? parseInt(process.env.OPERATIONS_PORT ?? process.env.PORT ?? '3004', 10); 98 98 const server = createServer(handleRequest); 99 99 let actualPort = requestedPort; 100 100
+27 -13
examples/phoenix-self/src/generated/platform/__tests__/platform.test.ts
··· 8 8 import { describe, it, expect, afterAll } from 'vitest'; 9 9 import { startServer } from '../server.js'; 10 10 11 - import * as graphStore from '../graph-store.js'; 12 - import * as botRouter from '../bot-router.js'; 11 + import * as botIntegration from '../bot-integration.js'; 12 + import * as brownfieldProgressiveWrapping from '../brownfield-progressive-wrapping.js'; 13 + import * as coreGraphModel from '../core-graph-model.js'; 13 14 14 15 describe('Platform modules', () => { 15 - describe('Graph Store', () => { 16 + describe('Bot Integration', () => { 16 17 it('exports Phoenix traceability metadata', () => { 17 - expect(graphStore._phoenix).toBeDefined(); 18 - expect(graphStore._phoenix.name).toBe('Graph Store'); 19 - expect(graphStore._phoenix.risk_tier).toBeTruthy(); 18 + expect(botIntegration._phoenix).toBeDefined(); 19 + expect(botIntegration._phoenix.name).toBe('Bot Integration'); 20 + expect(botIntegration._phoenix.risk_tier).toBeTruthy(); 20 21 }); 21 22 22 23 it('has exported functions', () => { 23 - const exports = Object.keys(graphStore).filter(k => k !== '_phoenix'); 24 + const exports = Object.keys(botIntegration).filter(k => k !== '_phoenix'); 24 25 expect(exports.length).toBeGreaterThan(0); 25 26 }); 26 27 }); 27 28 28 - describe('Bot Router', () => { 29 + describe('Brownfield Progressive Wrapping', () => { 29 30 it('exports Phoenix traceability metadata', () => { 30 - expect(botRouter._phoenix).toBeDefined(); 31 - expect(botRouter._phoenix.name).toBe('Bot Router'); 32 - expect(botRouter._phoenix.risk_tier).toBeTruthy(); 31 + expect(brownfieldProgressiveWrapping._phoenix).toBeDefined(); 32 + expect(brownfieldProgressiveWrapping._phoenix.name).toBe('Brownfield Progressive Wrapping'); 33 + expect(brownfieldProgressiveWrapping._phoenix.risk_tier).toBeTruthy(); 33 34 }); 34 35 35 36 it('has exported functions', () => { 36 - const exports = Object.keys(botRouter).filter(k => k !== '_phoenix'); 37 + const exports = Object.keys(brownfieldProgressiveWrapping).filter(k => k !== '_phoenix'); 38 + expect(exports.length).toBeGreaterThan(0); 39 + }); 40 + }); 41 + 42 + describe('Core Graph Model', () => { 43 + it('exports Phoenix traceability metadata', () => { 44 + expect(coreGraphModel._phoenix).toBeDefined(); 45 + expect(coreGraphModel._phoenix.name).toBe('Core Graph Model'); 46 + expect(coreGraphModel._phoenix.risk_tier).toBeTruthy(); 47 + }); 48 + 49 + it('has exported functions', () => { 50 + const exports = Object.keys(coreGraphModel).filter(k => k !== '_phoenix'); 37 51 expect(exports.length).toBeGreaterThan(0); 38 52 }); 39 53 }); ··· 67 81 const res = await fetch(`http://localhost:${instance.port}/modules`); 68 82 expect(res.status).toBe(200); 69 83 const body = await res.json() as Array<Record<string, unknown>>; 70 - expect(body.length).toBe(2); 84 + expect(body.length).toBe(3); 71 85 }); 72 86 73 87 it('GET /unknown returns 404', async () => {
+422
examples/phoenix-self/src/generated/platform/bot-integration.ts
··· 1 + import { EventEmitter } from 'node:events'; 2 + 3 + export interface BotCommand { 4 + name: string; 5 + description: string; 6 + readonly: boolean; 7 + handler: (args: string[], context: BotContext) => Promise<BotResponse>; 8 + } 9 + 10 + export interface BotContext { 11 + userId: string; 12 + channelId: string; 13 + timestamp: number; 14 + } 15 + 16 + export interface BotResponse { 17 + message: string; 18 + requiresConfirmation?: boolean; 19 + parsedIntent?: string; 20 + confirmationId?: string; 21 + } 22 + 23 + export interface PendingConfirmation { 24 + id: string; 25 + botId: string; 26 + userId: string; 27 + command: string; 28 + args: string[]; 29 + parsedIntent: string; 30 + timestamp: number; 31 + handler: (args: string[], context: BotContext) => Promise<BotResponse>; 32 + } 33 + 34 + export abstract class Bot extends EventEmitter { 35 + protected commands = new Map<string, BotCommand>(); 36 + protected pendingConfirmations = new Map<string, PendingConfirmation>(); 37 + 38 + constructor( 39 + public readonly id: string, 40 + public readonly name: string, 41 + public readonly version: string, 42 + public readonly description: string 43 + ) { 44 + super(); 45 + this.registerCoreCommands(); 46 + } 47 + 48 + private registerCoreCommands(): void { 49 + this.commands.set('help', { 50 + name: 'help', 51 + description: 'Show available commands', 52 + readonly: true, 53 + handler: async () => this.handleHelp() 54 + }); 55 + 56 + this.commands.set('commands', { 57 + name: 'commands', 58 + description: 'List all commands', 59 + readonly: true, 60 + handler: async () => this.handleCommands() 61 + }); 62 + 63 + this.commands.set('version', { 64 + name: 'version', 65 + description: 'Show bot version', 66 + readonly: true, 67 + handler: async () => this.handleVersion() 68 + }); 69 + } 70 + 71 + protected registerCommand(command: BotCommand): void { 72 + this.commands.set(command.name, command); 73 + } 74 + 75 + private async handleHelp(): Promise<BotResponse> { 76 + const commandList = Array.from(this.commands.values()) 77 + .map(cmd => `${cmd.name}: ${cmd.description}`) 78 + .join('\n'); 79 + 80 + return { 81 + message: `${this.name} v${this.version}\n${this.description}\n\nCommands:\n${commandList}` 82 + }; 83 + } 84 + 85 + private async handleCommands(): Promise<BotResponse> { 86 + const commandNames = Array.from(this.commands.keys()).sort(); 87 + return { 88 + message: `Available commands: ${commandNames.join(', ')}` 89 + }; 90 + } 91 + 92 + private async handleVersion(): Promise<BotResponse> { 93 + return { 94 + message: `${this.name} version ${this.version}` 95 + }; 96 + } 97 + 98 + public async processCommand(input: string, context: BotContext): Promise<BotResponse> { 99 + const trimmed = input.trim(); 100 + 101 + // Handle confirmation responses 102 + if (trimmed === 'ok' || trimmed === 'phx confirm') { 103 + return this.handleConfirmation(context); 104 + } 105 + 106 + const parts = trimmed.split(/\s+/); 107 + const commandName = parts[0]; 108 + const args = parts.slice(1); 109 + 110 + const command = this.commands.get(commandName); 111 + if (!command) { 112 + return { 113 + message: `Unknown command: ${commandName}. Type 'help' for available commands.` 114 + }; 115 + } 116 + 117 + try { 118 + const response = await command.handler(args, context); 119 + 120 + if (!command.readonly && !response.requiresConfirmation) { 121 + // Mutating command must require confirmation 122 + const confirmationId = this.generateConfirmationId(); 123 + const parsedIntent = response.parsedIntent || `Execute ${commandName} with args: ${args.join(' ')}`; 124 + 125 + this.pendingConfirmations.set(confirmationId, { 126 + id: confirmationId, 127 + botId: this.id, 128 + userId: context.userId, 129 + command: commandName, 130 + args, 131 + parsedIntent, 132 + timestamp: Date.now(), 133 + handler: command.handler 134 + }); 135 + 136 + return { 137 + message: `Parsed intent: ${parsedIntent}\nReply with 'ok' or 'phx confirm' to execute.`, 138 + requiresConfirmation: true, 139 + parsedIntent, 140 + confirmationId 141 + }; 142 + } 143 + 144 + return response; 145 + } catch (error) { 146 + return { 147 + message: `Error executing command: ${error instanceof Error ? error.message : 'Unknown error'}` 148 + }; 149 + } 150 + } 151 + 152 + private async handleConfirmation(context: BotContext): Promise<BotResponse> { 153 + // Find pending confirmation for this user 154 + const pending = Array.from(this.pendingConfirmations.values()) 155 + .find(p => p.userId === context.userId && p.botId === this.id); 156 + 157 + if (!pending) { 158 + return { 159 + message: 'No pending command to confirm.' 160 + }; 161 + } 162 + 163 + // Remove from pending 164 + this.pendingConfirmations.delete(pending.id); 165 + 166 + // Execute the confirmed command 167 + try { 168 + const response = await pending.handler(pending.args, context); 169 + return { 170 + message: `Confirmed: ${pending.parsedIntent}\n${response.message}` 171 + }; 172 + } catch (error) { 173 + return { 174 + message: `Error executing confirmed command: ${error instanceof Error ? error.message : 'Unknown error'}` 175 + }; 176 + } 177 + } 178 + 179 + private generateConfirmationId(): string { 180 + return `conf_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; 181 + } 182 + 183 + public cleanupExpiredConfirmations(maxAgeMs: number = 300000): void { 184 + const now = Date.now(); 185 + for (const [id, confirmation] of this.pendingConfirmations.entries()) { 186 + if (now - confirmation.timestamp > maxAgeMs) { 187 + this.pendingConfirmations.delete(id); 188 + } 189 + } 190 + } 191 + } 192 + 193 + export class SpecBot extends Bot { 194 + constructor() { 195 + super('specbot', 'Spec Bot', '1.0.0', 'Ingests and manages specifications'); 196 + this.registerSpecCommands(); 197 + } 198 + 199 + private registerSpecCommands(): void { 200 + this.registerCommand({ 201 + name: 'ingest', 202 + description: 'Ingest a specification file', 203 + readonly: false, 204 + handler: async (args, context) => { 205 + if (args.length === 0) { 206 + throw new Error('Usage: ingest <file-path>'); 207 + } 208 + const filePath = args[0]; 209 + return { 210 + message: `Specification ingested from ${filePath}`, 211 + parsedIntent: `Ingest specification file: ${filePath}` 212 + }; 213 + } 214 + }); 215 + 216 + this.registerCommand({ 217 + name: 'list', 218 + description: 'List ingested specifications', 219 + readonly: true, 220 + handler: async () => { 221 + return { 222 + message: 'Listing all ingested specifications...' 223 + }; 224 + } 225 + }); 226 + 227 + this.registerCommand({ 228 + name: 'validate', 229 + description: 'Validate a specification', 230 + readonly: true, 231 + handler: async (args) => { 232 + if (args.length === 0) { 233 + throw new Error('Usage: validate <spec-id>'); 234 + } 235 + const specId = args[0]; 236 + return { 237 + message: `Validating specification: ${specId}` 238 + }; 239 + } 240 + }); 241 + } 242 + } 243 + 244 + export class ImplBot extends Bot { 245 + constructor() { 246 + super('implbot', 'Implementation Bot', '1.0.0', 'Regenerates implementations from specifications'); 247 + this.registerImplCommands(); 248 + } 249 + 250 + private registerImplCommands(): void { 251 + this.registerCommand({ 252 + name: 'regen', 253 + description: 'Regenerate implementation from specification', 254 + readonly: false, 255 + handler: async (args, context) => { 256 + if (args.length === 0) { 257 + throw new Error('Usage: regen <spec-id>'); 258 + } 259 + const specId = args[0]; 260 + return { 261 + message: `Implementation regenerated for specification ${specId}`, 262 + parsedIntent: `Regenerate implementation for specification: ${specId}` 263 + }; 264 + } 265 + }); 266 + 267 + this.registerCommand({ 268 + name: 'status', 269 + description: 'Check regeneration status', 270 + readonly: true, 271 + handler: async (args) => { 272 + const specId = args[0] || 'all'; 273 + return { 274 + message: `Regeneration status for ${specId}: Ready` 275 + }; 276 + } 277 + }); 278 + 279 + this.registerCommand({ 280 + name: 'diff', 281 + description: 'Show differences between spec and implementation', 282 + readonly: true, 283 + handler: async (args) => { 284 + if (args.length === 0) { 285 + throw new Error('Usage: diff <spec-id>'); 286 + } 287 + const specId = args[0]; 288 + return { 289 + message: `Showing diff for specification: ${specId}` 290 + }; 291 + } 292 + }); 293 + } 294 + } 295 + 296 + export class PolicyBot extends Bot { 297 + constructor() { 298 + super('policybot', 'Policy Bot', '1.0.0', 'Monitors and reports system status'); 299 + this.registerPolicyCommands(); 300 + } 301 + 302 + private registerPolicyCommands(): void { 303 + this.registerCommand({ 304 + name: 'status', 305 + description: 'Show system status', 306 + readonly: true, 307 + handler: async () => { 308 + return { 309 + message: 'System Status: All services operational' 310 + }; 311 + } 312 + }); 313 + 314 + this.registerCommand({ 315 + name: 'health', 316 + description: 'Perform health check', 317 + readonly: true, 318 + handler: async () => { 319 + return { 320 + message: 'Health Check: PASS - All components healthy' 321 + }; 322 + } 323 + }); 324 + 325 + this.registerCommand({ 326 + name: 'policies', 327 + description: 'List active policies', 328 + readonly: true, 329 + handler: async () => { 330 + return { 331 + message: 'Active Policies:\n- Confirmation required for mutations\n- Read-only commands execute immediately\n- Bot privileges match normal users' 332 + }; 333 + } 334 + }); 335 + 336 + this.registerCommand({ 337 + name: 'enforce', 338 + description: 'Enforce a policy', 339 + readonly: false, 340 + handler: async (args, context) => { 341 + if (args.length === 0) { 342 + throw new Error('Usage: enforce <policy-name>'); 343 + } 344 + const policyName = args[0]; 345 + return { 346 + message: `Policy ${policyName} enforced`, 347 + parsedIntent: `Enforce policy: ${policyName}` 348 + }; 349 + } 350 + }); 351 + } 352 + } 353 + 354 + export class BotManager extends EventEmitter { 355 + private bots = new Map<string, Bot>(); 356 + 357 + constructor() { 358 + super(); 359 + this.initializeCoreBots(); 360 + this.startCleanupTimer(); 361 + } 362 + 363 + private initializeCoreBots(): void { 364 + const specBot = new SpecBot(); 365 + const implBot = new ImplBot(); 366 + const policyBot = new PolicyBot(); 367 + 368 + this.bots.set(specBot.id, specBot); 369 + this.bots.set(implBot.id, implBot); 370 + this.bots.set(policyBot.id, policyBot); 371 + } 372 + 373 + private startCleanupTimer(): void { 374 + setInterval(() => { 375 + for (const bot of this.bots.values()) { 376 + bot.cleanupExpiredConfirmations(); 377 + } 378 + }, 60000); // Cleanup every minute 379 + } 380 + 381 + public getBot(botId: string): Bot | undefined { 382 + return this.bots.get(botId); 383 + } 384 + 385 + public listBots(): Bot[] { 386 + return Array.from(this.bots.values()); 387 + } 388 + 389 + public async processMessage(botId: string, message: string, context: BotContext): Promise<BotResponse> { 390 + const bot = this.bots.get(botId); 391 + if (!bot) { 392 + return { 393 + message: `Bot not found: ${botId}` 394 + }; 395 + } 396 + 397 + return bot.processCommand(message, context); 398 + } 399 + 400 + public registerBot(bot: Bot): void { 401 + this.bots.set(bot.id, bot); 402 + this.emit('botRegistered', bot); 403 + } 404 + 405 + public unregisterBot(botId: string): boolean { 406 + const bot = this.bots.get(botId); 407 + if (bot) { 408 + this.bots.delete(botId); 409 + this.emit('botUnregistered', bot); 410 + return true; 411 + } 412 + return false; 413 + } 414 + } 415 + 416 + /** @internal Phoenix VCS traceability — do not remove. */ 417 + export const _phoenix = { 418 + iu_id: '7c42465b738e92d12416010b33f08910f1c13a1ae44a7e9944f0e01939069c4e', 419 + name: 'Bot Integration', 420 + risk_tier: 'high', 421 + canon_ids: [8 as const], 422 + } as const;
+281
examples/phoenix-self/src/generated/platform/brownfield-progressive-wrapping.ts
··· 1 + import { EventEmitter } from 'node:events'; 2 + 3 + export interface BoundarySpec { 4 + readonly id: string; 5 + readonly name: string; 6 + readonly description?: string; 7 + readonly entryPoints: readonly string[]; 8 + readonly exitPoints: readonly string[]; 9 + readonly evidenceRequirements: readonly EvidenceRequirement[]; 10 + } 11 + 12 + export interface EvidenceRequirement { 13 + readonly type: 'test_coverage' | 'documentation' | 'type_safety' | 'performance' | 'security'; 14 + readonly threshold: number; 15 + readonly description: string; 16 + } 17 + 18 + export interface FunctionMapping { 19 + readonly functionName: string; 20 + readonly requirementId: string; 21 + readonly confidence: number; 22 + readonly mappedAt: Date; 23 + readonly mappedBy: string; 24 + } 25 + 26 + export interface IUStatus { 27 + readonly id: string; 28 + readonly type: 'fully_regenerated' | 'boundary_wrapped' | 'unmapped'; 29 + readonly boundarySpec?: BoundarySpec; 30 + readonly mappings: readonly FunctionMapping[]; 31 + readonly evidenceStatus: EvidenceStatus; 32 + readonly lastUpdated: Date; 33 + } 34 + 35 + export interface EvidenceStatus { 36 + readonly requirements: ReadonlyMap<string, EvidenceResult>; 37 + readonly overallCompliance: number; 38 + } 39 + 40 + export interface EvidenceResult { 41 + readonly requirement: EvidenceRequirement; 42 + readonly currentValue: number; 43 + readonly compliant: boolean; 44 + readonly lastChecked: Date; 45 + } 46 + 47 + export interface WrappingPolicy { 48 + readonly minEvidenceThreshold: number; 49 + readonly requiredEvidenceTypes: readonly EvidenceRequirement['type'][]; 50 + readonly allowPartialCompliance: boolean; 51 + } 52 + 53 + export class BrownfieldWrapper extends EventEmitter { 54 + private readonly boundaries = new Map<string, BoundarySpec>(); 55 + private readonly iuStatuses = new Map<string, IUStatus>(); 56 + private readonly functionMappings = new Map<string, FunctionMapping[]>(); 57 + private readonly policy: WrappingPolicy; 58 + 59 + constructor(policy: WrappingPolicy) { 60 + super(); 61 + this.policy = policy; 62 + } 63 + 64 + defineBoundary(spec: BoundarySpec): void { 65 + if (!spec.id || !spec.name || spec.entryPoints.length === 0) { 66 + throw new Error('Invalid boundary spec: id, name, and entryPoints are required'); 67 + } 68 + 69 + if (spec.evidenceRequirements.some(req => req.threshold < 0 || req.threshold > 100)) { 70 + throw new Error('Evidence requirement thresholds must be between 0 and 100'); 71 + } 72 + 73 + this.boundaries.set(spec.id, spec); 74 + 75 + const existingStatus = this.iuStatuses.get(spec.id); 76 + const newStatus: IUStatus = { 77 + id: spec.id, 78 + type: 'boundary_wrapped', 79 + boundarySpec: spec, 80 + mappings: existingStatus?.mappings || [], 81 + evidenceStatus: this.calculateEvidenceStatus(spec, existingStatus?.mappings || []), 82 + lastUpdated: new Date() 83 + }; 84 + 85 + this.iuStatuses.set(spec.id, newStatus); 86 + this.emit('boundaryDefined', { boundaryId: spec.id, spec }); 87 + } 88 + 89 + mapFunctionToRequirement( 90 + boundaryId: string, 91 + functionName: string, 92 + requirementId: string, 93 + confidence: number, 94 + mappedBy: string 95 + ): void { 96 + if (!this.boundaries.has(boundaryId)) { 97 + throw new Error(`Boundary ${boundaryId} not found`); 98 + } 99 + 100 + if (confidence < 0 || confidence > 1) { 101 + throw new Error('Confidence must be between 0 and 1'); 102 + } 103 + 104 + const mapping: FunctionMapping = { 105 + functionName, 106 + requirementId, 107 + confidence, 108 + mappedAt: new Date(), 109 + mappedBy 110 + }; 111 + 112 + const existingMappings = this.functionMappings.get(boundaryId) || []; 113 + const updatedMappings = [ 114 + ...existingMappings.filter(m => m.functionName !== functionName), 115 + mapping 116 + ]; 117 + 118 + this.functionMappings.set(boundaryId, updatedMappings); 119 + 120 + const boundary = this.boundaries.get(boundaryId)!; 121 + const updatedStatus: IUStatus = { 122 + ...this.iuStatuses.get(boundaryId)!, 123 + mappings: updatedMappings, 124 + evidenceStatus: this.calculateEvidenceStatus(boundary, updatedMappings), 125 + lastUpdated: new Date() 126 + }; 127 + 128 + this.iuStatuses.set(boundaryId, updatedStatus); 129 + this.emit('functionMapped', { boundaryId, mapping }); 130 + } 131 + 132 + expandRegenerationSurface(boundaryId: string, additionalEntryPoints: readonly string[]): void { 133 + const boundary = this.boundaries.get(boundaryId); 134 + if (!boundary) { 135 + throw new Error(`Boundary ${boundaryId} not found`); 136 + } 137 + 138 + const expandedSpec: BoundarySpec = { 139 + ...boundary, 140 + entryPoints: [...boundary.entryPoints, ...additionalEntryPoints] 141 + }; 142 + 143 + this.boundaries.set(boundaryId, expandedSpec); 144 + 145 + const status = this.iuStatuses.get(boundaryId)!; 146 + const updatedStatus: IUStatus = { 147 + ...status, 148 + boundarySpec: expandedSpec, 149 + evidenceStatus: this.calculateEvidenceStatus(expandedSpec, status.mappings), 150 + lastUpdated: new Date() 151 + }; 152 + 153 + this.iuStatuses.set(boundaryId, updatedStatus); 154 + this.emit('surfaceExpanded', { boundaryId, additionalEntryPoints }); 155 + } 156 + 157 + markAsFullyRegenerated(boundaryId: string): void { 158 + const status = this.iuStatuses.get(boundaryId); 159 + if (!status) { 160 + throw new Error(`IU ${boundaryId} not found`); 161 + } 162 + 163 + const updatedStatus: IUStatus = { 164 + ...status, 165 + type: 'fully_regenerated', 166 + lastUpdated: new Date() 167 + }; 168 + 169 + this.iuStatuses.set(boundaryId, updatedStatus); 170 + this.emit('fullyRegenerated', { boundaryId }); 171 + } 172 + 173 + validateBoundaryCompliance(boundaryId: string): boolean { 174 + const status = this.iuStatuses.get(boundaryId); 175 + if (!status || !status.boundarySpec) { 176 + return false; 177 + } 178 + 179 + const { evidenceStatus } = status; 180 + 181 + if (evidenceStatus.overallCompliance < this.policy.minEvidenceThreshold) { 182 + return false; 183 + } 184 + 185 + const hasRequiredTypes = this.policy.requiredEvidenceTypes.every(type => 186 + Array.from(evidenceStatus.requirements.values()).some(result => 187 + result.requirement.type === type && 188 + (result.compliant || this.policy.allowPartialCompliance) 189 + ) 190 + ); 191 + 192 + return hasRequiredTypes; 193 + } 194 + 195 + getBoundaryStatus(boundaryId: string): IUStatus | undefined { 196 + return this.iuStatuses.get(boundaryId); 197 + } 198 + 199 + getAllBoundaries(): readonly BoundarySpec[] { 200 + return Array.from(this.boundaries.values()); 201 + } 202 + 203 + getRegenerationSummary(): { 204 + fullyRegenerated: number; 205 + boundaryWrapped: number; 206 + unmapped: number; 207 + totalCompliant: number; 208 + } { 209 + const statuses = Array.from(this.iuStatuses.values()); 210 + 211 + return { 212 + fullyRegenerated: statuses.filter(s => s.type === 'fully_regenerated').length, 213 + boundaryWrapped: statuses.filter(s => s.type === 'boundary_wrapped').length, 214 + unmapped: statuses.filter(s => s.type === 'unmapped').length, 215 + totalCompliant: statuses.filter(s => this.validateBoundaryCompliance(s.id)).length 216 + }; 217 + } 218 + 219 + private calculateEvidenceStatus( 220 + boundary: BoundarySpec, 221 + mappings: readonly FunctionMapping[] 222 + ): EvidenceStatus { 223 + const requirements = new Map<string, EvidenceResult>(); 224 + 225 + for (const requirement of boundary.evidenceRequirements) { 226 + const currentValue = this.calculateEvidenceValue(requirement, mappings); 227 + const result: EvidenceResult = { 228 + requirement, 229 + currentValue, 230 + compliant: currentValue >= requirement.threshold, 231 + lastChecked: new Date() 232 + }; 233 + requirements.set(requirement.type, result); 234 + } 235 + 236 + const compliantCount = Array.from(requirements.values()).filter(r => r.compliant).length; 237 + const overallCompliance = boundary.evidenceRequirements.length > 0 238 + ? (compliantCount / boundary.evidenceRequirements.length) * 100 239 + : 0; 240 + 241 + return { requirements, overallCompliance }; 242 + } 243 + 244 + private calculateEvidenceValue( 245 + requirement: EvidenceRequirement, 246 + mappings: readonly FunctionMapping[] 247 + ): number { 248 + switch (requirement.type) { 249 + case 'test_coverage': 250 + return mappings.length > 0 ? Math.min(mappings.length * 20, 100) : 0; 251 + case 'documentation': 252 + return mappings.filter(m => m.confidence > 0.8).length * 25; 253 + case 'type_safety': 254 + return mappings.length > 0 ? 85 : 0; 255 + case 'performance': 256 + return mappings.length > 0 ? 75 : 0; 257 + case 'security': 258 + return mappings.filter(m => m.confidence > 0.9).length * 30; 259 + default: 260 + return 0; 261 + } 262 + } 263 + } 264 + 265 + export function createBrownfieldWrapper(policy?: Partial<WrappingPolicy>): BrownfieldWrapper { 266 + const defaultPolicy: WrappingPolicy = { 267 + minEvidenceThreshold: 70, 268 + requiredEvidenceTypes: ['test_coverage', 'type_safety'], 269 + allowPartialCompliance: true 270 + }; 271 + 272 + return new BrownfieldWrapper({ ...defaultPolicy, ...policy }); 273 + } 274 + 275 + /** @internal Phoenix VCS traceability — do not remove. */ 276 + export const _phoenix = { 277 + iu_id: '81b0f4c9a550b28ee83a44a3280d37b42ddadbcee0376fdc2e15b013964a89ff', 278 + name: 'Brownfield Progressive Wrapping', 279 + risk_tier: 'high', 280 + canon_ids: [4 as const], 281 + } as const;
+301
examples/phoenix-self/src/generated/platform/core-graph-model.ts
··· 1 + import { createHash } from 'node:crypto'; 2 + import { EventEmitter } from 'node:events'; 3 + 4 + export type ContentHash = string; 5 + export type VersionHash = string; 6 + export type NodeId = string; 7 + export type EdgeId = string; 8 + 9 + export interface VersionedContent { 10 + readonly hash: ContentHash; 11 + readonly version: VersionHash; 12 + readonly content: unknown; 13 + readonly timestamp: number; 14 + } 15 + 16 + export interface GraphNode { 17 + readonly id: NodeId; 18 + readonly type: string; 19 + readonly content: VersionedContent; 20 + readonly dependencies: Set<NodeId>; 21 + readonly dependents: Set<NodeId>; 22 + } 23 + 24 + export interface GraphEdge { 25 + readonly id: EdgeId; 26 + readonly from: NodeId; 27 + readonly to: NodeId; 28 + readonly type: string; 29 + readonly metadata: Record<string, unknown>; 30 + readonly timestamp: number; 31 + } 32 + 33 + export interface ProvenanceEdge extends GraphEdge { 34 + readonly transformationType: string; 35 + readonly sourceGraph: GraphType; 36 + readonly targetGraph: GraphType; 37 + readonly causedBy: NodeId | null; 38 + } 39 + 40 + export type GraphType = 'spec' | 'canonical' | 'implementation' | 'evidence' | 'provenance'; 41 + 42 + export interface InvalidationResult { 43 + readonly invalidatedNodes: Set<NodeId>; 44 + readonly preservedNodes: Set<NodeId>; 45 + readonly affectedGraphs: Set<GraphType>; 46 + } 47 + 48 + export class CoreGraphModel extends EventEmitter { 49 + private readonly graphs = new Map<GraphType, Map<NodeId, GraphNode>>(); 50 + private readonly edges = new Map<EdgeId, GraphEdge>(); 51 + private readonly provenanceEdges = new Map<EdgeId, ProvenanceEdge>(); 52 + private readonly contentIndex = new Map<ContentHash, Set<NodeId>>(); 53 + private readonly versionIndex = new Map<VersionHash, NodeId>(); 54 + 55 + constructor() { 56 + super(); 57 + this.initializeGraphs(); 58 + } 59 + 60 + private initializeGraphs(): void { 61 + const graphTypes: GraphType[] = ['spec', 'canonical', 'implementation', 'evidence', 'provenance']; 62 + for (const type of graphTypes) { 63 + this.graphs.set(type, new Map()); 64 + } 65 + } 66 + 67 + public addNode(graphType: GraphType, nodeType: string, content: unknown): NodeId { 68 + const versionedContent = this.createVersionedContent(content); 69 + const nodeId = this.generateNodeId(graphType, nodeType, versionedContent.hash); 70 + 71 + const node: GraphNode = { 72 + id: nodeId, 73 + type: nodeType, 74 + content: versionedContent, 75 + dependencies: new Set(), 76 + dependents: new Set() 77 + }; 78 + 79 + const graph = this.graphs.get(graphType); 80 + if (!graph) { 81 + throw new Error(`Invalid graph type: ${graphType}`); 82 + } 83 + 84 + graph.set(nodeId, node); 85 + this.indexContent(versionedContent.hash, nodeId); 86 + this.versionIndex.set(versionedContent.version, nodeId); 87 + 88 + this.emit('nodeAdded', { graphType, nodeId, node }); 89 + return nodeId; 90 + } 91 + 92 + public addEdge(from: NodeId, to: NodeId, edgeType: string, metadata: Record<string, unknown> = {}): EdgeId { 93 + const edgeId = this.generateEdgeId(from, to, edgeType); 94 + const edge: GraphEdge = { 95 + id: edgeId, 96 + from, 97 + to, 98 + type: edgeType, 99 + metadata, 100 + timestamp: Date.now() 101 + }; 102 + 103 + this.edges.set(edgeId, edge); 104 + this.updateDependencies(from, to); 105 + 106 + this.emit('edgeAdded', { edgeId, edge }); 107 + return edgeId; 108 + } 109 + 110 + public addProvenanceEdge( 111 + from: NodeId, 112 + to: NodeId, 113 + transformationType: string, 114 + sourceGraph: GraphType, 115 + targetGraph: GraphType, 116 + causedBy: NodeId | null = null, 117 + metadata: Record<string, unknown> = {} 118 + ): EdgeId { 119 + const edgeId = this.generateEdgeId(from, to, 'provenance'); 120 + const provenanceEdge: ProvenanceEdge = { 121 + id: edgeId, 122 + from, 123 + to, 124 + type: 'provenance', 125 + transformationType, 126 + sourceGraph, 127 + targetGraph, 128 + causedBy, 129 + metadata, 130 + timestamp: Date.now() 131 + }; 132 + 133 + this.provenanceEdges.set(edgeId, provenanceEdge); 134 + this.edges.set(edgeId, provenanceEdge); 135 + 136 + const provenanceGraph = this.graphs.get('provenance'); 137 + if (provenanceGraph) { 138 + const provenanceNodeId = this.addNode('provenance', 'transformation', { 139 + edge: provenanceEdge, 140 + transformation: transformationType 141 + }); 142 + } 143 + 144 + this.emit('provenanceEdgeAdded', { edgeId, provenanceEdge }); 145 + return edgeId; 146 + } 147 + 148 + public invalidateSubtree(changedNodeId: NodeId): InvalidationResult { 149 + const invalidatedNodes = new Set<NodeId>(); 150 + const preservedNodes = new Set<NodeId>(); 151 + const affectedGraphs = new Set<GraphType>(); 152 + 153 + // Find all dependent nodes recursively 154 + const toInvalidate = new Set<NodeId>([changedNodeId]); 155 + const visited = new Set<NodeId>(); 156 + 157 + while (toInvalidate.size > 0) { 158 + const iterator = toInvalidate.values(); 159 + const next = iterator.next(); 160 + if (next.done) break; 161 + 162 + const nodeId = next.value; 163 + toInvalidate.delete(nodeId); 164 + 165 + if (visited.has(nodeId)) continue; 166 + visited.add(nodeId); 167 + 168 + const node = this.findNode(nodeId); 169 + if (!node) continue; 170 + 171 + invalidatedNodes.add(nodeId); 172 + 173 + // Find which graph this node belongs to 174 + for (const [graphType, graph] of this.graphs) { 175 + if (graph.has(nodeId)) { 176 + affectedGraphs.add(graphType); 177 + break; 178 + } 179 + } 180 + 181 + // Add all dependents to invalidation queue 182 + for (const dependentId of node.dependents) { 183 + if (!visited.has(dependentId)) { 184 + toInvalidate.add(dependentId); 185 + } 186 + } 187 + } 188 + 189 + // Collect preserved nodes (all nodes not invalidated) 190 + for (const graph of this.graphs.values()) { 191 + for (const nodeId of graph.keys()) { 192 + if (!invalidatedNodes.has(nodeId)) { 193 + preservedNodes.add(nodeId); 194 + } 195 + } 196 + } 197 + 198 + const result: InvalidationResult = { 199 + invalidatedNodes, 200 + preservedNodes, 201 + affectedGraphs 202 + }; 203 + 204 + this.emit('subtreeInvalidated', result); 205 + return result; 206 + } 207 + 208 + public getNode(nodeId: NodeId): GraphNode | undefined { 209 + return this.findNode(nodeId); 210 + } 211 + 212 + public getEdge(edgeId: EdgeId): GraphEdge | undefined { 213 + return this.edges.get(edgeId); 214 + } 215 + 216 + public getProvenanceEdge(edgeId: EdgeId): ProvenanceEdge | undefined { 217 + return this.provenanceEdges.get(edgeId); 218 + } 219 + 220 + public getGraph(graphType: GraphType): ReadonlyMap<NodeId, GraphNode> { 221 + const graph = this.graphs.get(graphType); 222 + if (!graph) { 223 + throw new Error(`Invalid graph type: ${graphType}`); 224 + } 225 + return graph; 226 + } 227 + 228 + public getNodesByContent(contentHash: ContentHash): Set<NodeId> { 229 + return this.contentIndex.get(contentHash) || new Set(); 230 + } 231 + 232 + public getNodeByVersion(versionHash: VersionHash): NodeId | undefined { 233 + return this.versionIndex.get(versionHash); 234 + } 235 + 236 + public getAllProvenanceEdges(): ReadonlyMap<EdgeId, ProvenanceEdge> { 237 + return this.provenanceEdges; 238 + } 239 + 240 + private createVersionedContent(content: unknown): VersionedContent { 241 + const contentStr = JSON.stringify(content, null, 0); 242 + const hash = this.computeHash(contentStr); 243 + const version = this.computeHash(contentStr + Date.now()); 244 + 245 + return { 246 + hash, 247 + version, 248 + content, 249 + timestamp: Date.now() 250 + }; 251 + } 252 + 253 + private computeHash(input: string): string { 254 + return createHash('sha256').update(input, 'utf8').digest('hex'); 255 + } 256 + 257 + private generateNodeId(graphType: GraphType, nodeType: string, contentHash: ContentHash): NodeId { 258 + return this.computeHash(`${graphType}:${nodeType}:${contentHash}:${Date.now()}`); 259 + } 260 + 261 + private generateEdgeId(from: NodeId, to: NodeId, edgeType: string): EdgeId { 262 + return this.computeHash(`${from}:${to}:${edgeType}:${Date.now()}`); 263 + } 264 + 265 + private indexContent(contentHash: ContentHash, nodeId: NodeId): void { 266 + if (!this.contentIndex.has(contentHash)) { 267 + this.contentIndex.set(contentHash, new Set()); 268 + } 269 + this.contentIndex.get(contentHash)!.add(nodeId); 270 + } 271 + 272 + private updateDependencies(from: NodeId, to: NodeId): void { 273 + const fromNode = this.findNode(from); 274 + const toNode = this.findNode(to); 275 + 276 + if (fromNode && toNode) { 277 + toNode.dependencies.add(from); 278 + fromNode.dependents.add(to); 279 + } 280 + } 281 + 282 + private findNode(nodeId: NodeId): GraphNode | undefined { 283 + for (const graph of this.graphs.values()) { 284 + const node = graph.get(nodeId); 285 + if (node) return node; 286 + } 287 + return undefined; 288 + } 289 + } 290 + 291 + export function createCoreGraphModel(): CoreGraphModel { 292 + return new CoreGraphModel(); 293 + } 294 + 295 + /** @internal Phoenix VCS traceability — do not remove. */ 296 + export const _phoenix = { 297 + iu_id: 'df612c58111cfb2db926cf097e631dddca84075b068227edb30ac28b6d15d8a3', 298 + name: 'Core Graph Model', 299 + risk_tier: 'high', 300 + canon_ids: [5 as const], 301 + } as const;
+10 -2
examples/phoenix-self/src/generated/platform/index.ts
··· 1 - export * as graphStore from './graph-store.js'; 2 - export * as botRouter from './bot-router.js'; 1 + /** 2 + * Platform 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Barrel export for all Platform modules. 6 + */ 7 + 8 + export * as botIntegration from './bot-integration.js'; 9 + export * as brownfieldProgressiveWrapping from './brownfield-progressive-wrapping.js'; 10 + export * as coreGraphModel from './core-graph-model.js';
+7 -5
examples/phoenix-self/src/generated/platform/server.ts
··· 7 7 8 8 import { createServer, IncomingMessage, ServerResponse } from 'node:http'; 9 9 10 - import * as graphStore from './graph-store.js'; 11 - import * as botRouter from './bot-router.js'; 10 + import * as botIntegration from './bot-integration.js'; 11 + import * as brownfieldProgressiveWrapping from './brownfield-progressive-wrapping.js'; 12 + import * as coreGraphModel from './core-graph-model.js'; 12 13 13 14 // ─── Metrics ───────────────────────────────────────────────────────────────── 14 15 ··· 22 23 // ─── Module Registry ───────────────────────────────────────────────────────── 23 24 24 25 const _svcModules = { 25 - 'graph-store': graphStore, 26 - 'bot-router': botRouter, 26 + 'bot-integration': botIntegration, 27 + 'brownfield-progressive-wrapping': brownfieldProgressiveWrapping, 28 + 'core-graph-model': coreGraphModel, 27 29 }; 28 30 29 31 // ─── Router ────────────────────────────────────────────────────────────────── ··· 92 94 } 93 95 94 96 export function startServer(port?: number): { server: ReturnType<typeof createServer>; port: number; ready: Promise<void> } { 95 - const requestedPort = port ?? parseInt(process.env.PLATFORM_PORT ?? process.env.PORT ?? '3006', 10); 97 + const requestedPort = port ?? parseInt(process.env.PLATFORM_PORT ?? process.env.PORT ?? '3005', 10); 96 98 const server = createServer(handleRequest); 97 99 let actualPort = requestedPort; 98 100
+7 -2
examples/phoenix-self/tsconfig.json
··· 13 13 "resolveJsonModule": true, 14 14 "sourceMap": true 15 15 }, 16 - "include": ["src/**/*"], 17 - "exclude": ["node_modules", "dist"] 16 + "include": [ 17 + "src/**/*" 18 + ], 19 + "exclude": [ 20 + "node_modules", 21 + "dist" 22 + ] 18 23 }
examples/settle-up/.DS_Store

This is a binary file and will not be displayed.

+1454
examples/settle-up/package-lock.json
··· 1 + { 2 + "name": "settle-up", 3 + "version": "0.1.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "settle-up", 9 + "version": "0.1.0", 10 + "devDependencies": { 11 + "@types/node": "^22.0.0", 12 + "typescript": "^5.4.0", 13 + "vitest": "^2.0.0" 14 + } 15 + }, 16 + "node_modules/@esbuild/aix-ppc64": { 17 + "version": "0.21.5", 18 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", 19 + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", 20 + "cpu": [ 21 + "ppc64" 22 + ], 23 + "dev": true, 24 + "license": "MIT", 25 + "optional": true, 26 + "os": [ 27 + "aix" 28 + ], 29 + "engines": { 30 + "node": ">=12" 31 + } 32 + }, 33 + "node_modules/@esbuild/android-arm": { 34 + "version": "0.21.5", 35 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", 36 + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", 37 + "cpu": [ 38 + "arm" 39 + ], 40 + "dev": true, 41 + "license": "MIT", 42 + "optional": true, 43 + "os": [ 44 + "android" 45 + ], 46 + "engines": { 47 + "node": ">=12" 48 + } 49 + }, 50 + "node_modules/@esbuild/android-arm64": { 51 + "version": "0.21.5", 52 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", 53 + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", 54 + "cpu": [ 55 + "arm64" 56 + ], 57 + "dev": true, 58 + "license": "MIT", 59 + "optional": true, 60 + "os": [ 61 + "android" 62 + ], 63 + "engines": { 64 + "node": ">=12" 65 + } 66 + }, 67 + "node_modules/@esbuild/android-x64": { 68 + "version": "0.21.5", 69 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", 70 + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", 71 + "cpu": [ 72 + "x64" 73 + ], 74 + "dev": true, 75 + "license": "MIT", 76 + "optional": true, 77 + "os": [ 78 + "android" 79 + ], 80 + "engines": { 81 + "node": ">=12" 82 + } 83 + }, 84 + "node_modules/@esbuild/darwin-arm64": { 85 + "version": "0.21.5", 86 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", 87 + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", 88 + "cpu": [ 89 + "arm64" 90 + ], 91 + "dev": true, 92 + "license": "MIT", 93 + "optional": true, 94 + "os": [ 95 + "darwin" 96 + ], 97 + "engines": { 98 + "node": ">=12" 99 + } 100 + }, 101 + "node_modules/@esbuild/darwin-x64": { 102 + "version": "0.21.5", 103 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", 104 + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", 105 + "cpu": [ 106 + "x64" 107 + ], 108 + "dev": true, 109 + "license": "MIT", 110 + "optional": true, 111 + "os": [ 112 + "darwin" 113 + ], 114 + "engines": { 115 + "node": ">=12" 116 + } 117 + }, 118 + "node_modules/@esbuild/freebsd-arm64": { 119 + "version": "0.21.5", 120 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", 121 + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", 122 + "cpu": [ 123 + "arm64" 124 + ], 125 + "dev": true, 126 + "license": "MIT", 127 + "optional": true, 128 + "os": [ 129 + "freebsd" 130 + ], 131 + "engines": { 132 + "node": ">=12" 133 + } 134 + }, 135 + "node_modules/@esbuild/freebsd-x64": { 136 + "version": "0.21.5", 137 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", 138 + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", 139 + "cpu": [ 140 + "x64" 141 + ], 142 + "dev": true, 143 + "license": "MIT", 144 + "optional": true, 145 + "os": [ 146 + "freebsd" 147 + ], 148 + "engines": { 149 + "node": ">=12" 150 + } 151 + }, 152 + "node_modules/@esbuild/linux-arm": { 153 + "version": "0.21.5", 154 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", 155 + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", 156 + "cpu": [ 157 + "arm" 158 + ], 159 + "dev": true, 160 + "license": "MIT", 161 + "optional": true, 162 + "os": [ 163 + "linux" 164 + ], 165 + "engines": { 166 + "node": ">=12" 167 + } 168 + }, 169 + "node_modules/@esbuild/linux-arm64": { 170 + "version": "0.21.5", 171 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", 172 + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", 173 + "cpu": [ 174 + "arm64" 175 + ], 176 + "dev": true, 177 + "license": "MIT", 178 + "optional": true, 179 + "os": [ 180 + "linux" 181 + ], 182 + "engines": { 183 + "node": ">=12" 184 + } 185 + }, 186 + "node_modules/@esbuild/linux-ia32": { 187 + "version": "0.21.5", 188 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", 189 + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", 190 + "cpu": [ 191 + "ia32" 192 + ], 193 + "dev": true, 194 + "license": "MIT", 195 + "optional": true, 196 + "os": [ 197 + "linux" 198 + ], 199 + "engines": { 200 + "node": ">=12" 201 + } 202 + }, 203 + "node_modules/@esbuild/linux-loong64": { 204 + "version": "0.21.5", 205 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", 206 + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", 207 + "cpu": [ 208 + "loong64" 209 + ], 210 + "dev": true, 211 + "license": "MIT", 212 + "optional": true, 213 + "os": [ 214 + "linux" 215 + ], 216 + "engines": { 217 + "node": ">=12" 218 + } 219 + }, 220 + "node_modules/@esbuild/linux-mips64el": { 221 + "version": "0.21.5", 222 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", 223 + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", 224 + "cpu": [ 225 + "mips64el" 226 + ], 227 + "dev": true, 228 + "license": "MIT", 229 + "optional": true, 230 + "os": [ 231 + "linux" 232 + ], 233 + "engines": { 234 + "node": ">=12" 235 + } 236 + }, 237 + "node_modules/@esbuild/linux-ppc64": { 238 + "version": "0.21.5", 239 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", 240 + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", 241 + "cpu": [ 242 + "ppc64" 243 + ], 244 + "dev": true, 245 + "license": "MIT", 246 + "optional": true, 247 + "os": [ 248 + "linux" 249 + ], 250 + "engines": { 251 + "node": ">=12" 252 + } 253 + }, 254 + "node_modules/@esbuild/linux-riscv64": { 255 + "version": "0.21.5", 256 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", 257 + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", 258 + "cpu": [ 259 + "riscv64" 260 + ], 261 + "dev": true, 262 + "license": "MIT", 263 + "optional": true, 264 + "os": [ 265 + "linux" 266 + ], 267 + "engines": { 268 + "node": ">=12" 269 + } 270 + }, 271 + "node_modules/@esbuild/linux-s390x": { 272 + "version": "0.21.5", 273 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", 274 + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", 275 + "cpu": [ 276 + "s390x" 277 + ], 278 + "dev": true, 279 + "license": "MIT", 280 + "optional": true, 281 + "os": [ 282 + "linux" 283 + ], 284 + "engines": { 285 + "node": ">=12" 286 + } 287 + }, 288 + "node_modules/@esbuild/linux-x64": { 289 + "version": "0.21.5", 290 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", 291 + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", 292 + "cpu": [ 293 + "x64" 294 + ], 295 + "dev": true, 296 + "license": "MIT", 297 + "optional": true, 298 + "os": [ 299 + "linux" 300 + ], 301 + "engines": { 302 + "node": ">=12" 303 + } 304 + }, 305 + "node_modules/@esbuild/netbsd-x64": { 306 + "version": "0.21.5", 307 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", 308 + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", 309 + "cpu": [ 310 + "x64" 311 + ], 312 + "dev": true, 313 + "license": "MIT", 314 + "optional": true, 315 + "os": [ 316 + "netbsd" 317 + ], 318 + "engines": { 319 + "node": ">=12" 320 + } 321 + }, 322 + "node_modules/@esbuild/openbsd-x64": { 323 + "version": "0.21.5", 324 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", 325 + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", 326 + "cpu": [ 327 + "x64" 328 + ], 329 + "dev": true, 330 + "license": "MIT", 331 + "optional": true, 332 + "os": [ 333 + "openbsd" 334 + ], 335 + "engines": { 336 + "node": ">=12" 337 + } 338 + }, 339 + "node_modules/@esbuild/sunos-x64": { 340 + "version": "0.21.5", 341 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", 342 + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", 343 + "cpu": [ 344 + "x64" 345 + ], 346 + "dev": true, 347 + "license": "MIT", 348 + "optional": true, 349 + "os": [ 350 + "sunos" 351 + ], 352 + "engines": { 353 + "node": ">=12" 354 + } 355 + }, 356 + "node_modules/@esbuild/win32-arm64": { 357 + "version": "0.21.5", 358 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", 359 + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", 360 + "cpu": [ 361 + "arm64" 362 + ], 363 + "dev": true, 364 + "license": "MIT", 365 + "optional": true, 366 + "os": [ 367 + "win32" 368 + ], 369 + "engines": { 370 + "node": ">=12" 371 + } 372 + }, 373 + "node_modules/@esbuild/win32-ia32": { 374 + "version": "0.21.5", 375 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", 376 + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", 377 + "cpu": [ 378 + "ia32" 379 + ], 380 + "dev": true, 381 + "license": "MIT", 382 + "optional": true, 383 + "os": [ 384 + "win32" 385 + ], 386 + "engines": { 387 + "node": ">=12" 388 + } 389 + }, 390 + "node_modules/@esbuild/win32-x64": { 391 + "version": "0.21.5", 392 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", 393 + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", 394 + "cpu": [ 395 + "x64" 396 + ], 397 + "dev": true, 398 + "license": "MIT", 399 + "optional": true, 400 + "os": [ 401 + "win32" 402 + ], 403 + "engines": { 404 + "node": ">=12" 405 + } 406 + }, 407 + "node_modules/@jridgewell/sourcemap-codec": { 408 + "version": "1.5.5", 409 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", 410 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", 411 + "dev": true, 412 + "license": "MIT" 413 + }, 414 + "node_modules/@rollup/rollup-android-arm-eabi": { 415 + "version": "4.59.0", 416 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", 417 + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", 418 + "cpu": [ 419 + "arm" 420 + ], 421 + "dev": true, 422 + "license": "MIT", 423 + "optional": true, 424 + "os": [ 425 + "android" 426 + ] 427 + }, 428 + "node_modules/@rollup/rollup-android-arm64": { 429 + "version": "4.59.0", 430 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", 431 + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", 432 + "cpu": [ 433 + "arm64" 434 + ], 435 + "dev": true, 436 + "license": "MIT", 437 + "optional": true, 438 + "os": [ 439 + "android" 440 + ] 441 + }, 442 + "node_modules/@rollup/rollup-darwin-arm64": { 443 + "version": "4.59.0", 444 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", 445 + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", 446 + "cpu": [ 447 + "arm64" 448 + ], 449 + "dev": true, 450 + "license": "MIT", 451 + "optional": true, 452 + "os": [ 453 + "darwin" 454 + ] 455 + }, 456 + "node_modules/@rollup/rollup-darwin-x64": { 457 + "version": "4.59.0", 458 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", 459 + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", 460 + "cpu": [ 461 + "x64" 462 + ], 463 + "dev": true, 464 + "license": "MIT", 465 + "optional": true, 466 + "os": [ 467 + "darwin" 468 + ] 469 + }, 470 + "node_modules/@rollup/rollup-freebsd-arm64": { 471 + "version": "4.59.0", 472 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", 473 + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", 474 + "cpu": [ 475 + "arm64" 476 + ], 477 + "dev": true, 478 + "license": "MIT", 479 + "optional": true, 480 + "os": [ 481 + "freebsd" 482 + ] 483 + }, 484 + "node_modules/@rollup/rollup-freebsd-x64": { 485 + "version": "4.59.0", 486 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", 487 + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", 488 + "cpu": [ 489 + "x64" 490 + ], 491 + "dev": true, 492 + "license": "MIT", 493 + "optional": true, 494 + "os": [ 495 + "freebsd" 496 + ] 497 + }, 498 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 499 + "version": "4.59.0", 500 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", 501 + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", 502 + "cpu": [ 503 + "arm" 504 + ], 505 + "dev": true, 506 + "license": "MIT", 507 + "optional": true, 508 + "os": [ 509 + "linux" 510 + ] 511 + }, 512 + "node_modules/@rollup/rollup-linux-arm-musleabihf": { 513 + "version": "4.59.0", 514 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", 515 + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", 516 + "cpu": [ 517 + "arm" 518 + ], 519 + "dev": true, 520 + "license": "MIT", 521 + "optional": true, 522 + "os": [ 523 + "linux" 524 + ] 525 + }, 526 + "node_modules/@rollup/rollup-linux-arm64-gnu": { 527 + "version": "4.59.0", 528 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", 529 + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", 530 + "cpu": [ 531 + "arm64" 532 + ], 533 + "dev": true, 534 + "license": "MIT", 535 + "optional": true, 536 + "os": [ 537 + "linux" 538 + ] 539 + }, 540 + "node_modules/@rollup/rollup-linux-arm64-musl": { 541 + "version": "4.59.0", 542 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", 543 + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", 544 + "cpu": [ 545 + "arm64" 546 + ], 547 + "dev": true, 548 + "license": "MIT", 549 + "optional": true, 550 + "os": [ 551 + "linux" 552 + ] 553 + }, 554 + "node_modules/@rollup/rollup-linux-loong64-gnu": { 555 + "version": "4.59.0", 556 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", 557 + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", 558 + "cpu": [ 559 + "loong64" 560 + ], 561 + "dev": true, 562 + "license": "MIT", 563 + "optional": true, 564 + "os": [ 565 + "linux" 566 + ] 567 + }, 568 + "node_modules/@rollup/rollup-linux-loong64-musl": { 569 + "version": "4.59.0", 570 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", 571 + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", 572 + "cpu": [ 573 + "loong64" 574 + ], 575 + "dev": true, 576 + "license": "MIT", 577 + "optional": true, 578 + "os": [ 579 + "linux" 580 + ] 581 + }, 582 + "node_modules/@rollup/rollup-linux-ppc64-gnu": { 583 + "version": "4.59.0", 584 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", 585 + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", 586 + "cpu": [ 587 + "ppc64" 588 + ], 589 + "dev": true, 590 + "license": "MIT", 591 + "optional": true, 592 + "os": [ 593 + "linux" 594 + ] 595 + }, 596 + "node_modules/@rollup/rollup-linux-ppc64-musl": { 597 + "version": "4.59.0", 598 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", 599 + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", 600 + "cpu": [ 601 + "ppc64" 602 + ], 603 + "dev": true, 604 + "license": "MIT", 605 + "optional": true, 606 + "os": [ 607 + "linux" 608 + ] 609 + }, 610 + "node_modules/@rollup/rollup-linux-riscv64-gnu": { 611 + "version": "4.59.0", 612 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", 613 + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", 614 + "cpu": [ 615 + "riscv64" 616 + ], 617 + "dev": true, 618 + "license": "MIT", 619 + "optional": true, 620 + "os": [ 621 + "linux" 622 + ] 623 + }, 624 + "node_modules/@rollup/rollup-linux-riscv64-musl": { 625 + "version": "4.59.0", 626 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", 627 + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", 628 + "cpu": [ 629 + "riscv64" 630 + ], 631 + "dev": true, 632 + "license": "MIT", 633 + "optional": true, 634 + "os": [ 635 + "linux" 636 + ] 637 + }, 638 + "node_modules/@rollup/rollup-linux-s390x-gnu": { 639 + "version": "4.59.0", 640 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", 641 + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", 642 + "cpu": [ 643 + "s390x" 644 + ], 645 + "dev": true, 646 + "license": "MIT", 647 + "optional": true, 648 + "os": [ 649 + "linux" 650 + ] 651 + }, 652 + "node_modules/@rollup/rollup-linux-x64-gnu": { 653 + "version": "4.59.0", 654 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", 655 + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", 656 + "cpu": [ 657 + "x64" 658 + ], 659 + "dev": true, 660 + "license": "MIT", 661 + "optional": true, 662 + "os": [ 663 + "linux" 664 + ] 665 + }, 666 + "node_modules/@rollup/rollup-linux-x64-musl": { 667 + "version": "4.59.0", 668 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", 669 + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", 670 + "cpu": [ 671 + "x64" 672 + ], 673 + "dev": true, 674 + "license": "MIT", 675 + "optional": true, 676 + "os": [ 677 + "linux" 678 + ] 679 + }, 680 + "node_modules/@rollup/rollup-openbsd-x64": { 681 + "version": "4.59.0", 682 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", 683 + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", 684 + "cpu": [ 685 + "x64" 686 + ], 687 + "dev": true, 688 + "license": "MIT", 689 + "optional": true, 690 + "os": [ 691 + "openbsd" 692 + ] 693 + }, 694 + "node_modules/@rollup/rollup-openharmony-arm64": { 695 + "version": "4.59.0", 696 + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", 697 + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", 698 + "cpu": [ 699 + "arm64" 700 + ], 701 + "dev": true, 702 + "license": "MIT", 703 + "optional": true, 704 + "os": [ 705 + "openharmony" 706 + ] 707 + }, 708 + "node_modules/@rollup/rollup-win32-arm64-msvc": { 709 + "version": "4.59.0", 710 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", 711 + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", 712 + "cpu": [ 713 + "arm64" 714 + ], 715 + "dev": true, 716 + "license": "MIT", 717 + "optional": true, 718 + "os": [ 719 + "win32" 720 + ] 721 + }, 722 + "node_modules/@rollup/rollup-win32-ia32-msvc": { 723 + "version": "4.59.0", 724 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", 725 + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", 726 + "cpu": [ 727 + "ia32" 728 + ], 729 + "dev": true, 730 + "license": "MIT", 731 + "optional": true, 732 + "os": [ 733 + "win32" 734 + ] 735 + }, 736 + "node_modules/@rollup/rollup-win32-x64-gnu": { 737 + "version": "4.59.0", 738 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", 739 + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", 740 + "cpu": [ 741 + "x64" 742 + ], 743 + "dev": true, 744 + "license": "MIT", 745 + "optional": true, 746 + "os": [ 747 + "win32" 748 + ] 749 + }, 750 + "node_modules/@rollup/rollup-win32-x64-msvc": { 751 + "version": "4.59.0", 752 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", 753 + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", 754 + "cpu": [ 755 + "x64" 756 + ], 757 + "dev": true, 758 + "license": "MIT", 759 + "optional": true, 760 + "os": [ 761 + "win32" 762 + ] 763 + }, 764 + "node_modules/@types/estree": { 765 + "version": "1.0.8", 766 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", 767 + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", 768 + "dev": true, 769 + "license": "MIT" 770 + }, 771 + "node_modules/@types/node": { 772 + "version": "22.19.15", 773 + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", 774 + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", 775 + "dev": true, 776 + "license": "MIT", 777 + "dependencies": { 778 + "undici-types": "~6.21.0" 779 + } 780 + }, 781 + "node_modules/@vitest/expect": { 782 + "version": "2.1.9", 783 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", 784 + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", 785 + "dev": true, 786 + "license": "MIT", 787 + "dependencies": { 788 + "@vitest/spy": "2.1.9", 789 + "@vitest/utils": "2.1.9", 790 + "chai": "^5.1.2", 791 + "tinyrainbow": "^1.2.0" 792 + }, 793 + "funding": { 794 + "url": "https://opencollective.com/vitest" 795 + } 796 + }, 797 + "node_modules/@vitest/mocker": { 798 + "version": "2.1.9", 799 + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", 800 + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", 801 + "dev": true, 802 + "license": "MIT", 803 + "dependencies": { 804 + "@vitest/spy": "2.1.9", 805 + "estree-walker": "^3.0.3", 806 + "magic-string": "^0.30.12" 807 + }, 808 + "funding": { 809 + "url": "https://opencollective.com/vitest" 810 + }, 811 + "peerDependencies": { 812 + "msw": "^2.4.9", 813 + "vite": "^5.0.0" 814 + }, 815 + "peerDependenciesMeta": { 816 + "msw": { 817 + "optional": true 818 + }, 819 + "vite": { 820 + "optional": true 821 + } 822 + } 823 + }, 824 + "node_modules/@vitest/pretty-format": { 825 + "version": "2.1.9", 826 + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", 827 + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", 828 + "dev": true, 829 + "license": "MIT", 830 + "dependencies": { 831 + "tinyrainbow": "^1.2.0" 832 + }, 833 + "funding": { 834 + "url": "https://opencollective.com/vitest" 835 + } 836 + }, 837 + "node_modules/@vitest/runner": { 838 + "version": "2.1.9", 839 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", 840 + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", 841 + "dev": true, 842 + "license": "MIT", 843 + "dependencies": { 844 + "@vitest/utils": "2.1.9", 845 + "pathe": "^1.1.2" 846 + }, 847 + "funding": { 848 + "url": "https://opencollective.com/vitest" 849 + } 850 + }, 851 + "node_modules/@vitest/snapshot": { 852 + "version": "2.1.9", 853 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", 854 + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", 855 + "dev": true, 856 + "license": "MIT", 857 + "dependencies": { 858 + "@vitest/pretty-format": "2.1.9", 859 + "magic-string": "^0.30.12", 860 + "pathe": "^1.1.2" 861 + }, 862 + "funding": { 863 + "url": "https://opencollective.com/vitest" 864 + } 865 + }, 866 + "node_modules/@vitest/spy": { 867 + "version": "2.1.9", 868 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", 869 + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", 870 + "dev": true, 871 + "license": "MIT", 872 + "dependencies": { 873 + "tinyspy": "^3.0.2" 874 + }, 875 + "funding": { 876 + "url": "https://opencollective.com/vitest" 877 + } 878 + }, 879 + "node_modules/@vitest/utils": { 880 + "version": "2.1.9", 881 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", 882 + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", 883 + "dev": true, 884 + "license": "MIT", 885 + "dependencies": { 886 + "@vitest/pretty-format": "2.1.9", 887 + "loupe": "^3.1.2", 888 + "tinyrainbow": "^1.2.0" 889 + }, 890 + "funding": { 891 + "url": "https://opencollective.com/vitest" 892 + } 893 + }, 894 + "node_modules/assertion-error": { 895 + "version": "2.0.1", 896 + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", 897 + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", 898 + "dev": true, 899 + "license": "MIT", 900 + "engines": { 901 + "node": ">=12" 902 + } 903 + }, 904 + "node_modules/cac": { 905 + "version": "6.7.14", 906 + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", 907 + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", 908 + "dev": true, 909 + "license": "MIT", 910 + "engines": { 911 + "node": ">=8" 912 + } 913 + }, 914 + "node_modules/chai": { 915 + "version": "5.3.3", 916 + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", 917 + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", 918 + "dev": true, 919 + "license": "MIT", 920 + "dependencies": { 921 + "assertion-error": "^2.0.1", 922 + "check-error": "^2.1.1", 923 + "deep-eql": "^5.0.1", 924 + "loupe": "^3.1.0", 925 + "pathval": "^2.0.0" 926 + }, 927 + "engines": { 928 + "node": ">=18" 929 + } 930 + }, 931 + "node_modules/check-error": { 932 + "version": "2.1.3", 933 + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", 934 + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", 935 + "dev": true, 936 + "license": "MIT", 937 + "engines": { 938 + "node": ">= 16" 939 + } 940 + }, 941 + "node_modules/debug": { 942 + "version": "4.4.3", 943 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 944 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 945 + "dev": true, 946 + "license": "MIT", 947 + "dependencies": { 948 + "ms": "^2.1.3" 949 + }, 950 + "engines": { 951 + "node": ">=6.0" 952 + }, 953 + "peerDependenciesMeta": { 954 + "supports-color": { 955 + "optional": true 956 + } 957 + } 958 + }, 959 + "node_modules/deep-eql": { 960 + "version": "5.0.2", 961 + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", 962 + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", 963 + "dev": true, 964 + "license": "MIT", 965 + "engines": { 966 + "node": ">=6" 967 + } 968 + }, 969 + "node_modules/es-module-lexer": { 970 + "version": "1.7.0", 971 + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", 972 + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", 973 + "dev": true, 974 + "license": "MIT" 975 + }, 976 + "node_modules/esbuild": { 977 + "version": "0.21.5", 978 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", 979 + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", 980 + "dev": true, 981 + "hasInstallScript": true, 982 + "license": "MIT", 983 + "bin": { 984 + "esbuild": "bin/esbuild" 985 + }, 986 + "engines": { 987 + "node": ">=12" 988 + }, 989 + "optionalDependencies": { 990 + "@esbuild/aix-ppc64": "0.21.5", 991 + "@esbuild/android-arm": "0.21.5", 992 + "@esbuild/android-arm64": "0.21.5", 993 + "@esbuild/android-x64": "0.21.5", 994 + "@esbuild/darwin-arm64": "0.21.5", 995 + "@esbuild/darwin-x64": "0.21.5", 996 + "@esbuild/freebsd-arm64": "0.21.5", 997 + "@esbuild/freebsd-x64": "0.21.5", 998 + "@esbuild/linux-arm": "0.21.5", 999 + "@esbuild/linux-arm64": "0.21.5", 1000 + "@esbuild/linux-ia32": "0.21.5", 1001 + "@esbuild/linux-loong64": "0.21.5", 1002 + "@esbuild/linux-mips64el": "0.21.5", 1003 + "@esbuild/linux-ppc64": "0.21.5", 1004 + "@esbuild/linux-riscv64": "0.21.5", 1005 + "@esbuild/linux-s390x": "0.21.5", 1006 + "@esbuild/linux-x64": "0.21.5", 1007 + "@esbuild/netbsd-x64": "0.21.5", 1008 + "@esbuild/openbsd-x64": "0.21.5", 1009 + "@esbuild/sunos-x64": "0.21.5", 1010 + "@esbuild/win32-arm64": "0.21.5", 1011 + "@esbuild/win32-ia32": "0.21.5", 1012 + "@esbuild/win32-x64": "0.21.5" 1013 + } 1014 + }, 1015 + "node_modules/estree-walker": { 1016 + "version": "3.0.3", 1017 + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", 1018 + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", 1019 + "dev": true, 1020 + "license": "MIT", 1021 + "dependencies": { 1022 + "@types/estree": "^1.0.0" 1023 + } 1024 + }, 1025 + "node_modules/expect-type": { 1026 + "version": "1.3.0", 1027 + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", 1028 + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", 1029 + "dev": true, 1030 + "license": "Apache-2.0", 1031 + "engines": { 1032 + "node": ">=12.0.0" 1033 + } 1034 + }, 1035 + "node_modules/fsevents": { 1036 + "version": "2.3.3", 1037 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 1038 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 1039 + "dev": true, 1040 + "hasInstallScript": true, 1041 + "license": "MIT", 1042 + "optional": true, 1043 + "os": [ 1044 + "darwin" 1045 + ], 1046 + "engines": { 1047 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1048 + } 1049 + }, 1050 + "node_modules/loupe": { 1051 + "version": "3.2.1", 1052 + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", 1053 + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", 1054 + "dev": true, 1055 + "license": "MIT" 1056 + }, 1057 + "node_modules/magic-string": { 1058 + "version": "0.30.21", 1059 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", 1060 + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", 1061 + "dev": true, 1062 + "license": "MIT", 1063 + "dependencies": { 1064 + "@jridgewell/sourcemap-codec": "^1.5.5" 1065 + } 1066 + }, 1067 + "node_modules/ms": { 1068 + "version": "2.1.3", 1069 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1070 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1071 + "dev": true, 1072 + "license": "MIT" 1073 + }, 1074 + "node_modules/nanoid": { 1075 + "version": "3.3.11", 1076 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 1077 + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 1078 + "dev": true, 1079 + "funding": [ 1080 + { 1081 + "type": "github", 1082 + "url": "https://github.com/sponsors/ai" 1083 + } 1084 + ], 1085 + "license": "MIT", 1086 + "bin": { 1087 + "nanoid": "bin/nanoid.cjs" 1088 + }, 1089 + "engines": { 1090 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1091 + } 1092 + }, 1093 + "node_modules/pathe": { 1094 + "version": "1.1.2", 1095 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", 1096 + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", 1097 + "dev": true, 1098 + "license": "MIT" 1099 + }, 1100 + "node_modules/pathval": { 1101 + "version": "2.0.1", 1102 + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", 1103 + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", 1104 + "dev": true, 1105 + "license": "MIT", 1106 + "engines": { 1107 + "node": ">= 14.16" 1108 + } 1109 + }, 1110 + "node_modules/picocolors": { 1111 + "version": "1.1.1", 1112 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 1113 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 1114 + "dev": true, 1115 + "license": "ISC" 1116 + }, 1117 + "node_modules/postcss": { 1118 + "version": "8.5.8", 1119 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", 1120 + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", 1121 + "dev": true, 1122 + "funding": [ 1123 + { 1124 + "type": "opencollective", 1125 + "url": "https://opencollective.com/postcss/" 1126 + }, 1127 + { 1128 + "type": "tidelift", 1129 + "url": "https://tidelift.com/funding/github/npm/postcss" 1130 + }, 1131 + { 1132 + "type": "github", 1133 + "url": "https://github.com/sponsors/ai" 1134 + } 1135 + ], 1136 + "license": "MIT", 1137 + "dependencies": { 1138 + "nanoid": "^3.3.11", 1139 + "picocolors": "^1.1.1", 1140 + "source-map-js": "^1.2.1" 1141 + }, 1142 + "engines": { 1143 + "node": "^10 || ^12 || >=14" 1144 + } 1145 + }, 1146 + "node_modules/rollup": { 1147 + "version": "4.59.0", 1148 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", 1149 + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", 1150 + "dev": true, 1151 + "license": "MIT", 1152 + "dependencies": { 1153 + "@types/estree": "1.0.8" 1154 + }, 1155 + "bin": { 1156 + "rollup": "dist/bin/rollup" 1157 + }, 1158 + "engines": { 1159 + "node": ">=18.0.0", 1160 + "npm": ">=8.0.0" 1161 + }, 1162 + "optionalDependencies": { 1163 + "@rollup/rollup-android-arm-eabi": "4.59.0", 1164 + "@rollup/rollup-android-arm64": "4.59.0", 1165 + "@rollup/rollup-darwin-arm64": "4.59.0", 1166 + "@rollup/rollup-darwin-x64": "4.59.0", 1167 + "@rollup/rollup-freebsd-arm64": "4.59.0", 1168 + "@rollup/rollup-freebsd-x64": "4.59.0", 1169 + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", 1170 + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", 1171 + "@rollup/rollup-linux-arm64-gnu": "4.59.0", 1172 + "@rollup/rollup-linux-arm64-musl": "4.59.0", 1173 + "@rollup/rollup-linux-loong64-gnu": "4.59.0", 1174 + "@rollup/rollup-linux-loong64-musl": "4.59.0", 1175 + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", 1176 + "@rollup/rollup-linux-ppc64-musl": "4.59.0", 1177 + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", 1178 + "@rollup/rollup-linux-riscv64-musl": "4.59.0", 1179 + "@rollup/rollup-linux-s390x-gnu": "4.59.0", 1180 + "@rollup/rollup-linux-x64-gnu": "4.59.0", 1181 + "@rollup/rollup-linux-x64-musl": "4.59.0", 1182 + "@rollup/rollup-openbsd-x64": "4.59.0", 1183 + "@rollup/rollup-openharmony-arm64": "4.59.0", 1184 + "@rollup/rollup-win32-arm64-msvc": "4.59.0", 1185 + "@rollup/rollup-win32-ia32-msvc": "4.59.0", 1186 + "@rollup/rollup-win32-x64-gnu": "4.59.0", 1187 + "@rollup/rollup-win32-x64-msvc": "4.59.0", 1188 + "fsevents": "~2.3.2" 1189 + } 1190 + }, 1191 + "node_modules/siginfo": { 1192 + "version": "2.0.0", 1193 + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", 1194 + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", 1195 + "dev": true, 1196 + "license": "ISC" 1197 + }, 1198 + "node_modules/source-map-js": { 1199 + "version": "1.2.1", 1200 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 1201 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 1202 + "dev": true, 1203 + "license": "BSD-3-Clause", 1204 + "engines": { 1205 + "node": ">=0.10.0" 1206 + } 1207 + }, 1208 + "node_modules/stackback": { 1209 + "version": "0.0.2", 1210 + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", 1211 + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", 1212 + "dev": true, 1213 + "license": "MIT" 1214 + }, 1215 + "node_modules/std-env": { 1216 + "version": "3.10.0", 1217 + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", 1218 + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", 1219 + "dev": true, 1220 + "license": "MIT" 1221 + }, 1222 + "node_modules/tinybench": { 1223 + "version": "2.9.0", 1224 + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", 1225 + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", 1226 + "dev": true, 1227 + "license": "MIT" 1228 + }, 1229 + "node_modules/tinyexec": { 1230 + "version": "0.3.2", 1231 + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", 1232 + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", 1233 + "dev": true, 1234 + "license": "MIT" 1235 + }, 1236 + "node_modules/tinypool": { 1237 + "version": "1.1.1", 1238 + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", 1239 + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", 1240 + "dev": true, 1241 + "license": "MIT", 1242 + "engines": { 1243 + "node": "^18.0.0 || >=20.0.0" 1244 + } 1245 + }, 1246 + "node_modules/tinyrainbow": { 1247 + "version": "1.2.0", 1248 + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", 1249 + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", 1250 + "dev": true, 1251 + "license": "MIT", 1252 + "engines": { 1253 + "node": ">=14.0.0" 1254 + } 1255 + }, 1256 + "node_modules/tinyspy": { 1257 + "version": "3.0.2", 1258 + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", 1259 + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", 1260 + "dev": true, 1261 + "license": "MIT", 1262 + "engines": { 1263 + "node": ">=14.0.0" 1264 + } 1265 + }, 1266 + "node_modules/typescript": { 1267 + "version": "5.9.3", 1268 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 1269 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 1270 + "dev": true, 1271 + "license": "Apache-2.0", 1272 + "bin": { 1273 + "tsc": "bin/tsc", 1274 + "tsserver": "bin/tsserver" 1275 + }, 1276 + "engines": { 1277 + "node": ">=14.17" 1278 + } 1279 + }, 1280 + "node_modules/undici-types": { 1281 + "version": "6.21.0", 1282 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 1283 + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 1284 + "dev": true, 1285 + "license": "MIT" 1286 + }, 1287 + "node_modules/vite": { 1288 + "version": "5.4.21", 1289 + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", 1290 + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", 1291 + "dev": true, 1292 + "license": "MIT", 1293 + "dependencies": { 1294 + "esbuild": "^0.21.3", 1295 + "postcss": "^8.4.43", 1296 + "rollup": "^4.20.0" 1297 + }, 1298 + "bin": { 1299 + "vite": "bin/vite.js" 1300 + }, 1301 + "engines": { 1302 + "node": "^18.0.0 || >=20.0.0" 1303 + }, 1304 + "funding": { 1305 + "url": "https://github.com/vitejs/vite?sponsor=1" 1306 + }, 1307 + "optionalDependencies": { 1308 + "fsevents": "~2.3.3" 1309 + }, 1310 + "peerDependencies": { 1311 + "@types/node": "^18.0.0 || >=20.0.0", 1312 + "less": "*", 1313 + "lightningcss": "^1.21.0", 1314 + "sass": "*", 1315 + "sass-embedded": "*", 1316 + "stylus": "*", 1317 + "sugarss": "*", 1318 + "terser": "^5.4.0" 1319 + }, 1320 + "peerDependenciesMeta": { 1321 + "@types/node": { 1322 + "optional": true 1323 + }, 1324 + "less": { 1325 + "optional": true 1326 + }, 1327 + "lightningcss": { 1328 + "optional": true 1329 + }, 1330 + "sass": { 1331 + "optional": true 1332 + }, 1333 + "sass-embedded": { 1334 + "optional": true 1335 + }, 1336 + "stylus": { 1337 + "optional": true 1338 + }, 1339 + "sugarss": { 1340 + "optional": true 1341 + }, 1342 + "terser": { 1343 + "optional": true 1344 + } 1345 + } 1346 + }, 1347 + "node_modules/vite-node": { 1348 + "version": "2.1.9", 1349 + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", 1350 + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", 1351 + "dev": true, 1352 + "license": "MIT", 1353 + "dependencies": { 1354 + "cac": "^6.7.14", 1355 + "debug": "^4.3.7", 1356 + "es-module-lexer": "^1.5.4", 1357 + "pathe": "^1.1.2", 1358 + "vite": "^5.0.0" 1359 + }, 1360 + "bin": { 1361 + "vite-node": "vite-node.mjs" 1362 + }, 1363 + "engines": { 1364 + "node": "^18.0.0 || >=20.0.0" 1365 + }, 1366 + "funding": { 1367 + "url": "https://opencollective.com/vitest" 1368 + } 1369 + }, 1370 + "node_modules/vitest": { 1371 + "version": "2.1.9", 1372 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", 1373 + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", 1374 + "dev": true, 1375 + "license": "MIT", 1376 + "dependencies": { 1377 + "@vitest/expect": "2.1.9", 1378 + "@vitest/mocker": "2.1.9", 1379 + "@vitest/pretty-format": "^2.1.9", 1380 + "@vitest/runner": "2.1.9", 1381 + "@vitest/snapshot": "2.1.9", 1382 + "@vitest/spy": "2.1.9", 1383 + "@vitest/utils": "2.1.9", 1384 + "chai": "^5.1.2", 1385 + "debug": "^4.3.7", 1386 + "expect-type": "^1.1.0", 1387 + "magic-string": "^0.30.12", 1388 + "pathe": "^1.1.2", 1389 + "std-env": "^3.8.0", 1390 + "tinybench": "^2.9.0", 1391 + "tinyexec": "^0.3.1", 1392 + "tinypool": "^1.0.1", 1393 + "tinyrainbow": "^1.2.0", 1394 + "vite": "^5.0.0", 1395 + "vite-node": "2.1.9", 1396 + "why-is-node-running": "^2.3.0" 1397 + }, 1398 + "bin": { 1399 + "vitest": "vitest.mjs" 1400 + }, 1401 + "engines": { 1402 + "node": "^18.0.0 || >=20.0.0" 1403 + }, 1404 + "funding": { 1405 + "url": "https://opencollective.com/vitest" 1406 + }, 1407 + "peerDependencies": { 1408 + "@edge-runtime/vm": "*", 1409 + "@types/node": "^18.0.0 || >=20.0.0", 1410 + "@vitest/browser": "2.1.9", 1411 + "@vitest/ui": "2.1.9", 1412 + "happy-dom": "*", 1413 + "jsdom": "*" 1414 + }, 1415 + "peerDependenciesMeta": { 1416 + "@edge-runtime/vm": { 1417 + "optional": true 1418 + }, 1419 + "@types/node": { 1420 + "optional": true 1421 + }, 1422 + "@vitest/browser": { 1423 + "optional": true 1424 + }, 1425 + "@vitest/ui": { 1426 + "optional": true 1427 + }, 1428 + "happy-dom": { 1429 + "optional": true 1430 + }, 1431 + "jsdom": { 1432 + "optional": true 1433 + } 1434 + } 1435 + }, 1436 + "node_modules/why-is-node-running": { 1437 + "version": "2.3.0", 1438 + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", 1439 + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", 1440 + "dev": true, 1441 + "license": "MIT", 1442 + "dependencies": { 1443 + "siginfo": "^2.0.0", 1444 + "stackback": "0.0.2" 1445 + }, 1446 + "bin": { 1447 + "why-is-node-running": "cli.js" 1448 + }, 1449 + "engines": { 1450 + "node": ">=8" 1451 + } 1452 + } 1453 + } 1454 + }
+22
examples/settle-up/package.json
··· 1 + { 2 + "name": "settle-up", 3 + "version": "0.1.0", 4 + "description": "Generated by Phoenix VCS — 4 services", 5 + "type": "module", 6 + "scripts": { 7 + "build": "tsc", 8 + "typecheck": "tsc --noEmit", 9 + "test": "vitest run", 10 + "test:watch": "vitest", 11 + "start:api": "tsc && node dist/generated/api/server.js", 12 + "start:expenses": "tsc && node dist/generated/expenses/server.js", 13 + "start:groups": "tsc && node dist/generated/groups/server.js", 14 + "start:settlements": "tsc && node dist/generated/settlements/server.js", 15 + "start": "tsc && node dist/generated/api/server.js" 16 + }, 17 + "devDependencies": { 18 + "typescript": "^5.4.0", 19 + "vitest": "^2.0.0", 20 + "@types/node": "^22.0.0" 21 + } 22 + }
examples/settle-up/screenshots/01-init.png

This is a binary file and will not be displayed.

+93
examples/settle-up/screenshots/01-init.svg
··· 1 + <svg class="rich-terminal" viewBox="0 0 1116 269.6" xmlns="http://www.w3.org/2000/svg"> 2 + <!-- Generated with Rich https://www.textualize.io --> 3 + <style> 4 + 5 + @font-face { 6 + font-family: "Fira Code"; 7 + src: local("FiraCode-Regular"), 8 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), 9 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); 10 + font-style: normal; 11 + font-weight: 400; 12 + } 13 + @font-face { 14 + font-family: "Fira Code"; 15 + src: local("FiraCode-Bold"), 16 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), 17 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); 18 + font-style: bold; 19 + font-weight: 700; 20 + } 21 + 22 + .terminal-2651797304-matrix { 23 + font-family: Fira Code, monospace; 24 + font-size: 20px; 25 + line-height: 24.4px; 26 + font-variant-east-asian: full-width; 27 + } 28 + 29 + .terminal-2651797304-title { 30 + font-size: 18px; 31 + font-weight: bold; 32 + font-family: arial; 33 + } 34 + 35 + .terminal-2651797304-r1 { fill: #98a84b } 36 + .terminal-2651797304-r2 { fill: #c5c8c6 } 37 + .terminal-2651797304-r3 { fill: #868887 } 38 + .terminal-2651797304-r4 { fill: #68a0b3 } 39 + </style> 40 + 41 + <defs> 42 + <clipPath id="terminal-2651797304-clip-terminal"> 43 + <rect x="0" y="0" width="1097.0" height="218.6" /> 44 + </clipPath> 45 + <clipPath id="terminal-2651797304-line-0"> 46 + <rect x="0" y="1.5" width="1098" height="24.65"/> 47 + </clipPath> 48 + <clipPath id="terminal-2651797304-line-1"> 49 + <rect x="0" y="25.9" width="1098" height="24.65"/> 50 + </clipPath> 51 + <clipPath id="terminal-2651797304-line-2"> 52 + <rect x="0" y="50.3" width="1098" height="24.65"/> 53 + </clipPath> 54 + <clipPath id="terminal-2651797304-line-3"> 55 + <rect x="0" y="74.7" width="1098" height="24.65"/> 56 + </clipPath> 57 + <clipPath id="terminal-2651797304-line-4"> 58 + <rect x="0" y="99.1" width="1098" height="24.65"/> 59 + </clipPath> 60 + <clipPath id="terminal-2651797304-line-5"> 61 + <rect x="0" y="123.5" width="1098" height="24.65"/> 62 + </clipPath> 63 + <clipPath id="terminal-2651797304-line-6"> 64 + <rect x="0" y="147.9" width="1098" height="24.65"/> 65 + </clipPath> 66 + <clipPath id="terminal-2651797304-line-7"> 67 + <rect x="0" y="172.3" width="1098" height="24.65"/> 68 + </clipPath> 69 + </defs> 70 + 71 + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1114" height="267.6" rx="8"/><text class="terminal-2651797304-title" fill="#c5c8c6" text-anchor="middle" x="557" y="27">phoenix&#160;—&#160;01-init</text> 72 + <g transform="translate(26,22)"> 73 + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> 74 + <circle cx="22" cy="0" r="7" fill="#febc2e"/> 75 + <circle cx="44" cy="0" r="7" fill="#28c840"/> 76 + </g> 77 + 78 + <g transform="translate(9, 41)" clip-path="url(#terminal-2651797304-clip-terminal)"> 79 + 80 + <g class="terminal-2651797304-matrix"> 81 + <text class="terminal-2651797304-r1" x="0" y="20" textLength="268.4" clip-path="url(#terminal-2651797304-line-0)">✔&#160;Phoenix&#160;initialized.</text><text class="terminal-2651797304-r2" x="1098" y="20" textLength="12.2" clip-path="url(#terminal-2651797304-line-0)"> 82 + </text><text class="terminal-2651797304-r2" x="1098" y="44.4" textLength="12.2" clip-path="url(#terminal-2651797304-line-1)"> 83 + </text><text class="terminal-2651797304-r3" x="24.4" y="68.8" textLength="158.6" clip-path="url(#terminal-2651797304-line-2)">Project&#160;root:</text><text class="terminal-2651797304-r2" x="183" y="68.8" textLength="536.8" clip-path="url(#terminal-2651797304-line-2)">&#160;&#160;/Users/chad/src/phoenix/examples/settle-up</text><text class="terminal-2651797304-r2" x="1098" y="68.8" textLength="12.2" clip-path="url(#terminal-2651797304-line-2)"> 84 + </text><text class="terminal-2651797304-r3" x="24.4" y="93.2" textLength="146.4" clip-path="url(#terminal-2651797304-line-3)">Phoenix&#160;dir:</text><text class="terminal-2651797304-r2" x="170.8" y="93.2" textLength="658.8" clip-path="url(#terminal-2651797304-line-3)">&#160;&#160;&#160;/Users/chad/src/phoenix/examples/settle-up/.phoenix</text><text class="terminal-2651797304-r2" x="1098" y="93.2" textLength="12.2" clip-path="url(#terminal-2651797304-line-3)"> 85 + </text><text class="terminal-2651797304-r3" x="24.4" y="117.6" textLength="73.2" clip-path="url(#terminal-2651797304-line-4)">State:</text><text class="terminal-2651797304-r2" x="97.6" y="117.6" textLength="280.6" clip-path="url(#terminal-2651797304-line-4)">&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;BOOTSTRAP_COLD</text><text class="terminal-2651797304-r2" x="1098" y="117.6" textLength="12.2" clip-path="url(#terminal-2651797304-line-4)"> 86 + </text><text class="terminal-2651797304-r2" x="1098" y="142" textLength="12.2" clip-path="url(#terminal-2651797304-line-5)"> 87 + </text><text class="terminal-2651797304-r3" x="24.4" y="166.4" textLength="134.2" clip-path="url(#terminal-2651797304-line-6)">Next&#160;steps:</text><text class="terminal-2651797304-r2" x="1098" y="166.4" textLength="12.2" clip-path="url(#terminal-2651797304-line-6)"> 88 + </text><text class="terminal-2651797304-r2" x="0" y="190.8" textLength="353.8" clip-path="url(#terminal-2651797304-line-7)">&#160;&#160;&#160;&#160;1.&#160;Add&#160;spec&#160;documents&#160;to&#160;</text><text class="terminal-2651797304-r4" x="353.8" y="190.8" textLength="61" clip-path="url(#terminal-2651797304-line-7)">spec/</text><text class="terminal-2651797304-r2" x="1098" y="190.8" textLength="12.2" clip-path="url(#terminal-2651797304-line-7)"> 89 + </text><text class="terminal-2651797304-r2" x="0" y="215.2" textLength="134.2" clip-path="url(#terminal-2651797304-line-8)">&#160;&#160;&#160;&#160;2.&#160;Run&#160;</text><text class="terminal-2651797304-r4" x="134.2" y="215.2" textLength="207.4" clip-path="url(#terminal-2651797304-line-8)">phoenix&#160;bootstrap</text><text class="terminal-2651797304-r2" x="341.6" y="215.2" textLength="305" clip-path="url(#terminal-2651797304-line-8)">&#160;to&#160;ingest&#160;&amp;&#160;canonicalize</text><text class="terminal-2651797304-r2" x="1098" y="215.2" textLength="12.2" clip-path="url(#terminal-2651797304-line-8)"> 90 + </text> 91 + </g> 92 + </g> 93 + </svg>
examples/settle-up/screenshots/02-bootstrap.png

This is a binary file and will not be displayed.

+541
examples/settle-up/screenshots/02-bootstrap.svg
··· 1 + <svg class="rich-terminal" viewBox="0 0 1360 2978.0" xmlns="http://www.w3.org/2000/svg"> 2 + <!-- Generated with Rich https://www.textualize.io --> 3 + <style> 4 + 5 + @font-face { 6 + font-family: "Fira Code"; 7 + src: local("FiraCode-Regular"), 8 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), 9 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); 10 + font-style: normal; 11 + font-weight: 400; 12 + } 13 + @font-face { 14 + font-family: "Fira Code"; 15 + src: local("FiraCode-Bold"), 16 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), 17 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); 18 + font-style: bold; 19 + font-weight: 700; 20 + } 21 + 22 + .terminal-2545847897-matrix { 23 + font-family: Fira Code, monospace; 24 + font-size: 20px; 25 + line-height: 24.4px; 26 + font-variant-east-asian: full-width; 27 + } 28 + 29 + .terminal-2545847897-title { 30 + font-size: 18px; 31 + font-weight: bold; 32 + font-family: arial; 33 + } 34 + 35 + .terminal-2545847897-r1 { fill: #c5c8c6;font-weight: bold } 36 + .terminal-2545847897-r2 { fill: #c5c8c6 } 37 + .terminal-2545847897-r3 { fill: #868887 } 38 + .terminal-2545847897-r4 { fill: #98a84b } 39 + .terminal-2545847897-r5 { fill: #68a0b3 } 40 + .terminal-2545847897-r6 { fill: #d0b344 } 41 + .terminal-2545847897-r7 { fill: #cc555a } 42 + .terminal-2545847897-r8 { fill: #608ab1 } 43 + </style> 44 + 45 + <defs> 46 + <clipPath id="terminal-2545847897-clip-terminal"> 47 + <rect x="0" y="0" width="1341.0" height="2927.0" /> 48 + </clipPath> 49 + <clipPath id="terminal-2545847897-line-0"> 50 + <rect x="0" y="1.5" width="1342" height="24.65"/> 51 + </clipPath> 52 + <clipPath id="terminal-2545847897-line-1"> 53 + <rect x="0" y="25.9" width="1342" height="24.65"/> 54 + </clipPath> 55 + <clipPath id="terminal-2545847897-line-2"> 56 + <rect x="0" y="50.3" width="1342" height="24.65"/> 57 + </clipPath> 58 + <clipPath id="terminal-2545847897-line-3"> 59 + <rect x="0" y="74.7" width="1342" height="24.65"/> 60 + </clipPath> 61 + <clipPath id="terminal-2545847897-line-4"> 62 + <rect x="0" y="99.1" width="1342" height="24.65"/> 63 + </clipPath> 64 + <clipPath id="terminal-2545847897-line-5"> 65 + <rect x="0" y="123.5" width="1342" height="24.65"/> 66 + </clipPath> 67 + <clipPath id="terminal-2545847897-line-6"> 68 + <rect x="0" y="147.9" width="1342" height="24.65"/> 69 + </clipPath> 70 + <clipPath id="terminal-2545847897-line-7"> 71 + <rect x="0" y="172.3" width="1342" height="24.65"/> 72 + </clipPath> 73 + <clipPath id="terminal-2545847897-line-8"> 74 + <rect x="0" y="196.7" width="1342" height="24.65"/> 75 + </clipPath> 76 + <clipPath id="terminal-2545847897-line-9"> 77 + <rect x="0" y="221.1" width="1342" height="24.65"/> 78 + </clipPath> 79 + <clipPath id="terminal-2545847897-line-10"> 80 + <rect x="0" y="245.5" width="1342" height="24.65"/> 81 + </clipPath> 82 + <clipPath id="terminal-2545847897-line-11"> 83 + <rect x="0" y="269.9" width="1342" height="24.65"/> 84 + </clipPath> 85 + <clipPath id="terminal-2545847897-line-12"> 86 + <rect x="0" y="294.3" width="1342" height="24.65"/> 87 + </clipPath> 88 + <clipPath id="terminal-2545847897-line-13"> 89 + <rect x="0" y="318.7" width="1342" height="24.65"/> 90 + </clipPath> 91 + <clipPath id="terminal-2545847897-line-14"> 92 + <rect x="0" y="343.1" width="1342" height="24.65"/> 93 + </clipPath> 94 + <clipPath id="terminal-2545847897-line-15"> 95 + <rect x="0" y="367.5" width="1342" height="24.65"/> 96 + </clipPath> 97 + <clipPath id="terminal-2545847897-line-16"> 98 + <rect x="0" y="391.9" width="1342" height="24.65"/> 99 + </clipPath> 100 + <clipPath id="terminal-2545847897-line-17"> 101 + <rect x="0" y="416.3" width="1342" height="24.65"/> 102 + </clipPath> 103 + <clipPath id="terminal-2545847897-line-18"> 104 + <rect x="0" y="440.7" width="1342" height="24.65"/> 105 + </clipPath> 106 + <clipPath id="terminal-2545847897-line-19"> 107 + <rect x="0" y="465.1" width="1342" height="24.65"/> 108 + </clipPath> 109 + <clipPath id="terminal-2545847897-line-20"> 110 + <rect x="0" y="489.5" width="1342" height="24.65"/> 111 + </clipPath> 112 + <clipPath id="terminal-2545847897-line-21"> 113 + <rect x="0" y="513.9" width="1342" height="24.65"/> 114 + </clipPath> 115 + <clipPath id="terminal-2545847897-line-22"> 116 + <rect x="0" y="538.3" width="1342" height="24.65"/> 117 + </clipPath> 118 + <clipPath id="terminal-2545847897-line-23"> 119 + <rect x="0" y="562.7" width="1342" height="24.65"/> 120 + </clipPath> 121 + <clipPath id="terminal-2545847897-line-24"> 122 + <rect x="0" y="587.1" width="1342" height="24.65"/> 123 + </clipPath> 124 + <clipPath id="terminal-2545847897-line-25"> 125 + <rect x="0" y="611.5" width="1342" height="24.65"/> 126 + </clipPath> 127 + <clipPath id="terminal-2545847897-line-26"> 128 + <rect x="0" y="635.9" width="1342" height="24.65"/> 129 + </clipPath> 130 + <clipPath id="terminal-2545847897-line-27"> 131 + <rect x="0" y="660.3" width="1342" height="24.65"/> 132 + </clipPath> 133 + <clipPath id="terminal-2545847897-line-28"> 134 + <rect x="0" y="684.7" width="1342" height="24.65"/> 135 + </clipPath> 136 + <clipPath id="terminal-2545847897-line-29"> 137 + <rect x="0" y="709.1" width="1342" height="24.65"/> 138 + </clipPath> 139 + <clipPath id="terminal-2545847897-line-30"> 140 + <rect x="0" y="733.5" width="1342" height="24.65"/> 141 + </clipPath> 142 + <clipPath id="terminal-2545847897-line-31"> 143 + <rect x="0" y="757.9" width="1342" height="24.65"/> 144 + </clipPath> 145 + <clipPath id="terminal-2545847897-line-32"> 146 + <rect x="0" y="782.3" width="1342" height="24.65"/> 147 + </clipPath> 148 + <clipPath id="terminal-2545847897-line-33"> 149 + <rect x="0" y="806.7" width="1342" height="24.65"/> 150 + </clipPath> 151 + <clipPath id="terminal-2545847897-line-34"> 152 + <rect x="0" y="831.1" width="1342" height="24.65"/> 153 + </clipPath> 154 + <clipPath id="terminal-2545847897-line-35"> 155 + <rect x="0" y="855.5" width="1342" height="24.65"/> 156 + </clipPath> 157 + <clipPath id="terminal-2545847897-line-36"> 158 + <rect x="0" y="879.9" width="1342" height="24.65"/> 159 + </clipPath> 160 + <clipPath id="terminal-2545847897-line-37"> 161 + <rect x="0" y="904.3" width="1342" height="24.65"/> 162 + </clipPath> 163 + <clipPath id="terminal-2545847897-line-38"> 164 + <rect x="0" y="928.7" width="1342" height="24.65"/> 165 + </clipPath> 166 + <clipPath id="terminal-2545847897-line-39"> 167 + <rect x="0" y="953.1" width="1342" height="24.65"/> 168 + </clipPath> 169 + <clipPath id="terminal-2545847897-line-40"> 170 + <rect x="0" y="977.5" width="1342" height="24.65"/> 171 + </clipPath> 172 + <clipPath id="terminal-2545847897-line-41"> 173 + <rect x="0" y="1001.9" width="1342" height="24.65"/> 174 + </clipPath> 175 + <clipPath id="terminal-2545847897-line-42"> 176 + <rect x="0" y="1026.3" width="1342" height="24.65"/> 177 + </clipPath> 178 + <clipPath id="terminal-2545847897-line-43"> 179 + <rect x="0" y="1050.7" width="1342" height="24.65"/> 180 + </clipPath> 181 + <clipPath id="terminal-2545847897-line-44"> 182 + <rect x="0" y="1075.1" width="1342" height="24.65"/> 183 + </clipPath> 184 + <clipPath id="terminal-2545847897-line-45"> 185 + <rect x="0" y="1099.5" width="1342" height="24.65"/> 186 + </clipPath> 187 + <clipPath id="terminal-2545847897-line-46"> 188 + <rect x="0" y="1123.9" width="1342" height="24.65"/> 189 + </clipPath> 190 + <clipPath id="terminal-2545847897-line-47"> 191 + <rect x="0" y="1148.3" width="1342" height="24.65"/> 192 + </clipPath> 193 + <clipPath id="terminal-2545847897-line-48"> 194 + <rect x="0" y="1172.7" width="1342" height="24.65"/> 195 + </clipPath> 196 + <clipPath id="terminal-2545847897-line-49"> 197 + <rect x="0" y="1197.1" width="1342" height="24.65"/> 198 + </clipPath> 199 + <clipPath id="terminal-2545847897-line-50"> 200 + <rect x="0" y="1221.5" width="1342" height="24.65"/> 201 + </clipPath> 202 + <clipPath id="terminal-2545847897-line-51"> 203 + <rect x="0" y="1245.9" width="1342" height="24.65"/> 204 + </clipPath> 205 + <clipPath id="terminal-2545847897-line-52"> 206 + <rect x="0" y="1270.3" width="1342" height="24.65"/> 207 + </clipPath> 208 + <clipPath id="terminal-2545847897-line-53"> 209 + <rect x="0" y="1294.7" width="1342" height="24.65"/> 210 + </clipPath> 211 + <clipPath id="terminal-2545847897-line-54"> 212 + <rect x="0" y="1319.1" width="1342" height="24.65"/> 213 + </clipPath> 214 + <clipPath id="terminal-2545847897-line-55"> 215 + <rect x="0" y="1343.5" width="1342" height="24.65"/> 216 + </clipPath> 217 + <clipPath id="terminal-2545847897-line-56"> 218 + <rect x="0" y="1367.9" width="1342" height="24.65"/> 219 + </clipPath> 220 + <clipPath id="terminal-2545847897-line-57"> 221 + <rect x="0" y="1392.3" width="1342" height="24.65"/> 222 + </clipPath> 223 + <clipPath id="terminal-2545847897-line-58"> 224 + <rect x="0" y="1416.7" width="1342" height="24.65"/> 225 + </clipPath> 226 + <clipPath id="terminal-2545847897-line-59"> 227 + <rect x="0" y="1441.1" width="1342" height="24.65"/> 228 + </clipPath> 229 + <clipPath id="terminal-2545847897-line-60"> 230 + <rect x="0" y="1465.5" width="1342" height="24.65"/> 231 + </clipPath> 232 + <clipPath id="terminal-2545847897-line-61"> 233 + <rect x="0" y="1489.9" width="1342" height="24.65"/> 234 + </clipPath> 235 + <clipPath id="terminal-2545847897-line-62"> 236 + <rect x="0" y="1514.3" width="1342" height="24.65"/> 237 + </clipPath> 238 + <clipPath id="terminal-2545847897-line-63"> 239 + <rect x="0" y="1538.7" width="1342" height="24.65"/> 240 + </clipPath> 241 + <clipPath id="terminal-2545847897-line-64"> 242 + <rect x="0" y="1563.1" width="1342" height="24.65"/> 243 + </clipPath> 244 + <clipPath id="terminal-2545847897-line-65"> 245 + <rect x="0" y="1587.5" width="1342" height="24.65"/> 246 + </clipPath> 247 + <clipPath id="terminal-2545847897-line-66"> 248 + <rect x="0" y="1611.9" width="1342" height="24.65"/> 249 + </clipPath> 250 + <clipPath id="terminal-2545847897-line-67"> 251 + <rect x="0" y="1636.3" width="1342" height="24.65"/> 252 + </clipPath> 253 + <clipPath id="terminal-2545847897-line-68"> 254 + <rect x="0" y="1660.7" width="1342" height="24.65"/> 255 + </clipPath> 256 + <clipPath id="terminal-2545847897-line-69"> 257 + <rect x="0" y="1685.1" width="1342" height="24.65"/> 258 + </clipPath> 259 + <clipPath id="terminal-2545847897-line-70"> 260 + <rect x="0" y="1709.5" width="1342" height="24.65"/> 261 + </clipPath> 262 + <clipPath id="terminal-2545847897-line-71"> 263 + <rect x="0" y="1733.9" width="1342" height="24.65"/> 264 + </clipPath> 265 + <clipPath id="terminal-2545847897-line-72"> 266 + <rect x="0" y="1758.3" width="1342" height="24.65"/> 267 + </clipPath> 268 + <clipPath id="terminal-2545847897-line-73"> 269 + <rect x="0" y="1782.7" width="1342" height="24.65"/> 270 + </clipPath> 271 + <clipPath id="terminal-2545847897-line-74"> 272 + <rect x="0" y="1807.1" width="1342" height="24.65"/> 273 + </clipPath> 274 + <clipPath id="terminal-2545847897-line-75"> 275 + <rect x="0" y="1831.5" width="1342" height="24.65"/> 276 + </clipPath> 277 + <clipPath id="terminal-2545847897-line-76"> 278 + <rect x="0" y="1855.9" width="1342" height="24.65"/> 279 + </clipPath> 280 + <clipPath id="terminal-2545847897-line-77"> 281 + <rect x="0" y="1880.3" width="1342" height="24.65"/> 282 + </clipPath> 283 + <clipPath id="terminal-2545847897-line-78"> 284 + <rect x="0" y="1904.7" width="1342" height="24.65"/> 285 + </clipPath> 286 + <clipPath id="terminal-2545847897-line-79"> 287 + <rect x="0" y="1929.1" width="1342" height="24.65"/> 288 + </clipPath> 289 + <clipPath id="terminal-2545847897-line-80"> 290 + <rect x="0" y="1953.5" width="1342" height="24.65"/> 291 + </clipPath> 292 + <clipPath id="terminal-2545847897-line-81"> 293 + <rect x="0" y="1977.9" width="1342" height="24.65"/> 294 + </clipPath> 295 + <clipPath id="terminal-2545847897-line-82"> 296 + <rect x="0" y="2002.3" width="1342" height="24.65"/> 297 + </clipPath> 298 + <clipPath id="terminal-2545847897-line-83"> 299 + <rect x="0" y="2026.7" width="1342" height="24.65"/> 300 + </clipPath> 301 + <clipPath id="terminal-2545847897-line-84"> 302 + <rect x="0" y="2051.1" width="1342" height="24.65"/> 303 + </clipPath> 304 + <clipPath id="terminal-2545847897-line-85"> 305 + <rect x="0" y="2075.5" width="1342" height="24.65"/> 306 + </clipPath> 307 + <clipPath id="terminal-2545847897-line-86"> 308 + <rect x="0" y="2099.9" width="1342" height="24.65"/> 309 + </clipPath> 310 + <clipPath id="terminal-2545847897-line-87"> 311 + <rect x="0" y="2124.3" width="1342" height="24.65"/> 312 + </clipPath> 313 + <clipPath id="terminal-2545847897-line-88"> 314 + <rect x="0" y="2148.7" width="1342" height="24.65"/> 315 + </clipPath> 316 + <clipPath id="terminal-2545847897-line-89"> 317 + <rect x="0" y="2173.1" width="1342" height="24.65"/> 318 + </clipPath> 319 + <clipPath id="terminal-2545847897-line-90"> 320 + <rect x="0" y="2197.5" width="1342" height="24.65"/> 321 + </clipPath> 322 + <clipPath id="terminal-2545847897-line-91"> 323 + <rect x="0" y="2221.9" width="1342" height="24.65"/> 324 + </clipPath> 325 + <clipPath id="terminal-2545847897-line-92"> 326 + <rect x="0" y="2246.3" width="1342" height="24.65"/> 327 + </clipPath> 328 + <clipPath id="terminal-2545847897-line-93"> 329 + <rect x="0" y="2270.7" width="1342" height="24.65"/> 330 + </clipPath> 331 + <clipPath id="terminal-2545847897-line-94"> 332 + <rect x="0" y="2295.1" width="1342" height="24.65"/> 333 + </clipPath> 334 + <clipPath id="terminal-2545847897-line-95"> 335 + <rect x="0" y="2319.5" width="1342" height="24.65"/> 336 + </clipPath> 337 + <clipPath id="terminal-2545847897-line-96"> 338 + <rect x="0" y="2343.9" width="1342" height="24.65"/> 339 + </clipPath> 340 + <clipPath id="terminal-2545847897-line-97"> 341 + <rect x="0" y="2368.3" width="1342" height="24.65"/> 342 + </clipPath> 343 + <clipPath id="terminal-2545847897-line-98"> 344 + <rect x="0" y="2392.7" width="1342" height="24.65"/> 345 + </clipPath> 346 + <clipPath id="terminal-2545847897-line-99"> 347 + <rect x="0" y="2417.1" width="1342" height="24.65"/> 348 + </clipPath> 349 + <clipPath id="terminal-2545847897-line-100"> 350 + <rect x="0" y="2441.5" width="1342" height="24.65"/> 351 + </clipPath> 352 + <clipPath id="terminal-2545847897-line-101"> 353 + <rect x="0" y="2465.9" width="1342" height="24.65"/> 354 + </clipPath> 355 + <clipPath id="terminal-2545847897-line-102"> 356 + <rect x="0" y="2490.3" width="1342" height="24.65"/> 357 + </clipPath> 358 + <clipPath id="terminal-2545847897-line-103"> 359 + <rect x="0" y="2514.7" width="1342" height="24.65"/> 360 + </clipPath> 361 + <clipPath id="terminal-2545847897-line-104"> 362 + <rect x="0" y="2539.1" width="1342" height="24.65"/> 363 + </clipPath> 364 + <clipPath id="terminal-2545847897-line-105"> 365 + <rect x="0" y="2563.5" width="1342" height="24.65"/> 366 + </clipPath> 367 + <clipPath id="terminal-2545847897-line-106"> 368 + <rect x="0" y="2587.9" width="1342" height="24.65"/> 369 + </clipPath> 370 + <clipPath id="terminal-2545847897-line-107"> 371 + <rect x="0" y="2612.3" width="1342" height="24.65"/> 372 + </clipPath> 373 + <clipPath id="terminal-2545847897-line-108"> 374 + <rect x="0" y="2636.7" width="1342" height="24.65"/> 375 + </clipPath> 376 + <clipPath id="terminal-2545847897-line-109"> 377 + <rect x="0" y="2661.1" width="1342" height="24.65"/> 378 + </clipPath> 379 + <clipPath id="terminal-2545847897-line-110"> 380 + <rect x="0" y="2685.5" width="1342" height="24.65"/> 381 + </clipPath> 382 + <clipPath id="terminal-2545847897-line-111"> 383 + <rect x="0" y="2709.9" width="1342" height="24.65"/> 384 + </clipPath> 385 + <clipPath id="terminal-2545847897-line-112"> 386 + <rect x="0" y="2734.3" width="1342" height="24.65"/> 387 + </clipPath> 388 + <clipPath id="terminal-2545847897-line-113"> 389 + <rect x="0" y="2758.7" width="1342" height="24.65"/> 390 + </clipPath> 391 + <clipPath id="terminal-2545847897-line-114"> 392 + <rect x="0" y="2783.1" width="1342" height="24.65"/> 393 + </clipPath> 394 + <clipPath id="terminal-2545847897-line-115"> 395 + <rect x="0" y="2807.5" width="1342" height="24.65"/> 396 + </clipPath> 397 + <clipPath id="terminal-2545847897-line-116"> 398 + <rect x="0" y="2831.9" width="1342" height="24.65"/> 399 + </clipPath> 400 + <clipPath id="terminal-2545847897-line-117"> 401 + <rect x="0" y="2856.3" width="1342" height="24.65"/> 402 + </clipPath> 403 + <clipPath id="terminal-2545847897-line-118"> 404 + <rect x="0" y="2880.7" width="1342" height="24.65"/> 405 + </clipPath> 406 + </defs> 407 + 408 + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1358" height="2976" rx="8"/><text class="terminal-2545847897-title" fill="#c5c8c6" text-anchor="middle" x="679" y="27">phoenix&#160;—&#160;02-bootstrap</text> 409 + <g transform="translate(26,22)"> 410 + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> 411 + <circle cx="22" cy="0" r="7" fill="#febc2e"/> 412 + <circle cx="44" cy="0" r="7" fill="#28c840"/> 413 + </g> 414 + 415 + <g transform="translate(9, 41)" clip-path="url(#terminal-2545847897-clip-terminal)"> 416 + 417 + <g class="terminal-2545847897-matrix"> 418 + <text class="terminal-2545847897-r1" x="0" y="20" textLength="231.8" clip-path="url(#terminal-2545847897-line-0)">🔥&#160;Phoenix&#160;Bootstrap</text><text class="terminal-2545847897-r2" x="1342" y="20" textLength="12.2" clip-path="url(#terminal-2545847897-line-0)"> 419 + </text><text class="terminal-2545847897-r2" x="1342" y="44.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-1)"> 420 + </text><text class="terminal-2545847897-r3" x="24.4" y="68.8" textLength="97.6" clip-path="url(#terminal-2545847897-line-2)">Phase&#160;A:</text><text class="terminal-2545847897-r2" x="122" y="68.8" textLength="402.6" clip-path="url(#terminal-2545847897-line-2)">&#160;Clause&#160;extraction&#160;+&#160;cold&#160;hashing</text><text class="terminal-2545847897-r2" x="1342" y="68.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-2)"> 421 + </text><text class="terminal-2545847897-r4" x="48.8" y="93.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-3)">✔</text><text class="terminal-2545847897-r2" x="61" y="93.2" textLength="292.8" clip-path="url(#terminal-2545847897-line-3)">&#160;spec/api.md&#160;→&#160;4&#160;clauses</text><text class="terminal-2545847897-r2" x="1342" y="93.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-3)"> 422 + </text><text class="terminal-2545847897-r4" x="48.8" y="117.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-4)">✔</text><text class="terminal-2545847897-r2" x="61" y="117.6" textLength="353.8" clip-path="url(#terminal-2545847897-line-4)">&#160;spec/expenses.md&#160;→&#160;5&#160;clauses</text><text class="terminal-2545847897-r2" x="1342" y="117.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-4)"> 423 + </text><text class="terminal-2545847897-r4" x="48.8" y="142" textLength="12.2" clip-path="url(#terminal-2545847897-line-5)">✔</text><text class="terminal-2545847897-r2" x="61" y="142" textLength="329.4" clip-path="url(#terminal-2545847897-line-5)">&#160;spec/groups.md&#160;→&#160;4&#160;clauses</text><text class="terminal-2545847897-r2" x="1342" y="142" textLength="12.2" clip-path="url(#terminal-2545847897-line-5)"> 424 + </text><text class="terminal-2545847897-r4" x="48.8" y="166.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-6)">✔</text><text class="terminal-2545847897-r2" x="61" y="166.4" textLength="390.4" clip-path="url(#terminal-2545847897-line-6)">&#160;spec/settlements.md&#160;→&#160;4&#160;clauses</text><text class="terminal-2545847897-r2" x="1342" y="166.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-6)"> 425 + </text><text class="terminal-2545847897-r3" x="48.8" y="190.8" textLength="329.4" clip-path="url(#terminal-2545847897-line-7)">Total:&#160;17&#160;clauses&#160;extracted</text><text class="terminal-2545847897-r2" x="1342" y="190.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-7)"> 426 + </text><text class="terminal-2545847897-r2" x="1342" y="215.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-8)"> 427 + </text><text class="terminal-2545847897-r3" x="24.4" y="239.6" textLength="97.6" clip-path="url(#terminal-2545847897-line-9)">Phase&#160;B:</text><text class="terminal-2545847897-r2" x="122" y="239.6" textLength="500.2" clip-path="url(#terminal-2545847897-line-9)">&#160;Canonicalization&#160;+&#160;warm&#160;context&#160;hashing&#160;</text><text class="terminal-2545847897-r3" x="622.2" y="239.6" textLength="500.2" clip-path="url(#terminal-2545847897-line-9)">(LLM:&#160;anthropic/claude-sonnet-4-20250514)</text><text class="terminal-2545847897-r2" x="1342" y="239.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-9)"> 428 + </text><text class="terminal-2545847897-r4" x="48.8" y="264" textLength="12.2" clip-path="url(#terminal-2545847897-line-10)">✔</text><text class="terminal-2545847897-r2" x="61" y="264" textLength="353.8" clip-path="url(#terminal-2545847897-line-10)">&#160;66&#160;canonical&#160;nodes&#160;extracted</text><text class="terminal-2545847897-r2" x="1342" y="264" textLength="12.2" clip-path="url(#terminal-2545847897-line-10)"> 429 + </text><text class="terminal-2545847897-r4" x="48.8" y="288.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-11)">✔</text><text class="terminal-2545847897-r2" x="61" y="288.4" textLength="390.4" clip-path="url(#terminal-2545847897-line-11)">&#160;17&#160;warm&#160;context&#160;hashes&#160;computed</text><text class="terminal-2545847897-r2" x="1342" y="288.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-11)"> 430 + </text><text class="terminal-2545847897-r4" x="48.8" y="312.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-12)">✔</text><text class="terminal-2545847897-r2" x="61" y="312.8" textLength="183" clip-path="url(#terminal-2545847897-line-12)">&#160;System&#160;state:&#160;</text><text class="terminal-2545847897-r5" x="244" y="312.8" textLength="207.4" clip-path="url(#terminal-2545847897-line-12)">BOOTSTRAP_WARMING</text><text class="terminal-2545847897-r2" x="1342" y="312.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-12)"> 431 + </text><text class="terminal-2545847897-r2" x="1342" y="337.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-13)"> 432 + </text><text class="terminal-2545847897-r3" x="24.4" y="361.6" textLength="97.6" clip-path="url(#terminal-2545847897-line-14)">Phase&#160;C:</text><text class="terminal-2545847897-r2" x="122" y="361.6" textLength="146.4" clip-path="url(#terminal-2545847897-line-14)">&#160;IU&#160;planning</text><text class="terminal-2545847897-r2" x="1342" y="361.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-14)"> 433 + </text><text class="terminal-2545847897-r4" x="48.8" y="386" textLength="12.2" clip-path="url(#terminal-2545847897-line-15)">✔</text><text class="terminal-2545847897-r2" x="61" y="386" textLength="390.4" clip-path="url(#terminal-2545847897-line-15)">&#160;12&#160;Implementation&#160;Units&#160;planned</text><text class="terminal-2545847897-r2" x="1342" y="386" textLength="12.2" clip-path="url(#terminal-2545847897-line-15)"> 434 + </text><text class="terminal-2545847897-r3" x="73.2" y="410.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-16)">·</text><text class="terminal-2545847897-r2" x="85.4" y="410.4" textLength="134.2" clip-path="url(#terminal-2545847897-line-16)">&#160;Endpoints&#160;</text><text class="terminal-2545847897-r3" x="219.6" y="410.4" textLength="73.2" clip-path="url(#terminal-2545847897-line-16)">(high)</text><text class="terminal-2545847897-r2" x="292.8" y="410.4" textLength="402.6" clip-path="url(#terminal-2545847897-line-16)">&#160;→&#160;src/generated/api/endpoints.ts</text><text class="terminal-2545847897-r2" x="1342" y="410.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-16)"> 435 + </text><text class="terminal-2545847897-r3" x="73.2" y="434.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-17)">·</text><text class="terminal-2545847897-r2" x="85.4" y="434.8" textLength="195.2" clip-path="url(#terminal-2545847897-line-17)">&#160;Error&#160;Handling&#160;</text><text class="terminal-2545847897-r3" x="280.6" y="434.8" textLength="97.6" clip-path="url(#terminal-2545847897-line-17)">(medium)</text><text class="terminal-2545847897-r2" x="378.2" y="434.8" textLength="463.6" clip-path="url(#terminal-2545847897-line-17)">&#160;→&#160;src/generated/api/error-handling.ts</text><text class="terminal-2545847897-r2" x="1342" y="434.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-17)"> 436 + </text><text class="terminal-2545847897-r3" x="73.2" y="459.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-18)">·</text><text class="terminal-2545847897-r2" x="85.4" y="459.2" textLength="207.4" clip-path="url(#terminal-2545847897-line-18)">&#160;Response&#160;Format&#160;</text><text class="terminal-2545847897-r3" x="292.8" y="459.2" textLength="61" clip-path="url(#terminal-2545847897-line-18)">(low)</text><text class="terminal-2545847897-r2" x="353.8" y="459.2" textLength="475.8" clip-path="url(#terminal-2545847897-line-18)">&#160;→&#160;src/generated/api/response-format.ts</text><text class="terminal-2545847897-r2" x="1342" y="459.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-18)"> 437 + </text><text class="terminal-2545847897-r3" x="73.2" y="483.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-19)">·</text><text class="terminal-2545847897-r2" x="85.4" y="483.6" textLength="256.2" clip-path="url(#terminal-2545847897-line-19)">&#160;Balance&#160;Calculation&#160;</text><text class="terminal-2545847897-r3" x="341.6" y="483.6" textLength="73.2" clip-path="url(#terminal-2545847897-line-19)">(high)</text><text class="terminal-2545847897-r2" x="414.8" y="483.6" textLength="585.6" clip-path="url(#terminal-2545847897-line-19)">&#160;→&#160;src/generated/expenses/balance-calculation.ts</text><text class="terminal-2545847897-r2" x="1342" y="483.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-19)"> 438 + </text><text class="terminal-2545847897-r3" x="73.2" y="508" textLength="12.2" clip-path="url(#terminal-2545847897-line-20)">·</text><text class="terminal-2545847897-r2" x="85.4" y="508" textLength="256.2" clip-path="url(#terminal-2545847897-line-20)">&#160;Creating&#160;an&#160;Expense&#160;</text><text class="terminal-2545847897-r3" x="341.6" y="508" textLength="73.2" clip-path="url(#terminal-2545847897-line-20)">(high)</text><text class="terminal-2545847897-r2" x="414.8" y="508" textLength="585.6" clip-path="url(#terminal-2545847897-line-20)">&#160;→&#160;src/generated/expenses/creating-an-expense.ts</text><text class="terminal-2545847897-r2" x="1342" y="508" textLength="12.2" clip-path="url(#terminal-2545847897-line-20)"> 439 + </text><text class="terminal-2545847897-r3" x="73.2" y="532.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-21)">·</text><text class="terminal-2545847897-r2" x="85.4" y="532.4" textLength="207.4" clip-path="url(#terminal-2545847897-line-21)">&#160;Expense&#160;History&#160;</text><text class="terminal-2545847897-r3" x="292.8" y="532.4" textLength="97.6" clip-path="url(#terminal-2545847897-line-21)">(medium)</text><text class="terminal-2545847897-r2" x="390.4" y="532.4" textLength="536.8" clip-path="url(#terminal-2545847897-line-21)">&#160;→&#160;src/generated/expenses/expense-history.ts</text><text class="terminal-2545847897-r2" x="1342" y="532.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-21)"> 440 + </text><text class="terminal-2545847897-r3" x="73.2" y="556.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-22)">·</text><text class="terminal-2545847897-r2" x="85.4" y="556.8" textLength="219.6" clip-path="url(#terminal-2545847897-line-22)">&#160;Split&#160;Strategies&#160;</text><text class="terminal-2545847897-r3" x="305" y="556.8" textLength="73.2" clip-path="url(#terminal-2545847897-line-22)">(high)</text><text class="terminal-2545847897-r2" x="378.2" y="556.8" textLength="549" clip-path="url(#terminal-2545847897-line-22)">&#160;→&#160;src/generated/expenses/split-strategies.ts</text><text class="terminal-2545847897-r2" x="1342" y="556.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-22)"> 441 + </text><text class="terminal-2545847897-r3" x="73.2" y="581.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-23)">·</text><text class="terminal-2545847897-r2" x="85.4" y="581.2" textLength="219.6" clip-path="url(#terminal-2545847897-line-23)">&#160;Group&#160;Management&#160;</text><text class="terminal-2545847897-r3" x="305" y="581.2" textLength="73.2" clip-path="url(#terminal-2545847897-line-23)">(high)</text><text class="terminal-2545847897-r2" x="378.2" y="581.2" textLength="524.6" clip-path="url(#terminal-2545847897-line-23)">&#160;→&#160;src/generated/groups/group-management.ts</text><text class="terminal-2545847897-r2" x="1342" y="581.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-23)"> 442 + </text><text class="terminal-2545847897-r3" x="73.2" y="605.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-24)">·</text><text class="terminal-2545847897-r2" x="85.4" y="605.6" textLength="183" clip-path="url(#terminal-2545847897-line-24)">&#160;Group&#160;Summary&#160;</text><text class="terminal-2545847897-r3" x="268.4" y="605.6" textLength="73.2" clip-path="url(#terminal-2545847897-line-24)">(high)</text><text class="terminal-2545847897-r2" x="341.6" y="605.6" textLength="488" clip-path="url(#terminal-2545847897-line-24)">&#160;→&#160;src/generated/groups/group-summary.ts</text><text class="terminal-2545847897-r2" x="1342" y="605.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-24)"> 443 + </text><text class="terminal-2545847897-r3" x="73.2" y="630" textLength="12.2" clip-path="url(#terminal-2545847897-line-25)">·</text><text class="terminal-2545847897-r2" x="85.4" y="630" textLength="256.2" clip-path="url(#terminal-2545847897-line-25)">&#160;Debt&#160;Simplification&#160;</text><text class="terminal-2545847897-r3" x="341.6" y="630" textLength="73.2" clip-path="url(#terminal-2545847897-line-25)">(high)</text><text class="terminal-2545847897-r2" x="414.8" y="630" textLength="622.2" clip-path="url(#terminal-2545847897-line-25)">&#160;→&#160;src/generated/settlements/debt-simplification.ts</text><text class="terminal-2545847897-r2" x="1342" y="630" textLength="12.2" clip-path="url(#terminal-2545847897-line-25)"> 444 + </text><text class="terminal-2545847897-r3" x="73.2" y="654.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-26)">·</text><text class="terminal-2545847897-r2" x="85.4" y="654.4" textLength="280.6" clip-path="url(#terminal-2545847897-line-26)">&#160;Recording&#160;Settlements&#160;</text><text class="terminal-2545847897-r3" x="366" y="654.4" textLength="61" clip-path="url(#terminal-2545847897-line-26)">(low)</text><text class="terminal-2545847897-r2" x="427" y="654.4" textLength="646.6" clip-path="url(#terminal-2545847897-line-26)">&#160;→&#160;src/generated/settlements/recording-settlements.ts</text><text class="terminal-2545847897-r2" x="1342" y="654.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-26)"> 445 + </text><text class="terminal-2545847897-r3" x="73.2" y="678.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-27)">·</text><text class="terminal-2545847897-r2" x="85.4" y="678.8" textLength="231.8" clip-path="url(#terminal-2545847897-line-27)">&#160;Settlement&#160;Status&#160;</text><text class="terminal-2545847897-r3" x="317.2" y="678.8" textLength="61" clip-path="url(#terminal-2545847897-line-27)">(low)</text><text class="terminal-2545847897-r2" x="378.2" y="678.8" textLength="597.8" clip-path="url(#terminal-2545847897-line-27)">&#160;→&#160;src/generated/settlements/settlement-status.ts</text><text class="terminal-2545847897-r2" x="1342" y="678.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-27)"> 446 + </text><text class="terminal-2545847897-r2" x="1342" y="703.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-28)"> 447 + </text><text class="terminal-2545847897-r3" x="24.4" y="727.6" textLength="97.6" clip-path="url(#terminal-2545847897-line-29)">Phase&#160;C:</text><text class="terminal-2545847897-r2" x="122" y="727.6" textLength="207.4" clip-path="url(#terminal-2545847897-line-29)">&#160;Code&#160;generation&#160;</text><text class="terminal-2545847897-r3" x="329.4" y="727.6" textLength="439.2" clip-path="url(#terminal-2545847897-line-29)">(anthropic/claude-sonnet-4-20250514)</text><text class="terminal-2545847897-r2" x="1342" y="727.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-29)"> 448 + </text><text class="terminal-2545847897-r2" x="0" y="752" textLength="207.4" clip-path="url(#terminal-2545847897-line-30)">&#160;&#160;&#160;&#160;⏳&#160;Endpoints…&#160;</text><text class="terminal-2545847897-r4" x="219.6" y="752" textLength="12.2" clip-path="url(#terminal-2545847897-line-30)">✔</text><text class="terminal-2545847897-r2" x="1342" y="752" textLength="12.2" clip-path="url(#terminal-2545847897-line-30)"> 449 + </text><text class="terminal-2545847897-r2" x="0" y="776.4" textLength="268.4" clip-path="url(#terminal-2545847897-line-31)">&#160;&#160;&#160;&#160;⏳&#160;Error&#160;Handling…&#160;</text><text class="terminal-2545847897-r4" x="280.6" y="776.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-31)">✔</text><text class="terminal-2545847897-r2" x="1342" y="776.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-31)"> 450 + </text><text class="terminal-2545847897-r2" x="0" y="800.8" textLength="280.6" clip-path="url(#terminal-2545847897-line-32)">&#160;&#160;&#160;&#160;⏳&#160;Response&#160;Format…&#160;</text><text class="terminal-2545847897-r4" x="292.8" y="800.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-32)">✔</text><text class="terminal-2545847897-r2" x="1342" y="800.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-32)"> 451 + </text><text class="terminal-2545847897-r2" x="0" y="825.2" textLength="329.4" clip-path="url(#terminal-2545847897-line-33)">&#160;&#160;&#160;&#160;⏳&#160;Balance&#160;Calculation…&#160;</text><text class="terminal-2545847897-r4" x="341.6" y="825.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-33)">✔</text><text class="terminal-2545847897-r2" x="1342" y="825.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-33)"> 452 + </text><text class="terminal-2545847897-r2" x="0" y="849.6" textLength="329.4" clip-path="url(#terminal-2545847897-line-34)">&#160;&#160;&#160;&#160;⏳&#160;Creating&#160;an&#160;Expense…&#160;</text><text class="terminal-2545847897-r4" x="341.6" y="849.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-34)">✔</text><text class="terminal-2545847897-r2" x="1342" y="849.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-34)"> 453 + </text><text class="terminal-2545847897-r2" x="0" y="874" textLength="280.6" clip-path="url(#terminal-2545847897-line-35)">&#160;&#160;&#160;&#160;⏳&#160;Expense&#160;History…&#160;</text><text class="terminal-2545847897-r4" x="292.8" y="874" textLength="12.2" clip-path="url(#terminal-2545847897-line-35)">✔</text><text class="terminal-2545847897-r2" x="1342" y="874" textLength="12.2" clip-path="url(#terminal-2545847897-line-35)"> 454 + </text><text class="terminal-2545847897-r2" x="0" y="898.4" textLength="292.8" clip-path="url(#terminal-2545847897-line-36)">&#160;&#160;&#160;&#160;⏳&#160;Split&#160;Strategies…&#160;</text><text class="terminal-2545847897-r4" x="305" y="898.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-36)">✔</text><text class="terminal-2545847897-r2" x="1342" y="898.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-36)"> 455 + </text><text class="terminal-2545847897-r2" x="0" y="922.8" textLength="292.8" clip-path="url(#terminal-2545847897-line-37)">&#160;&#160;&#160;&#160;⏳&#160;Group&#160;Management…&#160;</text><text class="terminal-2545847897-r4" x="305" y="922.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-37)">✔</text><text class="terminal-2545847897-r2" x="1342" y="922.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-37)"> 456 + </text><text class="terminal-2545847897-r2" x="0" y="947.2" textLength="256.2" clip-path="url(#terminal-2545847897-line-38)">&#160;&#160;&#160;&#160;⏳&#160;Group&#160;Summary…&#160;</text><text class="terminal-2545847897-r4" x="268.4" y="947.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-38)">✔</text><text class="terminal-2545847897-r2" x="1342" y="947.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-38)"> 457 + </text><text class="terminal-2545847897-r2" x="0" y="971.6" textLength="329.4" clip-path="url(#terminal-2545847897-line-39)">&#160;&#160;&#160;&#160;⏳&#160;Debt&#160;Simplification…&#160;</text><text class="terminal-2545847897-r4" x="341.6" y="971.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-39)">✔</text><text class="terminal-2545847897-r2" x="1342" y="971.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-39)"> 458 + </text><text class="terminal-2545847897-r2" x="0" y="996" textLength="353.8" clip-path="url(#terminal-2545847897-line-40)">&#160;&#160;&#160;&#160;⏳&#160;Recording&#160;Settlements…&#160;</text><text class="terminal-2545847897-r4" x="366" y="996" textLength="12.2" clip-path="url(#terminal-2545847897-line-40)">✔</text><text class="terminal-2545847897-r2" x="1342" y="996" textLength="12.2" clip-path="url(#terminal-2545847897-line-40)"> 459 + </text><text class="terminal-2545847897-r2" x="0" y="1020.4" textLength="305" clip-path="url(#terminal-2545847897-line-41)">&#160;&#160;&#160;&#160;⏳&#160;Settlement&#160;Status…&#160;</text><text class="terminal-2545847897-r4" x="317.2" y="1020.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-41)">✔</text><text class="terminal-2545847897-r2" x="1342" y="1020.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-41)"> 460 + </text><text class="terminal-2545847897-r2" x="1342" y="1044.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-42)"> 461 + </text><text class="terminal-2545847897-r3" x="24.4" y="1069.2" textLength="109.8" clip-path="url(#terminal-2545847897-line-43)">Scaffold:</text><text class="terminal-2545847897-r2" x="134.2" y="1069.2" textLength="390.4" clip-path="url(#terminal-2545847897-line-43)">&#160;Service&#160;wiring&#160;+&#160;project&#160;config</text><text class="terminal-2545847897-r2" x="1342" y="1069.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-43)"> 462 + </text><text class="terminal-2545847897-r4" x="48.8" y="1093.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-44)">✔</text><text class="terminal-2545847897-r2" x="61" y="1093.6" textLength="292.8" clip-path="url(#terminal-2545847897-line-44)">&#160;Api&#160;→&#160;:3000&#160;(3&#160;modules)</text><text class="terminal-2545847897-r2" x="1342" y="1093.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-44)"> 463 + </text><text class="terminal-2545847897-r4" x="48.8" y="1118" textLength="12.2" clip-path="url(#terminal-2545847897-line-45)">✔</text><text class="terminal-2545847897-r2" x="61" y="1118" textLength="353.8" clip-path="url(#terminal-2545847897-line-45)">&#160;Expenses&#160;→&#160;:3001&#160;(4&#160;modules)</text><text class="terminal-2545847897-r2" x="1342" y="1118" textLength="12.2" clip-path="url(#terminal-2545847897-line-45)"> 464 + </text><text class="terminal-2545847897-r4" x="48.8" y="1142.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-46)">✔</text><text class="terminal-2545847897-r2" x="61" y="1142.4" textLength="329.4" clip-path="url(#terminal-2545847897-line-46)">&#160;Groups&#160;→&#160;:3002&#160;(2&#160;modules)</text><text class="terminal-2545847897-r2" x="1342" y="1142.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-46)"> 465 + </text><text class="terminal-2545847897-r4" x="48.8" y="1166.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-47)">✔</text><text class="terminal-2545847897-r2" x="61" y="1166.8" textLength="390.4" clip-path="url(#terminal-2545847897-line-47)">&#160;Settlements&#160;→&#160;:3003&#160;(3&#160;modules)</text><text class="terminal-2545847897-r2" x="1342" y="1166.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-47)"> 466 + </text><text class="terminal-2545847897-r4" x="48.8" y="1191.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-48)">✔</text><text class="terminal-2545847897-r2" x="61" y="1191.2" textLength="341.6" clip-path="url(#terminal-2545847897-line-48)">&#160;package.json,&#160;tsconfig.json</text><text class="terminal-2545847897-r2" x="1342" y="1191.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-48)"> 467 + </text><text class="terminal-2545847897-r2" x="1342" y="1215.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-49)"> 468 + </text><text class="terminal-2545847897-r3" x="24.4" y="1240" textLength="97.6" clip-path="url(#terminal-2545847897-line-50)">Phase&#160;D:</text><text class="terminal-2545847897-r2" x="122" y="1240" textLength="195.2" clip-path="url(#terminal-2545847897-line-50)">&#160;Trust&#160;Dashboard</text><text class="terminal-2545847897-r2" x="1342" y="1240" textLength="12.2" clip-path="url(#terminal-2545847897-line-50)"> 469 + </text><text class="terminal-2545847897-r2" x="1342" y="1264.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-51)"> 470 + </text><text class="terminal-2545847897-r3" x="24.4" y="1288.8" textLength="158.6" clip-path="url(#terminal-2545847897-line-52)">System&#160;State:</text><text class="terminal-2545847897-r6" x="195.2" y="1288.8" textLength="207.4" clip-path="url(#terminal-2545847897-line-52)">BOOTSTRAP_WARMING</text><text class="terminal-2545847897-r2" x="1342" y="1288.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-52)"> 471 + </text><text class="terminal-2545847897-r3" x="24.4" y="1313.2" textLength="195.2" clip-path="url(#terminal-2545847897-line-53)">Canonical&#160;Nodes:</text><text class="terminal-2545847897-r2" x="219.6" y="1313.2" textLength="36.6" clip-path="url(#terminal-2545847897-line-53)">&#160;66</text><text class="terminal-2545847897-r2" x="1342" y="1313.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-53)"> 472 + </text><text class="terminal-2545847897-r3" x="24.4" y="1337.6" textLength="256.2" clip-path="url(#terminal-2545847897-line-54)">Implementation&#160;Units:</text><text class="terminal-2545847897-r2" x="280.6" y="1337.6" textLength="36.6" clip-path="url(#terminal-2545847897-line-54)">&#160;12</text><text class="terminal-2545847897-r2" x="1342" y="1337.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-54)"> 473 + </text><text class="terminal-2545847897-r3" x="24.4" y="1362" textLength="158.6" clip-path="url(#terminal-2545847897-line-55)">Spec&#160;Clauses:</text><text class="terminal-2545847897-r2" x="183" y="1362" textLength="36.6" clip-path="url(#terminal-2545847897-line-55)">&#160;17</text><text class="terminal-2545847897-r2" x="1342" y="1362" textLength="12.2" clip-path="url(#terminal-2545847897-line-55)"> 474 + </text><text class="terminal-2545847897-r3" x="24.4" y="1386.4" textLength="146.4" clip-path="url(#terminal-2545847897-line-56)">Canon&#160;Types:</text><text class="terminal-2545847897-r3" x="183" y="1386.4" textLength="817.4" clip-path="url(#terminal-2545847897-line-56)">38&#160;REQUIREMENT,&#160;17&#160;CONTEXT,&#160;7&#160;CONSTRAINT,&#160;3&#160;INVARIANT,&#160;1&#160;DEFINITION</text><text class="terminal-2545847897-r2" x="1342" y="1386.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-56)"> 475 + </text><text class="terminal-2545847897-r3" x="24.4" y="1410.8" textLength="134.2" clip-path="url(#terminal-2545847897-line-57)">Resolution:</text><text class="terminal-2545847897-r2" x="158.6" y="1410.8" textLength="134.2" clip-path="url(#terminal-2545847897-line-57)">&#160;321&#160;edges&#160;</text><text class="terminal-2545847897-r3" x="292.8" y="1410.8" textLength="195.2" clip-path="url(#terminal-2545847897-line-57)">(44%&#160;relates_to)</text><text class="terminal-2545847897-r3" x="488" y="1410.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-57)">,</text><text class="terminal-2545847897-r2" x="500.2" y="1410.8" textLength="158.6" clip-path="url(#terminal-2545847897-line-57)">&#160;max&#160;degree&#160;8</text><text class="terminal-2545847897-r3" x="658.8" y="1410.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-57)">,</text><text class="terminal-2545847897-r2" x="671" y="1410.8" textLength="158.6" clip-path="url(#terminal-2545847897-line-57)">&#160;0%&#160;hierarchy</text><text class="terminal-2545847897-r2" x="1342" y="1410.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-57)"> 476 + </text><text class="terminal-2545847897-r3" x="24.4" y="1435.2" textLength="109.8" clip-path="url(#terminal-2545847897-line-58)">Coverage:</text><text class="terminal-2545847897-r7" x="146.4" y="1435.2" textLength="36.6" clip-path="url(#terminal-2545847897-line-58)">76%</text><text class="terminal-2545847897-r2" x="183" y="1435.2" textLength="134.2" clip-path="url(#terminal-2545847897-line-58)">&#160;extraction</text><text class="terminal-2545847897-r3" x="317.2" y="1435.2" textLength="268.4" clip-path="url(#terminal-2545847897-line-58)">&#160;(4&#160;clauses&#160;below&#160;80%)</text><text class="terminal-2545847897-r2" x="1342" y="1435.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-58)"> 477 + </text><text class="terminal-2545847897-r2" x="1342" y="1459.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-59)"> 478 + </text><text class="terminal-2545847897-r3" x="24.4" y="1484" textLength="85.4" clip-path="url(#terminal-2545847897-line-60)">D-Rate:</text><text class="terminal-2545847897-r3" x="122" y="1484" textLength="85.4" clip-path="url(#terminal-2545847897-line-60)">no&#160;data</text><text class="terminal-2545847897-r2" x="1342" y="1484" textLength="12.2" clip-path="url(#terminal-2545847897-line-60)"> 479 + </text><text class="terminal-2545847897-r3" x="24.4" y="1508.4" textLength="73.2" clip-path="url(#terminal-2545847897-line-61)">Drift:</text><text class="terminal-2545847897-r4" x="109.8" y="1508.4" textLength="61" clip-path="url(#terminal-2545847897-line-61)">clean</text><text class="terminal-2545847897-r3" x="183" y="1508.4" textLength="122" clip-path="url(#terminal-2545847897-line-61)">(12&#160;clean)</text><text class="terminal-2545847897-r2" x="1342" y="1508.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-61)"> 480 + </text><text class="terminal-2545847897-r3" x="24.4" y="1532.8" textLength="109.8" clip-path="url(#terminal-2545847897-line-62)">Evidence:</text><text class="terminal-2545847897-r4" x="146.4" y="1532.8" textLength="73.2" clip-path="url(#terminal-2545847897-line-62)">0&#160;pass</text><text class="terminal-2545847897-r2" x="219.6" y="1532.8" textLength="24.4" clip-path="url(#terminal-2545847897-line-62)">,&#160;</text><text class="terminal-2545847897-r3" x="244" y="1532.8" textLength="73.2" clip-path="url(#terminal-2545847897-line-62)">0&#160;fail</text><text class="terminal-2545847897-r2" x="317.2" y="1532.8" textLength="24.4" clip-path="url(#terminal-2545847897-line-62)">,&#160;</text><text class="terminal-2545847897-r6" x="341.6" y="1532.8" textLength="158.6" clip-path="url(#terminal-2545847897-line-62)">12&#160;incomplete</text><text class="terminal-2545847897-r2" x="1342" y="1532.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-62)"> 481 + </text><text class="terminal-2545847897-r3" x="24.4" y="1557.2" textLength="109.8" clip-path="url(#terminal-2545847897-line-63)">Cascades:</text><text class="terminal-2545847897-r3" x="146.4" y="1557.2" textLength="48.8" clip-path="url(#terminal-2545847897-line-63)">none</text><text class="terminal-2545847897-r2" x="1342" y="1557.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-63)"> 482 + </text><text class="terminal-2545847897-r2" x="1342" y="1581.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-64)"> 483 + </text><text class="terminal-2545847897-r1" x="0" y="1606" textLength="256.2" clip-path="url(#terminal-2545847897-line-65)">&#160;&#160;───&#160;Diagnostics&#160;───</text><text class="terminal-2545847897-r2" x="1342" y="1606" textLength="12.2" clip-path="url(#terminal-2545847897-line-65)"> 484 + </text><text class="terminal-2545847897-r2" x="1342" y="1630.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-66)"> 485 + </text><text class="terminal-2545847897-r1" x="24.4" y="1654.8" textLength="97.6" clip-path="url(#terminal-2545847897-line-67)">Warnings</text><text class="terminal-2545847897-r2" x="122" y="1654.8" textLength="73.2" clip-path="url(#terminal-2545847897-line-67)">&#160;(12):</text><text class="terminal-2545847897-r2" x="1342" y="1654.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-67)"> 486 + </text><text class="terminal-2545847897-r6" x="48.8" y="1679.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-68)">⚠</text><text class="terminal-2545847897-r1" x="73.2" y="1679.2" textLength="97.6" clip-path="url(#terminal-2545847897-line-68)">evidence</text><text class="terminal-2545847897-r3" x="183" y="1679.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-68)">·</text><text class="terminal-2545847897-r2" x="195.2" y="1679.2" textLength="122" clip-path="url(#terminal-2545847897-line-68)">&#160;Endpoints</text><text class="terminal-2545847897-r2" x="1342" y="1679.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-68)"> 487 + </text><text class="terminal-2545847897-r2" x="0" y="1703.6" textLength="1281" clip-path="url(#terminal-2545847897-line-69)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;static_analysis</text><text class="terminal-2545847897-r2" x="1342" y="1703.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-69)"> 488 + </text><text class="terminal-2545847897-r3" x="73.2" y="1728" textLength="12.2" clip-path="url(#terminal-2545847897-line-70)">→</text><text class="terminal-2545847897-r3" x="97.6" y="1728" textLength="475.8" clip-path="url(#terminal-2545847897-line-70)">Collect&#160;required&#160;evidence&#160;for&#160;high&#160;tier</text><text class="terminal-2545847897-r2" x="1342" y="1728" textLength="12.2" clip-path="url(#terminal-2545847897-line-70)"> 489 + </text><text class="terminal-2545847897-r6" x="48.8" y="1752.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-71)">⚠</text><text class="terminal-2545847897-r1" x="73.2" y="1752.4" textLength="97.6" clip-path="url(#terminal-2545847897-line-71)">evidence</text><text class="terminal-2545847897-r3" x="183" y="1752.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-71)">·</text><text class="terminal-2545847897-r2" x="195.2" y="1752.4" textLength="183" clip-path="url(#terminal-2545847897-line-71)">&#160;Error&#160;Handling</text><text class="terminal-2545847897-r2" x="1342" y="1752.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-71)"> 490 + </text><text class="terminal-2545847897-r2" x="0" y="1776.8" textLength="878.4" clip-path="url(#terminal-2545847897-line-72)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests</text><text class="terminal-2545847897-r2" x="1342" y="1776.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-72)"> 491 + </text><text class="terminal-2545847897-r3" x="73.2" y="1801.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-73)">→</text><text class="terminal-2545847897-r3" x="97.6" y="1801.2" textLength="500.2" clip-path="url(#terminal-2545847897-line-73)">Collect&#160;required&#160;evidence&#160;for&#160;medium&#160;tier</text><text class="terminal-2545847897-r2" x="1342" y="1801.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-73)"> 492 + </text><text class="terminal-2545847897-r6" x="48.8" y="1825.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-74)">⚠</text><text class="terminal-2545847897-r1" x="73.2" y="1825.6" textLength="97.6" clip-path="url(#terminal-2545847897-line-74)">evidence</text><text class="terminal-2545847897-r3" x="183" y="1825.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-74)">·</text><text class="terminal-2545847897-r2" x="195.2" y="1825.6" textLength="195.2" clip-path="url(#terminal-2545847897-line-74)">&#160;Response&#160;Format</text><text class="terminal-2545847897-r2" x="1342" y="1825.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-74)"> 493 + </text><text class="terminal-2545847897-r2" x="0" y="1850" textLength="732" clip-path="url(#terminal-2545847897-line-75)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation</text><text class="terminal-2545847897-r2" x="1342" y="1850" textLength="12.2" clip-path="url(#terminal-2545847897-line-75)"> 494 + </text><text class="terminal-2545847897-r3" x="73.2" y="1874.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-76)">→</text><text class="terminal-2545847897-r3" x="97.6" y="1874.4" textLength="463.6" clip-path="url(#terminal-2545847897-line-76)">Collect&#160;required&#160;evidence&#160;for&#160;low&#160;tier</text><text class="terminal-2545847897-r2" x="1342" y="1874.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-76)"> 495 + </text><text class="terminal-2545847897-r6" x="48.8" y="1898.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-77)">⚠</text><text class="terminal-2545847897-r1" x="73.2" y="1898.8" textLength="97.6" clip-path="url(#terminal-2545847897-line-77)">evidence</text><text class="terminal-2545847897-r3" x="183" y="1898.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-77)">·</text><text class="terminal-2545847897-r2" x="195.2" y="1898.8" textLength="244" clip-path="url(#terminal-2545847897-line-77)">&#160;Balance&#160;Calculation</text><text class="terminal-2545847897-r2" x="1342" y="1898.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-77)"> 496 + </text><text class="terminal-2545847897-r2" x="0" y="1923.2" textLength="1281" clip-path="url(#terminal-2545847897-line-78)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;static_analysis</text><text class="terminal-2545847897-r2" x="1342" y="1923.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-78)"> 497 + </text><text class="terminal-2545847897-r3" x="73.2" y="1947.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-79)">→</text><text class="terminal-2545847897-r3" x="97.6" y="1947.6" textLength="475.8" clip-path="url(#terminal-2545847897-line-79)">Collect&#160;required&#160;evidence&#160;for&#160;high&#160;tier</text><text class="terminal-2545847897-r2" x="1342" y="1947.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-79)"> 498 + </text><text class="terminal-2545847897-r6" x="48.8" y="1972" textLength="12.2" clip-path="url(#terminal-2545847897-line-80)">⚠</text><text class="terminal-2545847897-r1" x="73.2" y="1972" textLength="97.6" clip-path="url(#terminal-2545847897-line-80)">evidence</text><text class="terminal-2545847897-r3" x="183" y="1972" textLength="12.2" clip-path="url(#terminal-2545847897-line-80)">·</text><text class="terminal-2545847897-r2" x="195.2" y="1972" textLength="244" clip-path="url(#terminal-2545847897-line-80)">&#160;Creating&#160;an&#160;Expense</text><text class="terminal-2545847897-r2" x="1342" y="1972" textLength="12.2" clip-path="url(#terminal-2545847897-line-80)"> 499 + </text><text class="terminal-2545847897-r2" x="0" y="1996.4" textLength="1281" clip-path="url(#terminal-2545847897-line-81)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;static_analysis</text><text class="terminal-2545847897-r2" x="1342" y="1996.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-81)"> 500 + </text><text class="terminal-2545847897-r3" x="73.2" y="2020.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-82)">→</text><text class="terminal-2545847897-r3" x="97.6" y="2020.8" textLength="475.8" clip-path="url(#terminal-2545847897-line-82)">Collect&#160;required&#160;evidence&#160;for&#160;high&#160;tier</text><text class="terminal-2545847897-r2" x="1342" y="2020.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-82)"> 501 + </text><text class="terminal-2545847897-r6" x="48.8" y="2045.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-83)">⚠</text><text class="terminal-2545847897-r1" x="73.2" y="2045.2" textLength="97.6" clip-path="url(#terminal-2545847897-line-83)">evidence</text><text class="terminal-2545847897-r3" x="183" y="2045.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-83)">·</text><text class="terminal-2545847897-r2" x="195.2" y="2045.2" textLength="195.2" clip-path="url(#terminal-2545847897-line-83)">&#160;Expense&#160;History</text><text class="terminal-2545847897-r2" x="1342" y="2045.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-83)"> 502 + </text><text class="terminal-2545847897-r2" x="0" y="2069.6" textLength="878.4" clip-path="url(#terminal-2545847897-line-84)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests</text><text class="terminal-2545847897-r2" x="1342" y="2069.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-84)"> 503 + </text><text class="terminal-2545847897-r3" x="73.2" y="2094" textLength="12.2" clip-path="url(#terminal-2545847897-line-85)">→</text><text class="terminal-2545847897-r3" x="97.6" y="2094" textLength="500.2" clip-path="url(#terminal-2545847897-line-85)">Collect&#160;required&#160;evidence&#160;for&#160;medium&#160;tier</text><text class="terminal-2545847897-r2" x="1342" y="2094" textLength="12.2" clip-path="url(#terminal-2545847897-line-85)"> 504 + </text><text class="terminal-2545847897-r6" x="48.8" y="2118.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-86)">⚠</text><text class="terminal-2545847897-r1" x="73.2" y="2118.4" textLength="97.6" clip-path="url(#terminal-2545847897-line-86)">evidence</text><text class="terminal-2545847897-r3" x="183" y="2118.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-86)">·</text><text class="terminal-2545847897-r2" x="195.2" y="2118.4" textLength="207.4" clip-path="url(#terminal-2545847897-line-86)">&#160;Split&#160;Strategies</text><text class="terminal-2545847897-r2" x="1342" y="2118.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-86)"> 505 + </text><text class="terminal-2545847897-r2" x="0" y="2142.8" textLength="1281" clip-path="url(#terminal-2545847897-line-87)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;static_analysis</text><text class="terminal-2545847897-r2" x="1342" y="2142.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-87)"> 506 + </text><text class="terminal-2545847897-r3" x="73.2" y="2167.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-88)">→</text><text class="terminal-2545847897-r3" x="97.6" y="2167.2" textLength="475.8" clip-path="url(#terminal-2545847897-line-88)">Collect&#160;required&#160;evidence&#160;for&#160;high&#160;tier</text><text class="terminal-2545847897-r2" x="1342" y="2167.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-88)"> 507 + </text><text class="terminal-2545847897-r6" x="48.8" y="2191.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-89)">⚠</text><text class="terminal-2545847897-r1" x="73.2" y="2191.6" textLength="97.6" clip-path="url(#terminal-2545847897-line-89)">evidence</text><text class="terminal-2545847897-r3" x="183" y="2191.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-89)">·</text><text class="terminal-2545847897-r2" x="195.2" y="2191.6" textLength="207.4" clip-path="url(#terminal-2545847897-line-89)">&#160;Group&#160;Management</text><text class="terminal-2545847897-r2" x="1342" y="2191.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-89)"> 508 + </text><text class="terminal-2545847897-r2" x="0" y="2216" textLength="1281" clip-path="url(#terminal-2545847897-line-90)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;static_analysis</text><text class="terminal-2545847897-r2" x="1342" y="2216" textLength="12.2" clip-path="url(#terminal-2545847897-line-90)"> 509 + </text><text class="terminal-2545847897-r3" x="73.2" y="2240.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-91)">→</text><text class="terminal-2545847897-r3" x="97.6" y="2240.4" textLength="475.8" clip-path="url(#terminal-2545847897-line-91)">Collect&#160;required&#160;evidence&#160;for&#160;high&#160;tier</text><text class="terminal-2545847897-r2" x="1342" y="2240.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-91)"> 510 + </text><text class="terminal-2545847897-r6" x="48.8" y="2264.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-92)">⚠</text><text class="terminal-2545847897-r1" x="73.2" y="2264.8" textLength="97.6" clip-path="url(#terminal-2545847897-line-92)">evidence</text><text class="terminal-2545847897-r3" x="183" y="2264.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-92)">·</text><text class="terminal-2545847897-r2" x="195.2" y="2264.8" textLength="170.8" clip-path="url(#terminal-2545847897-line-92)">&#160;Group&#160;Summary</text><text class="terminal-2545847897-r2" x="1342" y="2264.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-92)"> 511 + </text><text class="terminal-2545847897-r2" x="0" y="2289.2" textLength="1281" clip-path="url(#terminal-2545847897-line-93)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;static_analysis</text><text class="terminal-2545847897-r2" x="1342" y="2289.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-93)"> 512 + </text><text class="terminal-2545847897-r3" x="73.2" y="2313.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-94)">→</text><text class="terminal-2545847897-r3" x="97.6" y="2313.6" textLength="475.8" clip-path="url(#terminal-2545847897-line-94)">Collect&#160;required&#160;evidence&#160;for&#160;high&#160;tier</text><text class="terminal-2545847897-r2" x="1342" y="2313.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-94)"> 513 + </text><text class="terminal-2545847897-r6" x="48.8" y="2338" textLength="12.2" clip-path="url(#terminal-2545847897-line-95)">⚠</text><text class="terminal-2545847897-r1" x="73.2" y="2338" textLength="97.6" clip-path="url(#terminal-2545847897-line-95)">evidence</text><text class="terminal-2545847897-r3" x="183" y="2338" textLength="12.2" clip-path="url(#terminal-2545847897-line-95)">·</text><text class="terminal-2545847897-r2" x="195.2" y="2338" textLength="244" clip-path="url(#terminal-2545847897-line-95)">&#160;Debt&#160;Simplification</text><text class="terminal-2545847897-r2" x="1342" y="2338" textLength="12.2" clip-path="url(#terminal-2545847897-line-95)"> 514 + </text><text class="terminal-2545847897-r2" x="0" y="2362.4" textLength="1281" clip-path="url(#terminal-2545847897-line-96)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;static_analysis</text><text class="terminal-2545847897-r2" x="1342" y="2362.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-96)"> 515 + </text><text class="terminal-2545847897-r3" x="73.2" y="2386.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-97)">→</text><text class="terminal-2545847897-r3" x="97.6" y="2386.8" textLength="475.8" clip-path="url(#terminal-2545847897-line-97)">Collect&#160;required&#160;evidence&#160;for&#160;high&#160;tier</text><text class="terminal-2545847897-r2" x="1342" y="2386.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-97)"> 516 + </text><text class="terminal-2545847897-r6" x="48.8" y="2411.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-98)">⚠</text><text class="terminal-2545847897-r1" x="73.2" y="2411.2" textLength="97.6" clip-path="url(#terminal-2545847897-line-98)">evidence</text><text class="terminal-2545847897-r3" x="183" y="2411.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-98)">·</text><text class="terminal-2545847897-r2" x="195.2" y="2411.2" textLength="268.4" clip-path="url(#terminal-2545847897-line-98)">&#160;Recording&#160;Settlements</text><text class="terminal-2545847897-r2" x="1342" y="2411.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-98)"> 517 + </text><text class="terminal-2545847897-r2" x="0" y="2435.6" textLength="732" clip-path="url(#terminal-2545847897-line-99)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation</text><text class="terminal-2545847897-r2" x="1342" y="2435.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-99)"> 518 + </text><text class="terminal-2545847897-r3" x="73.2" y="2460" textLength="12.2" clip-path="url(#terminal-2545847897-line-100)">→</text><text class="terminal-2545847897-r3" x="97.6" y="2460" textLength="463.6" clip-path="url(#terminal-2545847897-line-100)">Collect&#160;required&#160;evidence&#160;for&#160;low&#160;tier</text><text class="terminal-2545847897-r2" x="1342" y="2460" textLength="12.2" clip-path="url(#terminal-2545847897-line-100)"> 519 + </text><text class="terminal-2545847897-r6" x="48.8" y="2484.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-101)">⚠</text><text class="terminal-2545847897-r1" x="73.2" y="2484.4" textLength="97.6" clip-path="url(#terminal-2545847897-line-101)">evidence</text><text class="terminal-2545847897-r3" x="183" y="2484.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-101)">·</text><text class="terminal-2545847897-r2" x="195.2" y="2484.4" textLength="219.6" clip-path="url(#terminal-2545847897-line-101)">&#160;Settlement&#160;Status</text><text class="terminal-2545847897-r2" x="1342" y="2484.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-101)"> 520 + </text><text class="terminal-2545847897-r2" x="0" y="2508.8" textLength="732" clip-path="url(#terminal-2545847897-line-102)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation</text><text class="terminal-2545847897-r2" x="1342" y="2508.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-102)"> 521 + </text><text class="terminal-2545847897-r3" x="73.2" y="2533.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-103)">→</text><text class="terminal-2545847897-r3" x="97.6" y="2533.2" textLength="463.6" clip-path="url(#terminal-2545847897-line-103)">Collect&#160;required&#160;evidence&#160;for&#160;low&#160;tier</text><text class="terminal-2545847897-r2" x="1342" y="2533.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-103)"> 522 + </text><text class="terminal-2545847897-r2" x="1342" y="2557.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-104)"> 523 + </text><text class="terminal-2545847897-r1" x="24.4" y="2582" textLength="48.8" clip-path="url(#terminal-2545847897-line-105)">Info</text><text class="terminal-2545847897-r2" x="73.2" y="2582" textLength="61" clip-path="url(#terminal-2545847897-line-105)">&#160;(4):</text><text class="terminal-2545847897-r2" x="1342" y="2582" textLength="12.2" clip-path="url(#terminal-2545847897-line-105)"> 524 + </text><text class="terminal-2545847897-r8" x="48.8" y="2606.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-106)">ℹ</text><text class="terminal-2545847897-r1" x="73.2" y="2606.4" textLength="61" clip-path="url(#terminal-2545847897-line-106)">canon</text><text class="terminal-2545847897-r3" x="146.4" y="2606.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-106)">·</text><text class="terminal-2545847897-r2" x="158.6" y="2606.4" textLength="158.6" clip-path="url(#terminal-2545847897-line-106)">&#160;d2f17c887607</text><text class="terminal-2545847897-r2" x="1342" y="2606.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-106)"> 525 + </text><text class="terminal-2545847897-r2" x="0" y="2630.8" textLength="536.8" clip-path="url(#terminal-2545847897-line-107)">&#160;&#160;&#160;&#160;&#160;&#160;Extraction&#160;coverage&#160;0%&#160;(0/0&#160;sentences)</text><text class="terminal-2545847897-r2" x="1342" y="2630.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-107)"> 526 + </text><text class="terminal-2545847897-r8" x="48.8" y="2655.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-108)">ℹ</text><text class="terminal-2545847897-r1" x="73.2" y="2655.2" textLength="61" clip-path="url(#terminal-2545847897-line-108)">canon</text><text class="terminal-2545847897-r3" x="146.4" y="2655.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-108)">·</text><text class="terminal-2545847897-r2" x="158.6" y="2655.2" textLength="158.6" clip-path="url(#terminal-2545847897-line-108)">&#160;c1b941bf88a0</text><text class="terminal-2545847897-r2" x="1342" y="2655.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-108)"> 527 + </text><text class="terminal-2545847897-r2" x="0" y="2679.6" textLength="536.8" clip-path="url(#terminal-2545847897-line-109)">&#160;&#160;&#160;&#160;&#160;&#160;Extraction&#160;coverage&#160;0%&#160;(0/0&#160;sentences)</text><text class="terminal-2545847897-r2" x="1342" y="2679.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-109)"> 528 + </text><text class="terminal-2545847897-r8" x="48.8" y="2704" textLength="12.2" clip-path="url(#terminal-2545847897-line-110)">ℹ</text><text class="terminal-2545847897-r1" x="73.2" y="2704" textLength="61" clip-path="url(#terminal-2545847897-line-110)">canon</text><text class="terminal-2545847897-r3" x="146.4" y="2704" textLength="12.2" clip-path="url(#terminal-2545847897-line-110)">·</text><text class="terminal-2545847897-r2" x="158.6" y="2704" textLength="158.6" clip-path="url(#terminal-2545847897-line-110)">&#160;54a00fe89a93</text><text class="terminal-2545847897-r2" x="1342" y="2704" textLength="12.2" clip-path="url(#terminal-2545847897-line-110)"> 529 + </text><text class="terminal-2545847897-r2" x="0" y="2728.4" textLength="536.8" clip-path="url(#terminal-2545847897-line-111)">&#160;&#160;&#160;&#160;&#160;&#160;Extraction&#160;coverage&#160;0%&#160;(0/0&#160;sentences)</text><text class="terminal-2545847897-r2" x="1342" y="2728.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-111)"> 530 + </text><text class="terminal-2545847897-r8" x="48.8" y="2752.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-112)">ℹ</text><text class="terminal-2545847897-r1" x="73.2" y="2752.8" textLength="61" clip-path="url(#terminal-2545847897-line-112)">canon</text><text class="terminal-2545847897-r3" x="146.4" y="2752.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-112)">·</text><text class="terminal-2545847897-r2" x="158.6" y="2752.8" textLength="158.6" clip-path="url(#terminal-2545847897-line-112)">&#160;09825f42242d</text><text class="terminal-2545847897-r2" x="1342" y="2752.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-112)"> 531 + </text><text class="terminal-2545847897-r2" x="0" y="2777.2" textLength="536.8" clip-path="url(#terminal-2545847897-line-113)">&#160;&#160;&#160;&#160;&#160;&#160;Extraction&#160;coverage&#160;0%&#160;(0/0&#160;sentences)</text><text class="terminal-2545847897-r2" x="1342" y="2777.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-113)"> 532 + </text><text class="terminal-2545847897-r2" x="1342" y="2801.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-114)"> 533 + </text><text class="terminal-2545847897-r6" x="24.4" y="2826" textLength="134.2" clip-path="url(#terminal-2545847897-line-115)">12&#160;warnings</text><text class="terminal-2545847897-r2" x="158.6" y="2826" textLength="24.4" clip-path="url(#terminal-2545847897-line-115)">,&#160;</text><text class="terminal-2545847897-r8" x="183" y="2826" textLength="73.2" clip-path="url(#terminal-2545847897-line-115)">4&#160;info</text><text class="terminal-2545847897-r2" x="1342" y="2826" textLength="12.2" clip-path="url(#terminal-2545847897-line-115)"> 534 + </text><text class="terminal-2545847897-r2" x="1342" y="2850.4" textLength="12.2" clip-path="url(#terminal-2545847897-line-116)"> 535 + </text><text class="terminal-2545847897-r4" x="0" y="2874.8" textLength="280.6" clip-path="url(#terminal-2545847897-line-117)">&#160;&#160;✔&#160;Bootstrap&#160;complete.</text><text class="terminal-2545847897-r2" x="1342" y="2874.8" textLength="12.2" clip-path="url(#terminal-2545847897-line-117)"> 536 + </text><text class="terminal-2545847897-r2" x="0" y="2899.2" textLength="134.2" clip-path="url(#terminal-2545847897-line-118)">&#160;&#160;&#160;&#160;State:&#160;</text><text class="terminal-2545847897-r5" x="134.2" y="2899.2" textLength="207.4" clip-path="url(#terminal-2545847897-line-118)">BOOTSTRAP_WARMING</text><text class="terminal-2545847897-r2" x="1342" y="2899.2" textLength="12.2" clip-path="url(#terminal-2545847897-line-118)"> 537 + </text><text class="terminal-2545847897-r2" x="0" y="2923.6" textLength="97.6" clip-path="url(#terminal-2545847897-line-119)">&#160;&#160;&#160;&#160;Run&#160;</text><text class="terminal-2545847897-r5" x="97.6" y="2923.6" textLength="170.8" clip-path="url(#terminal-2545847897-line-119)">phoenix&#160;status</text><text class="terminal-2545847897-r2" x="268.4" y="2923.6" textLength="341.6" clip-path="url(#terminal-2545847897-line-119)">&#160;to&#160;see&#160;the&#160;trust&#160;dashboard.</text><text class="terminal-2545847897-r2" x="1342" y="2923.6" textLength="12.2" clip-path="url(#terminal-2545847897-line-119)"> 538 + </text> 539 + </g> 540 + </g> 541 + </svg>
examples/settle-up/screenshots/03-status.png

This is a binary file and will not be displayed.

+356
examples/settle-up/screenshots/03-status.svg
··· 1 + <svg class="rich-terminal" viewBox="0 0 1238 1855.6" xmlns="http://www.w3.org/2000/svg"> 2 + <!-- Generated with Rich https://www.textualize.io --> 3 + <style> 4 + 5 + @font-face { 6 + font-family: "Fira Code"; 7 + src: local("FiraCode-Regular"), 8 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), 9 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); 10 + font-style: normal; 11 + font-weight: 400; 12 + } 13 + @font-face { 14 + font-family: "Fira Code"; 15 + src: local("FiraCode-Bold"), 16 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), 17 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); 18 + font-style: bold; 19 + font-weight: 700; 20 + } 21 + 22 + .terminal-4100504436-matrix { 23 + font-family: Fira Code, monospace; 24 + font-size: 20px; 25 + line-height: 24.4px; 26 + font-variant-east-asian: full-width; 27 + } 28 + 29 + .terminal-4100504436-title { 30 + font-size: 18px; 31 + font-weight: bold; 32 + font-family: arial; 33 + } 34 + 35 + .terminal-4100504436-r1 { fill: #c5c8c6 } 36 + .terminal-4100504436-r2 { fill: #c5c8c6;font-weight: bold } 37 + .terminal-4100504436-r3 { fill: #868887 } 38 + .terminal-4100504436-r4 { fill: #d0b344 } 39 + .terminal-4100504436-r5 { fill: #cc555a } 40 + .terminal-4100504436-r6 { fill: #98a84b } 41 + .terminal-4100504436-r7 { fill: #608ab1 } 42 + </style> 43 + 44 + <defs> 45 + <clipPath id="terminal-4100504436-clip-terminal"> 46 + <rect x="0" y="0" width="1219.0" height="1804.6" /> 47 + </clipPath> 48 + <clipPath id="terminal-4100504436-line-0"> 49 + <rect x="0" y="1.5" width="1220" height="24.65"/> 50 + </clipPath> 51 + <clipPath id="terminal-4100504436-line-1"> 52 + <rect x="0" y="25.9" width="1220" height="24.65"/> 53 + </clipPath> 54 + <clipPath id="terminal-4100504436-line-2"> 55 + <rect x="0" y="50.3" width="1220" height="24.65"/> 56 + </clipPath> 57 + <clipPath id="terminal-4100504436-line-3"> 58 + <rect x="0" y="74.7" width="1220" height="24.65"/> 59 + </clipPath> 60 + <clipPath id="terminal-4100504436-line-4"> 61 + <rect x="0" y="99.1" width="1220" height="24.65"/> 62 + </clipPath> 63 + <clipPath id="terminal-4100504436-line-5"> 64 + <rect x="0" y="123.5" width="1220" height="24.65"/> 65 + </clipPath> 66 + <clipPath id="terminal-4100504436-line-6"> 67 + <rect x="0" y="147.9" width="1220" height="24.65"/> 68 + </clipPath> 69 + <clipPath id="terminal-4100504436-line-7"> 70 + <rect x="0" y="172.3" width="1220" height="24.65"/> 71 + </clipPath> 72 + <clipPath id="terminal-4100504436-line-8"> 73 + <rect x="0" y="196.7" width="1220" height="24.65"/> 74 + </clipPath> 75 + <clipPath id="terminal-4100504436-line-9"> 76 + <rect x="0" y="221.1" width="1220" height="24.65"/> 77 + </clipPath> 78 + <clipPath id="terminal-4100504436-line-10"> 79 + <rect x="0" y="245.5" width="1220" height="24.65"/> 80 + </clipPath> 81 + <clipPath id="terminal-4100504436-line-11"> 82 + <rect x="0" y="269.9" width="1220" height="24.65"/> 83 + </clipPath> 84 + <clipPath id="terminal-4100504436-line-12"> 85 + <rect x="0" y="294.3" width="1220" height="24.65"/> 86 + </clipPath> 87 + <clipPath id="terminal-4100504436-line-13"> 88 + <rect x="0" y="318.7" width="1220" height="24.65"/> 89 + </clipPath> 90 + <clipPath id="terminal-4100504436-line-14"> 91 + <rect x="0" y="343.1" width="1220" height="24.65"/> 92 + </clipPath> 93 + <clipPath id="terminal-4100504436-line-15"> 94 + <rect x="0" y="367.5" width="1220" height="24.65"/> 95 + </clipPath> 96 + <clipPath id="terminal-4100504436-line-16"> 97 + <rect x="0" y="391.9" width="1220" height="24.65"/> 98 + </clipPath> 99 + <clipPath id="terminal-4100504436-line-17"> 100 + <rect x="0" y="416.3" width="1220" height="24.65"/> 101 + </clipPath> 102 + <clipPath id="terminal-4100504436-line-18"> 103 + <rect x="0" y="440.7" width="1220" height="24.65"/> 104 + </clipPath> 105 + <clipPath id="terminal-4100504436-line-19"> 106 + <rect x="0" y="465.1" width="1220" height="24.65"/> 107 + </clipPath> 108 + <clipPath id="terminal-4100504436-line-20"> 109 + <rect x="0" y="489.5" width="1220" height="24.65"/> 110 + </clipPath> 111 + <clipPath id="terminal-4100504436-line-21"> 112 + <rect x="0" y="513.9" width="1220" height="24.65"/> 113 + </clipPath> 114 + <clipPath id="terminal-4100504436-line-22"> 115 + <rect x="0" y="538.3" width="1220" height="24.65"/> 116 + </clipPath> 117 + <clipPath id="terminal-4100504436-line-23"> 118 + <rect x="0" y="562.7" width="1220" height="24.65"/> 119 + </clipPath> 120 + <clipPath id="terminal-4100504436-line-24"> 121 + <rect x="0" y="587.1" width="1220" height="24.65"/> 122 + </clipPath> 123 + <clipPath id="terminal-4100504436-line-25"> 124 + <rect x="0" y="611.5" width="1220" height="24.65"/> 125 + </clipPath> 126 + <clipPath id="terminal-4100504436-line-26"> 127 + <rect x="0" y="635.9" width="1220" height="24.65"/> 128 + </clipPath> 129 + <clipPath id="terminal-4100504436-line-27"> 130 + <rect x="0" y="660.3" width="1220" height="24.65"/> 131 + </clipPath> 132 + <clipPath id="terminal-4100504436-line-28"> 133 + <rect x="0" y="684.7" width="1220" height="24.65"/> 134 + </clipPath> 135 + <clipPath id="terminal-4100504436-line-29"> 136 + <rect x="0" y="709.1" width="1220" height="24.65"/> 137 + </clipPath> 138 + <clipPath id="terminal-4100504436-line-30"> 139 + <rect x="0" y="733.5" width="1220" height="24.65"/> 140 + </clipPath> 141 + <clipPath id="terminal-4100504436-line-31"> 142 + <rect x="0" y="757.9" width="1220" height="24.65"/> 143 + </clipPath> 144 + <clipPath id="terminal-4100504436-line-32"> 145 + <rect x="0" y="782.3" width="1220" height="24.65"/> 146 + </clipPath> 147 + <clipPath id="terminal-4100504436-line-33"> 148 + <rect x="0" y="806.7" width="1220" height="24.65"/> 149 + </clipPath> 150 + <clipPath id="terminal-4100504436-line-34"> 151 + <rect x="0" y="831.1" width="1220" height="24.65"/> 152 + </clipPath> 153 + <clipPath id="terminal-4100504436-line-35"> 154 + <rect x="0" y="855.5" width="1220" height="24.65"/> 155 + </clipPath> 156 + <clipPath id="terminal-4100504436-line-36"> 157 + <rect x="0" y="879.9" width="1220" height="24.65"/> 158 + </clipPath> 159 + <clipPath id="terminal-4100504436-line-37"> 160 + <rect x="0" y="904.3" width="1220" height="24.65"/> 161 + </clipPath> 162 + <clipPath id="terminal-4100504436-line-38"> 163 + <rect x="0" y="928.7" width="1220" height="24.65"/> 164 + </clipPath> 165 + <clipPath id="terminal-4100504436-line-39"> 166 + <rect x="0" y="953.1" width="1220" height="24.65"/> 167 + </clipPath> 168 + <clipPath id="terminal-4100504436-line-40"> 169 + <rect x="0" y="977.5" width="1220" height="24.65"/> 170 + </clipPath> 171 + <clipPath id="terminal-4100504436-line-41"> 172 + <rect x="0" y="1001.9" width="1220" height="24.65"/> 173 + </clipPath> 174 + <clipPath id="terminal-4100504436-line-42"> 175 + <rect x="0" y="1026.3" width="1220" height="24.65"/> 176 + </clipPath> 177 + <clipPath id="terminal-4100504436-line-43"> 178 + <rect x="0" y="1050.7" width="1220" height="24.65"/> 179 + </clipPath> 180 + <clipPath id="terminal-4100504436-line-44"> 181 + <rect x="0" y="1075.1" width="1220" height="24.65"/> 182 + </clipPath> 183 + <clipPath id="terminal-4100504436-line-45"> 184 + <rect x="0" y="1099.5" width="1220" height="24.65"/> 185 + </clipPath> 186 + <clipPath id="terminal-4100504436-line-46"> 187 + <rect x="0" y="1123.9" width="1220" height="24.65"/> 188 + </clipPath> 189 + <clipPath id="terminal-4100504436-line-47"> 190 + <rect x="0" y="1148.3" width="1220" height="24.65"/> 191 + </clipPath> 192 + <clipPath id="terminal-4100504436-line-48"> 193 + <rect x="0" y="1172.7" width="1220" height="24.65"/> 194 + </clipPath> 195 + <clipPath id="terminal-4100504436-line-49"> 196 + <rect x="0" y="1197.1" width="1220" height="24.65"/> 197 + </clipPath> 198 + <clipPath id="terminal-4100504436-line-50"> 199 + <rect x="0" y="1221.5" width="1220" height="24.65"/> 200 + </clipPath> 201 + <clipPath id="terminal-4100504436-line-51"> 202 + <rect x="0" y="1245.9" width="1220" height="24.65"/> 203 + </clipPath> 204 + <clipPath id="terminal-4100504436-line-52"> 205 + <rect x="0" y="1270.3" width="1220" height="24.65"/> 206 + </clipPath> 207 + <clipPath id="terminal-4100504436-line-53"> 208 + <rect x="0" y="1294.7" width="1220" height="24.65"/> 209 + </clipPath> 210 + <clipPath id="terminal-4100504436-line-54"> 211 + <rect x="0" y="1319.1" width="1220" height="24.65"/> 212 + </clipPath> 213 + <clipPath id="terminal-4100504436-line-55"> 214 + <rect x="0" y="1343.5" width="1220" height="24.65"/> 215 + </clipPath> 216 + <clipPath id="terminal-4100504436-line-56"> 217 + <rect x="0" y="1367.9" width="1220" height="24.65"/> 218 + </clipPath> 219 + <clipPath id="terminal-4100504436-line-57"> 220 + <rect x="0" y="1392.3" width="1220" height="24.65"/> 221 + </clipPath> 222 + <clipPath id="terminal-4100504436-line-58"> 223 + <rect x="0" y="1416.7" width="1220" height="24.65"/> 224 + </clipPath> 225 + <clipPath id="terminal-4100504436-line-59"> 226 + <rect x="0" y="1441.1" width="1220" height="24.65"/> 227 + </clipPath> 228 + <clipPath id="terminal-4100504436-line-60"> 229 + <rect x="0" y="1465.5" width="1220" height="24.65"/> 230 + </clipPath> 231 + <clipPath id="terminal-4100504436-line-61"> 232 + <rect x="0" y="1489.9" width="1220" height="24.65"/> 233 + </clipPath> 234 + <clipPath id="terminal-4100504436-line-62"> 235 + <rect x="0" y="1514.3" width="1220" height="24.65"/> 236 + </clipPath> 237 + <clipPath id="terminal-4100504436-line-63"> 238 + <rect x="0" y="1538.7" width="1220" height="24.65"/> 239 + </clipPath> 240 + <clipPath id="terminal-4100504436-line-64"> 241 + <rect x="0" y="1563.1" width="1220" height="24.65"/> 242 + </clipPath> 243 + <clipPath id="terminal-4100504436-line-65"> 244 + <rect x="0" y="1587.5" width="1220" height="24.65"/> 245 + </clipPath> 246 + <clipPath id="terminal-4100504436-line-66"> 247 + <rect x="0" y="1611.9" width="1220" height="24.65"/> 248 + </clipPath> 249 + <clipPath id="terminal-4100504436-line-67"> 250 + <rect x="0" y="1636.3" width="1220" height="24.65"/> 251 + </clipPath> 252 + <clipPath id="terminal-4100504436-line-68"> 253 + <rect x="0" y="1660.7" width="1220" height="24.65"/> 254 + </clipPath> 255 + <clipPath id="terminal-4100504436-line-69"> 256 + <rect x="0" y="1685.1" width="1220" height="24.65"/> 257 + </clipPath> 258 + <clipPath id="terminal-4100504436-line-70"> 259 + <rect x="0" y="1709.5" width="1220" height="24.65"/> 260 + </clipPath> 261 + <clipPath id="terminal-4100504436-line-71"> 262 + <rect x="0" y="1733.9" width="1220" height="24.65"/> 263 + </clipPath> 264 + <clipPath id="terminal-4100504436-line-72"> 265 + <rect x="0" y="1758.3" width="1220" height="24.65"/> 266 + </clipPath> 267 + </defs> 268 + 269 + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="1853.6" rx="8"/><text class="terminal-4100504436-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">phoenix&#160;—&#160;03-status</text> 270 + <g transform="translate(26,22)"> 271 + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> 272 + <circle cx="22" cy="0" r="7" fill="#febc2e"/> 273 + <circle cx="44" cy="0" r="7" fill="#28c840"/> 274 + </g> 275 + 276 + <g transform="translate(9, 41)" clip-path="url(#terminal-4100504436-clip-terminal)"> 277 + 278 + <g class="terminal-4100504436-matrix"> 279 + <text class="terminal-4100504436-r1" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-4100504436-line-0)"> 280 + </text><text class="terminal-4100504436-r2" x="0" y="44.4" textLength="195.2" clip-path="url(#terminal-4100504436-line-1)">🔥&#160;Phoenix&#160;Status</text><text class="terminal-4100504436-r1" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-1)"> 281 + </text><text class="terminal-4100504436-r1" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-2)"> 282 + </text><text class="terminal-4100504436-r3" x="24.4" y="93.2" textLength="158.6" clip-path="url(#terminal-4100504436-line-3)">System&#160;State:</text><text class="terminal-4100504436-r4" x="195.2" y="93.2" textLength="207.4" clip-path="url(#terminal-4100504436-line-3)">BOOTSTRAP_WARMING</text><text class="terminal-4100504436-r1" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-3)"> 283 + </text><text class="terminal-4100504436-r3" x="24.4" y="117.6" textLength="195.2" clip-path="url(#terminal-4100504436-line-4)">Canonical&#160;Nodes:</text><text class="terminal-4100504436-r1" x="219.6" y="117.6" textLength="36.6" clip-path="url(#terminal-4100504436-line-4)">&#160;66</text><text class="terminal-4100504436-r1" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-4)"> 284 + </text><text class="terminal-4100504436-r3" x="24.4" y="142" textLength="256.2" clip-path="url(#terminal-4100504436-line-5)">Implementation&#160;Units:</text><text class="terminal-4100504436-r1" x="280.6" y="142" textLength="36.6" clip-path="url(#terminal-4100504436-line-5)">&#160;12</text><text class="terminal-4100504436-r1" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-4100504436-line-5)"> 285 + </text><text class="terminal-4100504436-r3" x="24.4" y="166.4" textLength="158.6" clip-path="url(#terminal-4100504436-line-6)">Spec&#160;Clauses:</text><text class="terminal-4100504436-r1" x="183" y="166.4" textLength="36.6" clip-path="url(#terminal-4100504436-line-6)">&#160;17</text><text class="terminal-4100504436-r1" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-6)"> 286 + </text><text class="terminal-4100504436-r3" x="24.4" y="190.8" textLength="146.4" clip-path="url(#terminal-4100504436-line-7)">Canon&#160;Types:</text><text class="terminal-4100504436-r3" x="183" y="190.8" textLength="817.4" clip-path="url(#terminal-4100504436-line-7)">38&#160;REQUIREMENT,&#160;17&#160;CONTEXT,&#160;7&#160;CONSTRAINT,&#160;3&#160;INVARIANT,&#160;1&#160;DEFINITION</text><text class="terminal-4100504436-r1" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-7)"> 287 + </text><text class="terminal-4100504436-r3" x="24.4" y="215.2" textLength="134.2" clip-path="url(#terminal-4100504436-line-8)">Resolution:</text><text class="terminal-4100504436-r1" x="158.6" y="215.2" textLength="134.2" clip-path="url(#terminal-4100504436-line-8)">&#160;321&#160;edges&#160;</text><text class="terminal-4100504436-r3" x="292.8" y="215.2" textLength="195.2" clip-path="url(#terminal-4100504436-line-8)">(44%&#160;relates_to)</text><text class="terminal-4100504436-r3" x="488" y="215.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-8)">,</text><text class="terminal-4100504436-r1" x="500.2" y="215.2" textLength="158.6" clip-path="url(#terminal-4100504436-line-8)">&#160;max&#160;degree&#160;8</text><text class="terminal-4100504436-r3" x="658.8" y="215.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-8)">,</text><text class="terminal-4100504436-r1" x="671" y="215.2" textLength="158.6" clip-path="url(#terminal-4100504436-line-8)">&#160;0%&#160;hierarchy</text><text class="terminal-4100504436-r1" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-8)"> 288 + </text><text class="terminal-4100504436-r3" x="24.4" y="239.6" textLength="109.8" clip-path="url(#terminal-4100504436-line-9)">Coverage:</text><text class="terminal-4100504436-r5" x="146.4" y="239.6" textLength="36.6" clip-path="url(#terminal-4100504436-line-9)">76%</text><text class="terminal-4100504436-r1" x="183" y="239.6" textLength="134.2" clip-path="url(#terminal-4100504436-line-9)">&#160;extraction</text><text class="terminal-4100504436-r3" x="317.2" y="239.6" textLength="268.4" clip-path="url(#terminal-4100504436-line-9)">&#160;(4&#160;clauses&#160;below&#160;80%)</text><text class="terminal-4100504436-r1" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-9)"> 289 + </text><text class="terminal-4100504436-r1" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-4100504436-line-10)"> 290 + </text><text class="terminal-4100504436-r3" x="24.4" y="288.4" textLength="85.4" clip-path="url(#terminal-4100504436-line-11)">D-Rate:</text><text class="terminal-4100504436-r3" x="122" y="288.4" textLength="85.4" clip-path="url(#terminal-4100504436-line-11)">no&#160;data</text><text class="terminal-4100504436-r1" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-11)"> 291 + </text><text class="terminal-4100504436-r3" x="24.4" y="312.8" textLength="73.2" clip-path="url(#terminal-4100504436-line-12)">Drift:</text><text class="terminal-4100504436-r6" x="109.8" y="312.8" textLength="61" clip-path="url(#terminal-4100504436-line-12)">clean</text><text class="terminal-4100504436-r3" x="183" y="312.8" textLength="122" clip-path="url(#terminal-4100504436-line-12)">(12&#160;clean)</text><text class="terminal-4100504436-r1" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-12)"> 292 + </text><text class="terminal-4100504436-r3" x="24.4" y="337.2" textLength="109.8" clip-path="url(#terminal-4100504436-line-13)">Evidence:</text><text class="terminal-4100504436-r6" x="146.4" y="337.2" textLength="73.2" clip-path="url(#terminal-4100504436-line-13)">0&#160;pass</text><text class="terminal-4100504436-r1" x="219.6" y="337.2" textLength="24.4" clip-path="url(#terminal-4100504436-line-13)">,&#160;</text><text class="terminal-4100504436-r3" x="244" y="337.2" textLength="73.2" clip-path="url(#terminal-4100504436-line-13)">0&#160;fail</text><text class="terminal-4100504436-r1" x="317.2" y="337.2" textLength="24.4" clip-path="url(#terminal-4100504436-line-13)">,&#160;</text><text class="terminal-4100504436-r4" x="341.6" y="337.2" textLength="158.6" clip-path="url(#terminal-4100504436-line-13)">12&#160;incomplete</text><text class="terminal-4100504436-r1" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-13)"> 293 + </text><text class="terminal-4100504436-r3" x="24.4" y="361.6" textLength="109.8" clip-path="url(#terminal-4100504436-line-14)">Cascades:</text><text class="terminal-4100504436-r3" x="146.4" y="361.6" textLength="48.8" clip-path="url(#terminal-4100504436-line-14)">none</text><text class="terminal-4100504436-r1" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-14)"> 294 + </text><text class="terminal-4100504436-r1" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-4100504436-line-15)"> 295 + </text><text class="terminal-4100504436-r2" x="0" y="410.4" textLength="256.2" clip-path="url(#terminal-4100504436-line-16)">&#160;&#160;───&#160;Diagnostics&#160;───</text><text class="terminal-4100504436-r1" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-16)"> 296 + </text><text class="terminal-4100504436-r1" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-17)"> 297 + </text><text class="terminal-4100504436-r2" x="24.4" y="459.2" textLength="97.6" clip-path="url(#terminal-4100504436-line-18)">Warnings</text><text class="terminal-4100504436-r1" x="122" y="459.2" textLength="73.2" clip-path="url(#terminal-4100504436-line-18)">&#160;(12):</text><text class="terminal-4100504436-r1" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-18)"> 298 + </text><text class="terminal-4100504436-r4" x="48.8" y="483.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-19)">⚠</text><text class="terminal-4100504436-r2" x="73.2" y="483.6" textLength="97.6" clip-path="url(#terminal-4100504436-line-19)">evidence</text><text class="terminal-4100504436-r3" x="183" y="483.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-19)">·</text><text class="terminal-4100504436-r1" x="195.2" y="483.6" textLength="122" clip-path="url(#terminal-4100504436-line-19)">&#160;Endpoints</text><text class="terminal-4100504436-r1" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-19)"> 299 + </text><text class="terminal-4100504436-r1" x="0" y="508" textLength="1098" clip-path="url(#terminal-4100504436-line-20)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;</text><text class="terminal-4100504436-r1" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-4100504436-line-20)"> 300 + </text><text class="terminal-4100504436-r1" x="0" y="532.4" textLength="183" clip-path="url(#terminal-4100504436-line-21)">static_analysis</text><text class="terminal-4100504436-r1" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-21)"> 301 + </text><text class="terminal-4100504436-r3" x="73.2" y="556.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-22)">→</text><text class="terminal-4100504436-r3" x="97.6" y="556.8" textLength="475.8" clip-path="url(#terminal-4100504436-line-22)">Collect&#160;required&#160;evidence&#160;for&#160;high&#160;tier</text><text class="terminal-4100504436-r1" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-22)"> 302 + </text><text class="terminal-4100504436-r4" x="48.8" y="581.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-23)">⚠</text><text class="terminal-4100504436-r2" x="73.2" y="581.2" textLength="97.6" clip-path="url(#terminal-4100504436-line-23)">evidence</text><text class="terminal-4100504436-r3" x="183" y="581.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-23)">·</text><text class="terminal-4100504436-r1" x="195.2" y="581.2" textLength="183" clip-path="url(#terminal-4100504436-line-23)">&#160;Error&#160;Handling</text><text class="terminal-4100504436-r1" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-23)"> 303 + </text><text class="terminal-4100504436-r1" x="0" y="605.6" textLength="878.4" clip-path="url(#terminal-4100504436-line-24)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests</text><text class="terminal-4100504436-r1" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-24)"> 304 + </text><text class="terminal-4100504436-r3" x="73.2" y="630" textLength="12.2" clip-path="url(#terminal-4100504436-line-25)">→</text><text class="terminal-4100504436-r3" x="97.6" y="630" textLength="500.2" clip-path="url(#terminal-4100504436-line-25)">Collect&#160;required&#160;evidence&#160;for&#160;medium&#160;tier</text><text class="terminal-4100504436-r1" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-4100504436-line-25)"> 305 + </text><text class="terminal-4100504436-r4" x="48.8" y="654.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-26)">⚠</text><text class="terminal-4100504436-r2" x="73.2" y="654.4" textLength="97.6" clip-path="url(#terminal-4100504436-line-26)">evidence</text><text class="terminal-4100504436-r3" x="183" y="654.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-26)">·</text><text class="terminal-4100504436-r1" x="195.2" y="654.4" textLength="195.2" clip-path="url(#terminal-4100504436-line-26)">&#160;Response&#160;Format</text><text class="terminal-4100504436-r1" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-26)"> 306 + </text><text class="terminal-4100504436-r1" x="0" y="678.8" textLength="732" clip-path="url(#terminal-4100504436-line-27)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation</text><text class="terminal-4100504436-r1" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-27)"> 307 + </text><text class="terminal-4100504436-r3" x="73.2" y="703.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-28)">→</text><text class="terminal-4100504436-r3" x="97.6" y="703.2" textLength="463.6" clip-path="url(#terminal-4100504436-line-28)">Collect&#160;required&#160;evidence&#160;for&#160;low&#160;tier</text><text class="terminal-4100504436-r1" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-28)"> 308 + </text><text class="terminal-4100504436-r4" x="48.8" y="727.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-29)">⚠</text><text class="terminal-4100504436-r2" x="73.2" y="727.6" textLength="97.6" clip-path="url(#terminal-4100504436-line-29)">evidence</text><text class="terminal-4100504436-r3" x="183" y="727.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-29)">·</text><text class="terminal-4100504436-r1" x="195.2" y="727.6" textLength="244" clip-path="url(#terminal-4100504436-line-29)">&#160;Balance&#160;Calculation</text><text class="terminal-4100504436-r1" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-29)"> 309 + </text><text class="terminal-4100504436-r1" x="0" y="752" textLength="1098" clip-path="url(#terminal-4100504436-line-30)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;</text><text class="terminal-4100504436-r1" x="1220" y="752" textLength="12.2" clip-path="url(#terminal-4100504436-line-30)"> 310 + </text><text class="terminal-4100504436-r1" x="0" y="776.4" textLength="183" clip-path="url(#terminal-4100504436-line-31)">static_analysis</text><text class="terminal-4100504436-r1" x="1220" y="776.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-31)"> 311 + </text><text class="terminal-4100504436-r3" x="73.2" y="800.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-32)">→</text><text class="terminal-4100504436-r3" x="97.6" y="800.8" textLength="475.8" clip-path="url(#terminal-4100504436-line-32)">Collect&#160;required&#160;evidence&#160;for&#160;high&#160;tier</text><text class="terminal-4100504436-r1" x="1220" y="800.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-32)"> 312 + </text><text class="terminal-4100504436-r4" x="48.8" y="825.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-33)">⚠</text><text class="terminal-4100504436-r2" x="73.2" y="825.2" textLength="97.6" clip-path="url(#terminal-4100504436-line-33)">evidence</text><text class="terminal-4100504436-r3" x="183" y="825.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-33)">·</text><text class="terminal-4100504436-r1" x="195.2" y="825.2" textLength="244" clip-path="url(#terminal-4100504436-line-33)">&#160;Creating&#160;an&#160;Expense</text><text class="terminal-4100504436-r1" x="1220" y="825.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-33)"> 313 + </text><text class="terminal-4100504436-r1" x="0" y="849.6" textLength="1098" clip-path="url(#terminal-4100504436-line-34)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;</text><text class="terminal-4100504436-r1" x="1220" y="849.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-34)"> 314 + </text><text class="terminal-4100504436-r1" x="0" y="874" textLength="183" clip-path="url(#terminal-4100504436-line-35)">static_analysis</text><text class="terminal-4100504436-r1" x="1220" y="874" textLength="12.2" clip-path="url(#terminal-4100504436-line-35)"> 315 + </text><text class="terminal-4100504436-r3" x="73.2" y="898.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-36)">→</text><text class="terminal-4100504436-r3" x="97.6" y="898.4" textLength="475.8" clip-path="url(#terminal-4100504436-line-36)">Collect&#160;required&#160;evidence&#160;for&#160;high&#160;tier</text><text class="terminal-4100504436-r1" x="1220" y="898.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-36)"> 316 + </text><text class="terminal-4100504436-r4" x="48.8" y="922.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-37)">⚠</text><text class="terminal-4100504436-r2" x="73.2" y="922.8" textLength="97.6" clip-path="url(#terminal-4100504436-line-37)">evidence</text><text class="terminal-4100504436-r3" x="183" y="922.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-37)">·</text><text class="terminal-4100504436-r1" x="195.2" y="922.8" textLength="195.2" clip-path="url(#terminal-4100504436-line-37)">&#160;Expense&#160;History</text><text class="terminal-4100504436-r1" x="1220" y="922.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-37)"> 317 + </text><text class="terminal-4100504436-r1" x="0" y="947.2" textLength="878.4" clip-path="url(#terminal-4100504436-line-38)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests</text><text class="terminal-4100504436-r1" x="1220" y="947.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-38)"> 318 + </text><text class="terminal-4100504436-r3" x="73.2" y="971.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-39)">→</text><text class="terminal-4100504436-r3" x="97.6" y="971.6" textLength="500.2" clip-path="url(#terminal-4100504436-line-39)">Collect&#160;required&#160;evidence&#160;for&#160;medium&#160;tier</text><text class="terminal-4100504436-r1" x="1220" y="971.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-39)"> 319 + </text><text class="terminal-4100504436-r4" x="48.8" y="996" textLength="12.2" clip-path="url(#terminal-4100504436-line-40)">⚠</text><text class="terminal-4100504436-r2" x="73.2" y="996" textLength="97.6" clip-path="url(#terminal-4100504436-line-40)">evidence</text><text class="terminal-4100504436-r3" x="183" y="996" textLength="12.2" clip-path="url(#terminal-4100504436-line-40)">·</text><text class="terminal-4100504436-r1" x="195.2" y="996" textLength="207.4" clip-path="url(#terminal-4100504436-line-40)">&#160;Split&#160;Strategies</text><text class="terminal-4100504436-r1" x="1220" y="996" textLength="12.2" clip-path="url(#terminal-4100504436-line-40)"> 320 + </text><text class="terminal-4100504436-r1" x="0" y="1020.4" textLength="1098" clip-path="url(#terminal-4100504436-line-41)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;</text><text class="terminal-4100504436-r1" x="1220" y="1020.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-41)"> 321 + </text><text class="terminal-4100504436-r1" x="0" y="1044.8" textLength="183" clip-path="url(#terminal-4100504436-line-42)">static_analysis</text><text class="terminal-4100504436-r1" x="1220" y="1044.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-42)"> 322 + </text><text class="terminal-4100504436-r3" x="73.2" y="1069.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-43)">→</text><text class="terminal-4100504436-r3" x="97.6" y="1069.2" textLength="475.8" clip-path="url(#terminal-4100504436-line-43)">Collect&#160;required&#160;evidence&#160;for&#160;high&#160;tier</text><text class="terminal-4100504436-r1" x="1220" y="1069.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-43)"> 323 + </text><text class="terminal-4100504436-r4" x="48.8" y="1093.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-44)">⚠</text><text class="terminal-4100504436-r2" x="73.2" y="1093.6" textLength="97.6" clip-path="url(#terminal-4100504436-line-44)">evidence</text><text class="terminal-4100504436-r3" x="183" y="1093.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-44)">·</text><text class="terminal-4100504436-r1" x="195.2" y="1093.6" textLength="207.4" clip-path="url(#terminal-4100504436-line-44)">&#160;Group&#160;Management</text><text class="terminal-4100504436-r1" x="1220" y="1093.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-44)"> 324 + </text><text class="terminal-4100504436-r1" x="0" y="1118" textLength="1098" clip-path="url(#terminal-4100504436-line-45)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;</text><text class="terminal-4100504436-r1" x="1220" y="1118" textLength="12.2" clip-path="url(#terminal-4100504436-line-45)"> 325 + </text><text class="terminal-4100504436-r1" x="0" y="1142.4" textLength="183" clip-path="url(#terminal-4100504436-line-46)">static_analysis</text><text class="terminal-4100504436-r1" x="1220" y="1142.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-46)"> 326 + </text><text class="terminal-4100504436-r3" x="73.2" y="1166.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-47)">→</text><text class="terminal-4100504436-r3" x="97.6" y="1166.8" textLength="475.8" clip-path="url(#terminal-4100504436-line-47)">Collect&#160;required&#160;evidence&#160;for&#160;high&#160;tier</text><text class="terminal-4100504436-r1" x="1220" y="1166.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-47)"> 327 + </text><text class="terminal-4100504436-r4" x="48.8" y="1191.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-48)">⚠</text><text class="terminal-4100504436-r2" x="73.2" y="1191.2" textLength="97.6" clip-path="url(#terminal-4100504436-line-48)">evidence</text><text class="terminal-4100504436-r3" x="183" y="1191.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-48)">·</text><text class="terminal-4100504436-r1" x="195.2" y="1191.2" textLength="170.8" clip-path="url(#terminal-4100504436-line-48)">&#160;Group&#160;Summary</text><text class="terminal-4100504436-r1" x="1220" y="1191.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-48)"> 328 + </text><text class="terminal-4100504436-r1" x="0" y="1215.6" textLength="1098" clip-path="url(#terminal-4100504436-line-49)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;</text><text class="terminal-4100504436-r1" x="1220" y="1215.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-49)"> 329 + </text><text class="terminal-4100504436-r1" x="0" y="1240" textLength="183" clip-path="url(#terminal-4100504436-line-50)">static_analysis</text><text class="terminal-4100504436-r1" x="1220" y="1240" textLength="12.2" clip-path="url(#terminal-4100504436-line-50)"> 330 + </text><text class="terminal-4100504436-r3" x="73.2" y="1264.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-51)">→</text><text class="terminal-4100504436-r3" x="97.6" y="1264.4" textLength="475.8" clip-path="url(#terminal-4100504436-line-51)">Collect&#160;required&#160;evidence&#160;for&#160;high&#160;tier</text><text class="terminal-4100504436-r1" x="1220" y="1264.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-51)"> 331 + </text><text class="terminal-4100504436-r4" x="48.8" y="1288.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-52)">⚠</text><text class="terminal-4100504436-r2" x="73.2" y="1288.8" textLength="97.6" clip-path="url(#terminal-4100504436-line-52)">evidence</text><text class="terminal-4100504436-r3" x="183" y="1288.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-52)">·</text><text class="terminal-4100504436-r1" x="195.2" y="1288.8" textLength="244" clip-path="url(#terminal-4100504436-line-52)">&#160;Debt&#160;Simplification</text><text class="terminal-4100504436-r1" x="1220" y="1288.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-52)"> 332 + </text><text class="terminal-4100504436-r1" x="0" y="1313.2" textLength="1098" clip-path="url(#terminal-4100504436-line-53)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;</text><text class="terminal-4100504436-r1" x="1220" y="1313.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-53)"> 333 + </text><text class="terminal-4100504436-r1" x="0" y="1337.6" textLength="183" clip-path="url(#terminal-4100504436-line-54)">static_analysis</text><text class="terminal-4100504436-r1" x="1220" y="1337.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-54)"> 334 + </text><text class="terminal-4100504436-r3" x="73.2" y="1362" textLength="12.2" clip-path="url(#terminal-4100504436-line-55)">→</text><text class="terminal-4100504436-r3" x="97.6" y="1362" textLength="475.8" clip-path="url(#terminal-4100504436-line-55)">Collect&#160;required&#160;evidence&#160;for&#160;high&#160;tier</text><text class="terminal-4100504436-r1" x="1220" y="1362" textLength="12.2" clip-path="url(#terminal-4100504436-line-55)"> 335 + </text><text class="terminal-4100504436-r4" x="48.8" y="1386.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-56)">⚠</text><text class="terminal-4100504436-r2" x="73.2" y="1386.4" textLength="97.6" clip-path="url(#terminal-4100504436-line-56)">evidence</text><text class="terminal-4100504436-r3" x="183" y="1386.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-56)">·</text><text class="terminal-4100504436-r1" x="195.2" y="1386.4" textLength="268.4" clip-path="url(#terminal-4100504436-line-56)">&#160;Recording&#160;Settlements</text><text class="terminal-4100504436-r1" x="1220" y="1386.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-56)"> 336 + </text><text class="terminal-4100504436-r1" x="0" y="1410.8" textLength="732" clip-path="url(#terminal-4100504436-line-57)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation</text><text class="terminal-4100504436-r1" x="1220" y="1410.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-57)"> 337 + </text><text class="terminal-4100504436-r3" x="73.2" y="1435.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-58)">→</text><text class="terminal-4100504436-r3" x="97.6" y="1435.2" textLength="463.6" clip-path="url(#terminal-4100504436-line-58)">Collect&#160;required&#160;evidence&#160;for&#160;low&#160;tier</text><text class="terminal-4100504436-r1" x="1220" y="1435.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-58)"> 338 + </text><text class="terminal-4100504436-r4" x="48.8" y="1459.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-59)">⚠</text><text class="terminal-4100504436-r2" x="73.2" y="1459.6" textLength="97.6" clip-path="url(#terminal-4100504436-line-59)">evidence</text><text class="terminal-4100504436-r3" x="183" y="1459.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-59)">·</text><text class="terminal-4100504436-r1" x="195.2" y="1459.6" textLength="219.6" clip-path="url(#terminal-4100504436-line-59)">&#160;Settlement&#160;Status</text><text class="terminal-4100504436-r1" x="1220" y="1459.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-59)"> 339 + </text><text class="terminal-4100504436-r1" x="0" y="1484" textLength="732" clip-path="url(#terminal-4100504436-line-60)">&#160;&#160;&#160;&#160;&#160;&#160;Missing&#160;evidence:&#160;typecheck,&#160;lint,&#160;boundary_validation</text><text class="terminal-4100504436-r1" x="1220" y="1484" textLength="12.2" clip-path="url(#terminal-4100504436-line-60)"> 340 + </text><text class="terminal-4100504436-r3" x="73.2" y="1508.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-61)">→</text><text class="terminal-4100504436-r3" x="97.6" y="1508.4" textLength="463.6" clip-path="url(#terminal-4100504436-line-61)">Collect&#160;required&#160;evidence&#160;for&#160;low&#160;tier</text><text class="terminal-4100504436-r1" x="1220" y="1508.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-61)"> 341 + </text><text class="terminal-4100504436-r1" x="1220" y="1532.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-62)"> 342 + </text><text class="terminal-4100504436-r2" x="24.4" y="1557.2" textLength="48.8" clip-path="url(#terminal-4100504436-line-63)">Info</text><text class="terminal-4100504436-r1" x="73.2" y="1557.2" textLength="61" clip-path="url(#terminal-4100504436-line-63)">&#160;(4):</text><text class="terminal-4100504436-r1" x="1220" y="1557.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-63)"> 343 + </text><text class="terminal-4100504436-r7" x="48.8" y="1581.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-64)">ℹ</text><text class="terminal-4100504436-r2" x="73.2" y="1581.6" textLength="61" clip-path="url(#terminal-4100504436-line-64)">canon</text><text class="terminal-4100504436-r3" x="146.4" y="1581.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-64)">·</text><text class="terminal-4100504436-r1" x="158.6" y="1581.6" textLength="158.6" clip-path="url(#terminal-4100504436-line-64)">&#160;d2f17c887607</text><text class="terminal-4100504436-r1" x="1220" y="1581.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-64)"> 344 + </text><text class="terminal-4100504436-r1" x="0" y="1606" textLength="536.8" clip-path="url(#terminal-4100504436-line-65)">&#160;&#160;&#160;&#160;&#160;&#160;Extraction&#160;coverage&#160;0%&#160;(0/0&#160;sentences)</text><text class="terminal-4100504436-r1" x="1220" y="1606" textLength="12.2" clip-path="url(#terminal-4100504436-line-65)"> 345 + </text><text class="terminal-4100504436-r7" x="48.8" y="1630.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-66)">ℹ</text><text class="terminal-4100504436-r2" x="73.2" y="1630.4" textLength="61" clip-path="url(#terminal-4100504436-line-66)">canon</text><text class="terminal-4100504436-r3" x="146.4" y="1630.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-66)">·</text><text class="terminal-4100504436-r1" x="158.6" y="1630.4" textLength="158.6" clip-path="url(#terminal-4100504436-line-66)">&#160;c1b941bf88a0</text><text class="terminal-4100504436-r1" x="1220" y="1630.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-66)"> 346 + </text><text class="terminal-4100504436-r1" x="0" y="1654.8" textLength="536.8" clip-path="url(#terminal-4100504436-line-67)">&#160;&#160;&#160;&#160;&#160;&#160;Extraction&#160;coverage&#160;0%&#160;(0/0&#160;sentences)</text><text class="terminal-4100504436-r1" x="1220" y="1654.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-67)"> 347 + </text><text class="terminal-4100504436-r7" x="48.8" y="1679.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-68)">ℹ</text><text class="terminal-4100504436-r2" x="73.2" y="1679.2" textLength="61" clip-path="url(#terminal-4100504436-line-68)">canon</text><text class="terminal-4100504436-r3" x="146.4" y="1679.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-68)">·</text><text class="terminal-4100504436-r1" x="158.6" y="1679.2" textLength="158.6" clip-path="url(#terminal-4100504436-line-68)">&#160;54a00fe89a93</text><text class="terminal-4100504436-r1" x="1220" y="1679.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-68)"> 348 + </text><text class="terminal-4100504436-r1" x="0" y="1703.6" textLength="536.8" clip-path="url(#terminal-4100504436-line-69)">&#160;&#160;&#160;&#160;&#160;&#160;Extraction&#160;coverage&#160;0%&#160;(0/0&#160;sentences)</text><text class="terminal-4100504436-r1" x="1220" y="1703.6" textLength="12.2" clip-path="url(#terminal-4100504436-line-69)"> 349 + </text><text class="terminal-4100504436-r7" x="48.8" y="1728" textLength="12.2" clip-path="url(#terminal-4100504436-line-70)">ℹ</text><text class="terminal-4100504436-r2" x="73.2" y="1728" textLength="61" clip-path="url(#terminal-4100504436-line-70)">canon</text><text class="terminal-4100504436-r3" x="146.4" y="1728" textLength="12.2" clip-path="url(#terminal-4100504436-line-70)">·</text><text class="terminal-4100504436-r1" x="158.6" y="1728" textLength="158.6" clip-path="url(#terminal-4100504436-line-70)">&#160;09825f42242d</text><text class="terminal-4100504436-r1" x="1220" y="1728" textLength="12.2" clip-path="url(#terminal-4100504436-line-70)"> 350 + </text><text class="terminal-4100504436-r1" x="0" y="1752.4" textLength="536.8" clip-path="url(#terminal-4100504436-line-71)">&#160;&#160;&#160;&#160;&#160;&#160;Extraction&#160;coverage&#160;0%&#160;(0/0&#160;sentences)</text><text class="terminal-4100504436-r1" x="1220" y="1752.4" textLength="12.2" clip-path="url(#terminal-4100504436-line-71)"> 351 + </text><text class="terminal-4100504436-r1" x="1220" y="1776.8" textLength="12.2" clip-path="url(#terminal-4100504436-line-72)"> 352 + </text><text class="terminal-4100504436-r4" x="24.4" y="1801.2" textLength="134.2" clip-path="url(#terminal-4100504436-line-73)">12&#160;warnings</text><text class="terminal-4100504436-r1" x="158.6" y="1801.2" textLength="24.4" clip-path="url(#terminal-4100504436-line-73)">,&#160;</text><text class="terminal-4100504436-r7" x="183" y="1801.2" textLength="73.2" clip-path="url(#terminal-4100504436-line-73)">4&#160;info</text><text class="terminal-4100504436-r1" x="1220" y="1801.2" textLength="12.2" clip-path="url(#terminal-4100504436-line-73)"> 353 + </text> 354 + </g> 355 + </g> 356 + </svg>
examples/settle-up/screenshots/04-canon.png

This is a binary file and will not be displayed.

+258
examples/settle-up/screenshots/04-canon.svg
··· 1 + <svg class="rich-terminal" viewBox="0 0 1360 1270.0" xmlns="http://www.w3.org/2000/svg"> 2 + <!-- Generated with Rich https://www.textualize.io --> 3 + <style> 4 + 5 + @font-face { 6 + font-family: "Fira Code"; 7 + src: local("FiraCode-Regular"), 8 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), 9 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); 10 + font-style: normal; 11 + font-weight: 400; 12 + } 13 + @font-face { 14 + font-family: "Fira Code"; 15 + src: local("FiraCode-Bold"), 16 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), 17 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); 18 + font-style: bold; 19 + font-weight: 700; 20 + } 21 + 22 + .terminal-699299816-matrix { 23 + font-family: Fira Code, monospace; 24 + font-size: 20px; 25 + line-height: 24.4px; 26 + font-variant-east-asian: full-width; 27 + } 28 + 29 + .terminal-699299816-title { 30 + font-size: 18px; 31 + font-weight: bold; 32 + font-family: arial; 33 + } 34 + 35 + .terminal-699299816-r1 { fill: #c5c8c6;font-weight: bold } 36 + .terminal-699299816-r2 { fill: #c5c8c6 } 37 + .terminal-699299816-r3 { fill: #868887 } 38 + .terminal-699299816-r4 { fill: #98a84b;font-weight: bold } 39 + .terminal-699299816-r5 { fill: #608ab1;font-weight: bold } 40 + </style> 41 + 42 + <defs> 43 + <clipPath id="terminal-699299816-clip-terminal"> 44 + <rect x="0" y="0" width="1341.0" height="1219.0" /> 45 + </clipPath> 46 + <clipPath id="terminal-699299816-line-0"> 47 + <rect x="0" y="1.5" width="1342" height="24.65"/> 48 + </clipPath> 49 + <clipPath id="terminal-699299816-line-1"> 50 + <rect x="0" y="25.9" width="1342" height="24.65"/> 51 + </clipPath> 52 + <clipPath id="terminal-699299816-line-2"> 53 + <rect x="0" y="50.3" width="1342" height="24.65"/> 54 + </clipPath> 55 + <clipPath id="terminal-699299816-line-3"> 56 + <rect x="0" y="74.7" width="1342" height="24.65"/> 57 + </clipPath> 58 + <clipPath id="terminal-699299816-line-4"> 59 + <rect x="0" y="99.1" width="1342" height="24.65"/> 60 + </clipPath> 61 + <clipPath id="terminal-699299816-line-5"> 62 + <rect x="0" y="123.5" width="1342" height="24.65"/> 63 + </clipPath> 64 + <clipPath id="terminal-699299816-line-6"> 65 + <rect x="0" y="147.9" width="1342" height="24.65"/> 66 + </clipPath> 67 + <clipPath id="terminal-699299816-line-7"> 68 + <rect x="0" y="172.3" width="1342" height="24.65"/> 69 + </clipPath> 70 + <clipPath id="terminal-699299816-line-8"> 71 + <rect x="0" y="196.7" width="1342" height="24.65"/> 72 + </clipPath> 73 + <clipPath id="terminal-699299816-line-9"> 74 + <rect x="0" y="221.1" width="1342" height="24.65"/> 75 + </clipPath> 76 + <clipPath id="terminal-699299816-line-10"> 77 + <rect x="0" y="245.5" width="1342" height="24.65"/> 78 + </clipPath> 79 + <clipPath id="terminal-699299816-line-11"> 80 + <rect x="0" y="269.9" width="1342" height="24.65"/> 81 + </clipPath> 82 + <clipPath id="terminal-699299816-line-12"> 83 + <rect x="0" y="294.3" width="1342" height="24.65"/> 84 + </clipPath> 85 + <clipPath id="terminal-699299816-line-13"> 86 + <rect x="0" y="318.7" width="1342" height="24.65"/> 87 + </clipPath> 88 + <clipPath id="terminal-699299816-line-14"> 89 + <rect x="0" y="343.1" width="1342" height="24.65"/> 90 + </clipPath> 91 + <clipPath id="terminal-699299816-line-15"> 92 + <rect x="0" y="367.5" width="1342" height="24.65"/> 93 + </clipPath> 94 + <clipPath id="terminal-699299816-line-16"> 95 + <rect x="0" y="391.9" width="1342" height="24.65"/> 96 + </clipPath> 97 + <clipPath id="terminal-699299816-line-17"> 98 + <rect x="0" y="416.3" width="1342" height="24.65"/> 99 + </clipPath> 100 + <clipPath id="terminal-699299816-line-18"> 101 + <rect x="0" y="440.7" width="1342" height="24.65"/> 102 + </clipPath> 103 + <clipPath id="terminal-699299816-line-19"> 104 + <rect x="0" y="465.1" width="1342" height="24.65"/> 105 + </clipPath> 106 + <clipPath id="terminal-699299816-line-20"> 107 + <rect x="0" y="489.5" width="1342" height="24.65"/> 108 + </clipPath> 109 + <clipPath id="terminal-699299816-line-21"> 110 + <rect x="0" y="513.9" width="1342" height="24.65"/> 111 + </clipPath> 112 + <clipPath id="terminal-699299816-line-22"> 113 + <rect x="0" y="538.3" width="1342" height="24.65"/> 114 + </clipPath> 115 + <clipPath id="terminal-699299816-line-23"> 116 + <rect x="0" y="562.7" width="1342" height="24.65"/> 117 + </clipPath> 118 + <clipPath id="terminal-699299816-line-24"> 119 + <rect x="0" y="587.1" width="1342" height="24.65"/> 120 + </clipPath> 121 + <clipPath id="terminal-699299816-line-25"> 122 + <rect x="0" y="611.5" width="1342" height="24.65"/> 123 + </clipPath> 124 + <clipPath id="terminal-699299816-line-26"> 125 + <rect x="0" y="635.9" width="1342" height="24.65"/> 126 + </clipPath> 127 + <clipPath id="terminal-699299816-line-27"> 128 + <rect x="0" y="660.3" width="1342" height="24.65"/> 129 + </clipPath> 130 + <clipPath id="terminal-699299816-line-28"> 131 + <rect x="0" y="684.7" width="1342" height="24.65"/> 132 + </clipPath> 133 + <clipPath id="terminal-699299816-line-29"> 134 + <rect x="0" y="709.1" width="1342" height="24.65"/> 135 + </clipPath> 136 + <clipPath id="terminal-699299816-line-30"> 137 + <rect x="0" y="733.5" width="1342" height="24.65"/> 138 + </clipPath> 139 + <clipPath id="terminal-699299816-line-31"> 140 + <rect x="0" y="757.9" width="1342" height="24.65"/> 141 + </clipPath> 142 + <clipPath id="terminal-699299816-line-32"> 143 + <rect x="0" y="782.3" width="1342" height="24.65"/> 144 + </clipPath> 145 + <clipPath id="terminal-699299816-line-33"> 146 + <rect x="0" y="806.7" width="1342" height="24.65"/> 147 + </clipPath> 148 + <clipPath id="terminal-699299816-line-34"> 149 + <rect x="0" y="831.1" width="1342" height="24.65"/> 150 + </clipPath> 151 + <clipPath id="terminal-699299816-line-35"> 152 + <rect x="0" y="855.5" width="1342" height="24.65"/> 153 + </clipPath> 154 + <clipPath id="terminal-699299816-line-36"> 155 + <rect x="0" y="879.9" width="1342" height="24.65"/> 156 + </clipPath> 157 + <clipPath id="terminal-699299816-line-37"> 158 + <rect x="0" y="904.3" width="1342" height="24.65"/> 159 + </clipPath> 160 + <clipPath id="terminal-699299816-line-38"> 161 + <rect x="0" y="928.7" width="1342" height="24.65"/> 162 + </clipPath> 163 + <clipPath id="terminal-699299816-line-39"> 164 + <rect x="0" y="953.1" width="1342" height="24.65"/> 165 + </clipPath> 166 + <clipPath id="terminal-699299816-line-40"> 167 + <rect x="0" y="977.5" width="1342" height="24.65"/> 168 + </clipPath> 169 + <clipPath id="terminal-699299816-line-41"> 170 + <rect x="0" y="1001.9" width="1342" height="24.65"/> 171 + </clipPath> 172 + <clipPath id="terminal-699299816-line-42"> 173 + <rect x="0" y="1026.3" width="1342" height="24.65"/> 174 + </clipPath> 175 + <clipPath id="terminal-699299816-line-43"> 176 + <rect x="0" y="1050.7" width="1342" height="24.65"/> 177 + </clipPath> 178 + <clipPath id="terminal-699299816-line-44"> 179 + <rect x="0" y="1075.1" width="1342" height="24.65"/> 180 + </clipPath> 181 + <clipPath id="terminal-699299816-line-45"> 182 + <rect x="0" y="1099.5" width="1342" height="24.65"/> 183 + </clipPath> 184 + <clipPath id="terminal-699299816-line-46"> 185 + <rect x="0" y="1123.9" width="1342" height="24.65"/> 186 + </clipPath> 187 + <clipPath id="terminal-699299816-line-47"> 188 + <rect x="0" y="1148.3" width="1342" height="24.65"/> 189 + </clipPath> 190 + <clipPath id="terminal-699299816-line-48"> 191 + <rect x="0" y="1172.7" width="1342" height="24.65"/> 192 + </clipPath> 193 + </defs> 194 + 195 + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1358" height="1268" rx="8"/><text class="terminal-699299816-title" fill="#c5c8c6" text-anchor="middle" x="679" y="27">phoenix&#160;—&#160;04-canon</text> 196 + <g transform="translate(26,22)"> 197 + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> 198 + <circle cx="22" cy="0" r="7" fill="#febc2e"/> 199 + <circle cx="44" cy="0" r="7" fill="#28c840"/> 200 + </g> 201 + 202 + <g transform="translate(9, 41)" clip-path="url(#terminal-699299816-clip-terminal)"> 203 + 204 + <g class="terminal-699299816-matrix"> 205 + <text class="terminal-699299816-r1" x="0" y="20" textLength="207.4" clip-path="url(#terminal-699299816-line-0)">📐&#160;Canonical&#160;Graph</text><text class="terminal-699299816-r2" x="1342" y="20" textLength="12.2" clip-path="url(#terminal-699299816-line-0)"> 206 + </text><text class="terminal-699299816-r2" x="1342" y="44.4" textLength="12.2" clip-path="url(#terminal-699299816-line-1)"> 207 + </text><text class="terminal-699299816-r3" x="24.4" y="68.8" textLength="97.6" clip-path="url(#terminal-699299816-line-2)">66&#160;nodes</text><text class="terminal-699299816-r2" x="1342" y="68.8" textLength="12.2" clip-path="url(#terminal-699299816-line-2)"> 208 + </text><text class="terminal-699299816-r2" x="1342" y="93.2" textLength="12.2" clip-path="url(#terminal-699299816-line-3)"> 209 + </text><text class="terminal-699299816-r4" x="24.4" y="117.6" textLength="134.2" clip-path="url(#terminal-699299816-line-4)">REQUIREMENT</text><text class="terminal-699299816-r2" x="158.6" y="117.6" textLength="61" clip-path="url(#terminal-699299816-line-4)">&#160;(38)</text><text class="terminal-699299816-r2" x="1342" y="117.6" textLength="12.2" clip-path="url(#terminal-699299816-line-4)"> 210 + </text><text class="terminal-699299816-r3" x="48.8" y="142" textLength="97.6" clip-path="url(#terminal-699299816-line-5)">18acbfd7</text><text class="terminal-699299816-r2" x="146.4" y="142" textLength="1000.4" clip-path="url(#terminal-699299816-line-5)">&#160;The&#160;system&#160;creates&#160;a&#160;new&#160;group&#160;when&#160;receiving&#160;a&#160;POST&#160;request&#160;to&#160;/groups&#160;endpoint…</text><text class="terminal-699299816-r3" x="1146.8" y="142" textLength="122" clip-path="url(#terminal-699299816-line-5)">&#160;←&#160;8&#160;links</text><text class="terminal-699299816-r2" x="1342" y="142" textLength="12.2" clip-path="url(#terminal-699299816-line-5)"> 211 + </text><text class="terminal-699299816-r3" x="48.8" y="166.4" textLength="97.6" clip-path="url(#terminal-699299816-line-6)">a3ddb93d</text><text class="terminal-699299816-r2" x="146.4" y="166.4" textLength="1000.4" clip-path="url(#terminal-699299816-line-6)">&#160;The&#160;system&#160;removes&#160;a&#160;member&#160;from&#160;a&#160;group&#160;when&#160;the&#160;DELETE&#160;request&#160;is&#160;made&#160;to&#160;the&#160;…</text><text class="terminal-699299816-r3" x="1146.8" y="166.4" textLength="122" clip-path="url(#terminal-699299816-line-6)">&#160;←&#160;8&#160;links</text><text class="terminal-699299816-r2" x="1342" y="166.4" textLength="12.2" clip-path="url(#terminal-699299816-line-6)"> 212 + </text><text class="terminal-699299816-r3" x="48.8" y="190.8" textLength="97.6" clip-path="url(#terminal-699299816-line-7)">da331d82</text><text class="terminal-699299816-r2" x="146.4" y="190.8" textLength="1000.4" clip-path="url(#terminal-699299816-line-7)">&#160;The&#160;system&#160;deletes&#160;an&#160;expense&#160;when&#160;receiving&#160;a&#160;DELETE&#160;request&#160;to&#160;the&#160;endpoint&#160;/g…</text><text class="terminal-699299816-r2" x="1342" y="190.8" textLength="12.2" clip-path="url(#terminal-699299816-line-7)"> 213 + </text><text class="terminal-699299816-r3" x="48.8" y="215.2" textLength="97.6" clip-path="url(#terminal-699299816-line-8)">0d4cc82d</text><text class="terminal-699299816-r2" x="146.4" y="215.2" textLength="1000.4" clip-path="url(#terminal-699299816-line-8)">&#160;The&#160;system&#160;returns&#160;structured&#160;JSON&#160;errors&#160;containing&#160;a&#160;code&#160;and&#160;human-readable&#160;m…</text><text class="terminal-699299816-r3" x="1146.8" y="215.2" textLength="122" clip-path="url(#terminal-699299816-line-8)">&#160;←&#160;1&#160;links</text><text class="terminal-699299816-r2" x="1342" y="215.2" textLength="12.2" clip-path="url(#terminal-699299816-line-8)"> 214 + </text><text class="terminal-699299816-r3" x="48.8" y="239.6" textLength="97.6" clip-path="url(#terminal-699299816-line-9)">6c46bbcd</text><text class="terminal-699299816-r2" x="146.4" y="239.6" textLength="841.8" clip-path="url(#terminal-699299816-line-9)">&#160;The&#160;system&#160;returns&#160;a&#160;404&#160;error&#160;when&#160;the&#160;group&#160;identifier&#160;is&#160;invalid.</text><text class="terminal-699299816-r3" x="988.2" y="239.6" textLength="122" clip-path="url(#terminal-699299816-line-9)">&#160;←&#160;3&#160;links</text><text class="terminal-699299816-r2" x="1342" y="239.6" textLength="12.2" clip-path="url(#terminal-699299816-line-9)"> 215 + </text><text class="terminal-699299816-r3" x="48.8" y="264" textLength="97.6" clip-path="url(#terminal-699299816-line-10)">ee462524</text><text class="terminal-699299816-r2" x="146.4" y="264" textLength="1000.4" clip-path="url(#terminal-699299816-line-10)">&#160;The&#160;system&#160;returns&#160;a&#160;403&#160;error&#160;code&#160;when&#160;an&#160;invalid&#160;member&#160;who&#160;is&#160;not&#160;in&#160;the&#160;gro…</text><text class="terminal-699299816-r3" x="1146.8" y="264" textLength="122" clip-path="url(#terminal-699299816-line-10)">&#160;←&#160;8&#160;links</text><text class="terminal-699299816-r2" x="1342" y="264" textLength="12.2" clip-path="url(#terminal-699299816-line-10)"> 216 + </text><text class="terminal-699299816-r3" x="48.8" y="288.4" textLength="97.6" clip-path="url(#terminal-699299816-line-11)">08265f58</text><text class="terminal-699299816-r2" x="146.4" y="288.4" textLength="1000.4" clip-path="url(#terminal-699299816-line-11)">&#160;The&#160;system&#160;returns&#160;HTTP&#160;status&#160;code&#160;400&#160;when&#160;expense&#160;data&#160;contains&#160;negative&#160;amou…</text><text class="terminal-699299816-r3" x="1146.8" y="288.4" textLength="122" clip-path="url(#terminal-699299816-line-11)">&#160;←&#160;8&#160;links</text><text class="terminal-699299816-r2" x="1342" y="288.4" textLength="12.2" clip-path="url(#terminal-699299816-line-11)"> 217 + </text><text class="terminal-699299816-r3" x="48.8" y="312.8" textLength="97.6" clip-path="url(#terminal-699299816-line-12)">3668917d</text><text class="terminal-699299816-r2" x="146.4" y="312.8" textLength="1000.4" clip-path="url(#terminal-699299816-line-12)">&#160;The&#160;system&#160;returns&#160;HTTP&#160;status&#160;code&#160;409&#160;when&#160;a&#160;user&#160;attempts&#160;to&#160;remove&#160;a&#160;member&#160;…</text><text class="terminal-699299816-r3" x="1146.8" y="312.8" textLength="122" clip-path="url(#terminal-699299816-line-12)">&#160;←&#160;3&#160;links</text><text class="terminal-699299816-r2" x="1342" y="312.8" textLength="12.2" clip-path="url(#terminal-699299816-line-12)"> 218 + </text><text class="terminal-699299816-r3" x="48.8" y="337.2" textLength="97.6" clip-path="url(#terminal-699299816-line-13)">a67ba11b</text><text class="terminal-699299816-r2" x="146.4" y="337.2" textLength="1000.4" clip-path="url(#terminal-699299816-line-13)">&#160;The&#160;system&#160;shall&#160;format&#160;all&#160;responses&#160;using&#160;JSON&#160;with&#160;a&#160;consistent&#160;envelope&#160;stru…</text><text class="terminal-699299816-r3" x="1146.8" y="337.2" textLength="122" clip-path="url(#terminal-699299816-line-13)">&#160;←&#160;1&#160;links</text><text class="terminal-699299816-r2" x="1342" y="337.2" textLength="12.2" clip-path="url(#terminal-699299816-line-13)"> 219 + </text><text class="terminal-699299816-r3" x="48.8" y="361.6" textLength="97.6" clip-path="url(#terminal-699299816-line-14)">ac74e67a</text><text class="terminal-699299816-r2" x="146.4" y="361.6" textLength="1000.4" clip-path="url(#terminal-699299816-line-14)">&#160;The&#160;system&#160;shall&#160;support&#160;pagination&#160;on&#160;list&#160;endpoints&#160;using&#160;limit&#160;and&#160;offset&#160;que…</text><text class="terminal-699299816-r2" x="1342" y="361.6" textLength="12.2" clip-path="url(#terminal-699299816-line-14)"> 220 + </text><text class="terminal-699299816-r3" x="48.8" y="386" textLength="97.6" clip-path="url(#terminal-699299816-line-15)">ad1325fa</text><text class="terminal-699299816-r2" x="146.4" y="386" textLength="1000.4" clip-path="url(#terminal-699299816-line-15)">&#160;The&#160;system&#160;represents&#160;monetary&#160;amounts&#160;in&#160;responses&#160;as&#160;integer&#160;cents&#160;to&#160;avoid&#160;fl…</text><text class="terminal-699299816-r2" x="1342" y="386" textLength="12.2" clip-path="url(#terminal-699299816-line-15)"> 221 + </text><text class="terminal-699299816-r3" x="48.8" y="410.4" textLength="97.6" clip-path="url(#terminal-699299816-line-16)">88f49aaa</text><text class="terminal-699299816-r2" x="146.4" y="410.4" textLength="1000.4" clip-path="url(#terminal-699299816-line-16)">&#160;The&#160;system&#160;shall&#160;assign&#160;a&#160;unique&#160;expense&#160;identifier,&#160;description,&#160;amount,&#160;and&#160;da…</text><text class="terminal-699299816-r3" x="1146.8" y="410.4" textLength="122" clip-path="url(#terminal-699299816-line-16)">&#160;←&#160;8&#160;links</text><text class="terminal-699299816-r2" x="1342" y="410.4" textLength="12.2" clip-path="url(#terminal-699299816-line-16)"> 222 + </text><text class="terminal-699299816-r3" x="48.8" y="434.8" textLength="97.6" clip-path="url(#terminal-699299816-line-17)">3112f12c</text><text class="terminal-699299816-r2" x="146.4" y="434.8" textLength="1000.4" clip-path="url(#terminal-699299816-line-17)">&#160;The&#160;system&#160;shall&#160;require&#160;each&#160;expense&#160;to&#160;reference&#160;exactly&#160;one&#160;payer&#160;by&#160;member&#160;i…</text><text class="terminal-699299816-r3" x="1146.8" y="434.8" textLength="122" clip-path="url(#terminal-699299816-line-17)">&#160;←&#160;8&#160;links</text><text class="terminal-699299816-r2" x="1342" y="434.8" textLength="12.2" clip-path="url(#terminal-699299816-line-17)"> 223 + </text><text class="terminal-699299816-r3" x="48.8" y="459.2" textLength="97.6" clip-path="url(#terminal-699299816-line-18)">b1704011</text><text class="terminal-699299816-r2" x="146.4" y="459.2" textLength="1000.4" clip-path="url(#terminal-699299816-line-18)">&#160;The&#160;system&#160;shall&#160;require&#160;an&#160;expense&#160;to&#160;reference&#160;one&#160;or&#160;more&#160;participants&#160;by&#160;mem…</text><text class="terminal-699299816-r3" x="1146.8" y="459.2" textLength="122" clip-path="url(#terminal-699299816-line-18)">&#160;←&#160;8&#160;links</text><text class="terminal-699299816-r2" x="1342" y="459.2" textLength="12.2" clip-path="url(#terminal-699299816-line-18)"> 224 + </text><text class="terminal-699299816-r3" x="48.8" y="483.6" textLength="97.6" clip-path="url(#terminal-699299816-line-19)">ada76261</text><text class="terminal-699299816-r2" x="146.4" y="483.6" textLength="793" clip-path="url(#terminal-699299816-line-19)">&#160;The&#160;system&#160;shall&#160;verify&#160;that&#160;the&#160;payer&#160;is&#160;a&#160;member&#160;of&#160;the&#160;group.</text><text class="terminal-699299816-r3" x="939.4" y="483.6" textLength="122" clip-path="url(#terminal-699299816-line-19)">&#160;←&#160;8&#160;links</text><text class="terminal-699299816-r2" x="1342" y="483.6" textLength="12.2" clip-path="url(#terminal-699299816-line-19)"> 225 + </text><text class="terminal-699299816-r3" x="48.8" y="508" textLength="97.6" clip-path="url(#terminal-699299816-line-20)">c73974bd</text><text class="terminal-699299816-r2" x="146.4" y="508" textLength="878.4" clip-path="url(#terminal-699299816-line-20)">&#160;The&#160;system&#160;shall&#160;verify&#160;that&#160;all&#160;participants&#160;are&#160;members&#160;of&#160;the&#160;group.</text><text class="terminal-699299816-r3" x="1024.8" y="508" textLength="122" clip-path="url(#terminal-699299816-line-20)">&#160;←&#160;2&#160;links</text><text class="terminal-699299816-r2" x="1342" y="508" textLength="12.2" clip-path="url(#terminal-699299816-line-20)"> 226 + </text><text class="terminal-699299816-r3" x="48.8" y="532.4" textLength="97.6" clip-path="url(#terminal-699299816-line-21)">eb904a0c</text><text class="terminal-699299816-r2" x="146.4" y="532.4" textLength="744.2" clip-path="url(#terminal-699299816-line-21)">&#160;The&#160;system&#160;splits&#160;an&#160;expense&#160;by&#160;percentages&#160;that&#160;sum&#160;to&#160;100.</text><text class="terminal-699299816-r3" x="890.6" y="532.4" textLength="122" clip-path="url(#terminal-699299816-line-21)">&#160;←&#160;4&#160;links</text><text class="terminal-699299816-r2" x="1342" y="532.4" textLength="12.2" clip-path="url(#terminal-699299816-line-21)"> 227 + </text><text class="terminal-699299816-r3" x="48.8" y="556.8" textLength="97.6" clip-path="url(#terminal-699299816-line-22)">a680be04</text><text class="terminal-699299816-r2" x="146.4" y="556.8" textLength="1000.4" clip-path="url(#terminal-699299816-line-22)">&#160;The&#160;system&#160;shall&#160;display&#160;all&#160;expenses&#160;in&#160;a&#160;group&#160;in&#160;reverse&#160;chronological&#160;order&#160;…</text><text class="terminal-699299816-r3" x="1146.8" y="556.8" textLength="122" clip-path="url(#terminal-699299816-line-22)">&#160;←&#160;3&#160;links</text><text class="terminal-699299816-r2" x="1342" y="556.8" textLength="12.2" clip-path="url(#terminal-699299816-line-22)"> 228 + </text><text class="terminal-699299816-r3" x="48.8" y="581.2" textLength="97.6" clip-path="url(#terminal-699299816-line-23)">7f00ee36</text><text class="terminal-699299816-r2" x="146.4" y="581.2" textLength="890.6" clip-path="url(#terminal-699299816-line-23)">&#160;The&#160;system&#160;filters&#160;expenses&#160;by&#160;payer,&#160;by&#160;participant,&#160;and&#160;by&#160;date&#160;range.</text><text class="terminal-699299816-r3" x="1037" y="581.2" textLength="122" clip-path="url(#terminal-699299816-line-23)">&#160;←&#160;2&#160;links</text><text class="terminal-699299816-r2" x="1342" y="581.2" textLength="12.2" clip-path="url(#terminal-699299816-line-23)"> 229 + </text><text class="terminal-699299816-r3" x="48.8" y="605.6" textLength="97.6" clip-path="url(#terminal-699299816-line-24)">e9e4ab72</text><text class="terminal-699299816-r2" x="146.4" y="605.6" textLength="939.4" clip-path="url(#terminal-699299816-line-24)">&#160;The&#160;system&#160;shall&#160;record&#160;the&#160;creator&#160;and&#160;creation&#160;timestamp&#160;for&#160;each&#160;expense.</text><text class="terminal-699299816-r3" x="1085.8" y="605.6" textLength="122" clip-path="url(#terminal-699299816-line-24)">&#160;←&#160;4&#160;links</text><text class="terminal-699299816-r2" x="1342" y="605.6" textLength="12.2" clip-path="url(#terminal-699299816-line-24)"> 230 + </text><text class="terminal-699299816-r3" x="48.8" y="630" textLength="97.6" clip-path="url(#terminal-699299816-line-25)">4d5f3377</text><text class="terminal-699299816-r2" x="146.4" y="630" textLength="1000.4" clip-path="url(#terminal-699299816-line-25)">&#160;The&#160;system&#160;deletes&#160;an&#160;expense&#160;and&#160;reverses&#160;the&#160;expense&#160;effect&#160;on&#160;all&#160;member&#160;bala…</text><text class="terminal-699299816-r3" x="1146.8" y="630" textLength="122" clip-path="url(#terminal-699299816-line-25)">&#160;←&#160;5&#160;links</text><text class="terminal-699299816-r2" x="1342" y="630" textLength="12.2" clip-path="url(#terminal-699299816-line-25)"> 231 + </text><text class="terminal-699299816-r3" x="48.8" y="654.4" textLength="97.6" clip-path="url(#terminal-699299816-line-26)">191a9d5d</text><text class="terminal-699299816-r2" x="146.4" y="654.4" textLength="890.6" clip-path="url(#terminal-699299816-line-26)">&#160;The&#160;system&#160;computes&#160;each&#160;member&#x27;s&#160;balance&#160;from&#160;the&#160;full&#160;expense&#160;history.</text><text class="terminal-699299816-r3" x="1037" y="654.4" textLength="122" clip-path="url(#terminal-699299816-line-26)">&#160;←&#160;3&#160;links</text><text class="terminal-699299816-r2" x="1342" y="654.4" textLength="12.2" clip-path="url(#terminal-699299816-line-26)"> 232 + </text><text class="terminal-699299816-r3" x="48.8" y="678.8" textLength="97.6" clip-path="url(#terminal-699299816-line-27)">5d9de155</text><text class="terminal-699299816-r2" x="146.4" y="678.8" textLength="1000.4" clip-path="url(#terminal-699299816-line-27)">&#160;The&#160;system&#160;recalculates&#160;balances&#160;when&#160;a&#160;user&#160;adds&#160;an&#160;expense&#160;or&#160;when&#160;a&#160;user&#160;dele…</text><text class="terminal-699299816-r3" x="1146.8" y="678.8" textLength="122" clip-path="url(#terminal-699299816-line-27)">&#160;←&#160;2&#160;links</text><text class="terminal-699299816-r2" x="1342" y="678.8" textLength="12.2" clip-path="url(#terminal-699299816-line-27)"> 233 + </text><text class="terminal-699299816-r3" x="48.8" y="703.2" textLength="97.6" clip-path="url(#terminal-699299816-line-28)">78a546df</text><text class="terminal-699299816-r2" x="146.4" y="703.2" textLength="1000.4" clip-path="url(#terminal-699299816-line-28)">&#160;The&#160;system&#160;shall&#160;assign&#160;each&#160;group&#160;a&#160;unique&#160;group&#160;identifier,&#160;a&#160;name,&#160;and&#160;a&#160;curr…</text><text class="terminal-699299816-r3" x="1146.8" y="703.2" textLength="122" clip-path="url(#terminal-699299816-line-28)">&#160;←&#160;4&#160;links</text><text class="terminal-699299816-r2" x="1342" y="703.2" textLength="12.2" clip-path="url(#terminal-699299816-line-28)"> 234 + </text><text class="terminal-699299816-r3" x="48.8" y="727.6" textLength="97.6" clip-path="url(#terminal-699299816-line-29)">2695e9ef</text><text class="terminal-699299816-r2" x="146.4" y="727.6" textLength="817.4" clip-path="url(#terminal-699299816-line-29)">&#160;The&#160;system&#160;shall&#160;record&#160;the&#160;creation&#160;date&#160;when&#160;a&#160;group&#160;is&#160;created.</text><text class="terminal-699299816-r2" x="1342" y="727.6" textLength="12.2" clip-path="url(#terminal-699299816-line-29)"> 235 + </text><text class="terminal-699299816-r3" x="48.8" y="752" textLength="97.6" clip-path="url(#terminal-699299816-line-30)">a184f271</text><text class="terminal-699299816-r2" x="146.4" y="752" textLength="1000.4" clip-path="url(#terminal-699299816-line-30)">&#160;The&#160;system&#160;assigns&#160;a&#160;unique&#160;member&#160;identifier,&#160;a&#160;display&#160;name,&#160;and&#160;an&#160;email&#160;addr…</text><text class="terminal-699299816-r3" x="1146.8" y="752" textLength="122" clip-path="url(#terminal-699299816-line-30)">&#160;←&#160;5&#160;links</text><text class="terminal-699299816-r2" x="1342" y="752" textLength="12.2" clip-path="url(#terminal-699299816-line-30)"> 236 + </text><text class="terminal-699299816-r3" x="48.8" y="776.4" textLength="97.6" clip-path="url(#terminal-699299816-line-31)">2877dbe8</text><text class="terminal-699299816-r2" x="146.4" y="776.4" textLength="1000.4" clip-path="url(#terminal-699299816-line-31)">&#160;The&#160;system&#160;displays&#160;each&#160;member&#x27;s&#160;net&#160;balance&#160;in&#160;the&#160;group&#160;summary&#160;where&#160;positiv…</text><text class="terminal-699299816-r3" x="1146.8" y="776.4" textLength="122" clip-path="url(#terminal-699299816-line-31)">&#160;←&#160;8&#160;links</text><text class="terminal-699299816-r2" x="1342" y="776.4" textLength="12.2" clip-path="url(#terminal-699299816-line-31)"> 237 + </text><text class="terminal-699299816-r3" x="48.8" y="800.8" textLength="97.6" clip-path="url(#terminal-699299816-line-32)">3cf44cb9</text><text class="terminal-699299816-r2" x="146.4" y="800.8" textLength="1000.4" clip-path="url(#terminal-699299816-line-32)">&#160;The&#160;system&#160;displays&#160;the&#160;total&#160;number&#160;of&#160;expenses&#160;and&#160;total&#160;amount&#160;spent&#160;in&#160;the&#160;g…</text><text class="terminal-699299816-r3" x="1146.8" y="800.8" textLength="122" clip-path="url(#terminal-699299816-line-32)">&#160;←&#160;8&#160;links</text><text class="terminal-699299816-r2" x="1342" y="800.8" textLength="12.2" clip-path="url(#terminal-699299816-line-32)"> 238 + </text><text class="terminal-699299816-r3" x="48.8" y="825.2" textLength="97.6" clip-path="url(#terminal-699299816-line-33)">f3e6b35b</text><text class="terminal-699299816-r2" x="146.4" y="825.2" textLength="1000.4" clip-path="url(#terminal-699299816-line-33)">&#160;The&#160;settlement&#160;plan&#160;lists&#160;each&#160;required&#160;payment&#160;with&#160;the&#160;payer,&#160;payee,&#160;and&#160;payme…</text><text class="terminal-699299816-r3" x="1146.8" y="825.2" textLength="122" clip-path="url(#terminal-699299816-line-33)">&#160;←&#160;5&#160;links</text><text class="terminal-699299816-r2" x="1342" y="825.2" textLength="12.2" clip-path="url(#terminal-699299816-line-33)"> 239 + </text><text class="terminal-699299816-r3" x="48.8" y="849.6" textLength="97.6" clip-path="url(#terminal-699299816-line-34)">e052d31a</text><text class="terminal-699299816-r2" x="146.4" y="849.6" textLength="1000.4" clip-path="url(#terminal-699299816-line-34)">&#160;The&#160;simplified&#160;settlement&#160;produces&#160;the&#160;same&#160;net&#160;effect&#160;as&#160;paying&#160;each&#160;debt&#160;indiv…</text><text class="terminal-699299816-r3" x="1146.8" y="849.6" textLength="122" clip-path="url(#terminal-699299816-line-34)">&#160;←&#160;2&#160;links</text><text class="terminal-699299816-r2" x="1342" y="849.6" textLength="12.2" clip-path="url(#terminal-699299816-line-34)"> 240 + </text><text class="terminal-699299816-r3" x="48.8" y="874" textLength="97.6" clip-path="url(#terminal-699299816-line-35)">cb1d55ad</text><text class="terminal-699299816-r2" x="146.4" y="874" textLength="756.4" clip-path="url(#terminal-699299816-line-35)">&#160;The&#160;algorithm&#160;handles&#160;cycles&#160;by&#160;reducing&#160;cycles&#160;to&#160;net&#160;flows.</text><text class="terminal-699299816-r3" x="902.8" y="874" textLength="122" clip-path="url(#terminal-699299816-line-35)">&#160;←&#160;1&#160;links</text><text class="terminal-699299816-r2" x="1342" y="874" textLength="12.2" clip-path="url(#terminal-699299816-line-35)"> 241 + </text><text class="terminal-699299816-r3" x="48.8" y="898.4" textLength="97.6" clip-path="url(#terminal-699299816-line-36)">e657daa9</text><text class="terminal-699299816-r2" x="146.4" y="898.4" textLength="1000.4" clip-path="url(#terminal-699299816-line-36)">&#160;The&#160;system&#160;produces&#160;an&#160;empty&#160;settlement&#160;plan&#160;when&#160;a&#160;group&#160;has&#160;all&#160;balances&#160;equal…</text><text class="terminal-699299816-r3" x="1146.8" y="898.4" textLength="122" clip-path="url(#terminal-699299816-line-36)">&#160;←&#160;8&#160;links</text><text class="terminal-699299816-r2" x="1342" y="898.4" textLength="12.2" clip-path="url(#terminal-699299816-line-36)"> 242 + </text><text class="terminal-699299816-r3" x="48.8" y="922.8" textLength="97.6" clip-path="url(#terminal-699299816-line-37)">3c742c6c</text><text class="terminal-699299816-r2" x="146.4" y="922.8" textLength="976" clip-path="url(#terminal-699299816-line-37)">&#160;The&#160;system&#160;updates&#160;both&#160;members&#x27;&#160;balances&#160;when&#160;the&#160;system&#160;records&#160;a&#160;settlement.</text><text class="terminal-699299816-r3" x="1122.4" y="922.8" textLength="122" clip-path="url(#terminal-699299816-line-37)">&#160;←&#160;3&#160;links</text><text class="terminal-699299816-r2" x="1342" y="922.8" textLength="12.2" clip-path="url(#terminal-699299816-line-37)"> 243 + </text><text class="terminal-699299816-r3" x="48.8" y="947.2" textLength="97.6" clip-path="url(#terminal-699299816-line-38)">c0de3df0</text><text class="terminal-699299816-r2" x="146.4" y="947.2" textLength="1000.4" clip-path="url(#terminal-699299816-line-38)">&#160;The&#160;system&#160;rejects&#160;a&#160;settlement&#160;when&#160;the&#160;settlement&#160;amount&#160;exceeds&#160;the&#160;amount&#160;th…</text><text class="terminal-699299816-r3" x="1146.8" y="947.2" textLength="122" clip-path="url(#terminal-699299816-line-38)">&#160;←&#160;3&#160;links</text><text class="terminal-699299816-r2" x="1342" y="947.2" textLength="12.2" clip-path="url(#terminal-699299816-line-38)"> 244 + </text><text class="terminal-699299816-r3" x="48.8" y="971.6" textLength="97.6" clip-path="url(#terminal-699299816-line-39)">7e4819be</text><text class="terminal-699299816-r2" x="146.4" y="971.6" textLength="866.2" clip-path="url(#terminal-699299816-line-39)">&#160;The&#160;system&#160;tracks&#160;settlements&#160;separately&#160;from&#160;expenses&#160;in&#160;the&#160;history.</text><text class="terminal-699299816-r3" x="1012.6" y="971.6" textLength="122" clip-path="url(#terminal-699299816-line-39)">&#160;←&#160;1&#160;links</text><text class="terminal-699299816-r2" x="1342" y="971.6" textLength="12.2" clip-path="url(#terminal-699299816-line-39)"> 245 + </text><text class="terminal-699299816-r3" x="48.8" y="996" textLength="97.6" clip-path="url(#terminal-699299816-line-40)">739db6ca</text><text class="terminal-699299816-r2" x="146.4" y="996" textLength="1000.4" clip-path="url(#terminal-699299816-line-40)">&#160;The&#160;system&#160;reports&#160;whether&#160;a&#160;group&#160;has&#160;all&#160;balances&#160;at&#160;zero&#160;or&#160;has&#160;outstanding&#160;d…</text><text class="terminal-699299816-r3" x="1146.8" y="996" textLength="122" clip-path="url(#terminal-699299816-line-40)">&#160;←&#160;6&#160;links</text><text class="terminal-699299816-r2" x="1342" y="996" textLength="12.2" clip-path="url(#terminal-699299816-line-40)"> 246 + </text><text class="terminal-699299816-r3" x="48.8" y="1020.4" textLength="97.6" clip-path="url(#terminal-699299816-line-41)">c82774df</text><text class="terminal-699299816-r2" x="146.4" y="1020.4" textLength="1000.4" clip-path="url(#terminal-699299816-line-41)">&#160;The&#160;system&#160;shall&#160;display&#160;to&#160;each&#160;member&#160;the&#160;list&#160;of&#160;people&#160;the&#160;member&#160;owes,&#160;the&#160;…</text><text class="terminal-699299816-r3" x="1146.8" y="1020.4" textLength="122" clip-path="url(#terminal-699299816-line-41)">&#160;←&#160;5&#160;links</text><text class="terminal-699299816-r2" x="1342" y="1020.4" textLength="12.2" clip-path="url(#terminal-699299816-line-41)"> 247 + </text><text class="terminal-699299816-r3" x="48.8" y="1044.8" textLength="97.6" clip-path="url(#terminal-699299816-line-42)">9ec637e1</text><text class="terminal-699299816-r2" x="146.4" y="1044.8" textLength="915" clip-path="url(#terminal-699299816-line-42)">&#160;The&#160;system&#160;displays&#160;the&#160;total&#160;of&#160;all&#160;outstanding&#160;debts&#160;at&#160;the&#160;group&#160;level.</text><text class="terminal-699299816-r3" x="1061.4" y="1044.8" textLength="122" clip-path="url(#terminal-699299816-line-42)">&#160;←&#160;3&#160;links</text><text class="terminal-699299816-r2" x="1342" y="1044.8" textLength="12.2" clip-path="url(#terminal-699299816-line-42)"> 248 + </text><text class="terminal-699299816-r2" x="1342" y="1069.2" textLength="12.2" clip-path="url(#terminal-699299816-line-43)"> 249 + </text><text class="terminal-699299816-r5" x="24.4" y="1093.6" textLength="85.4" clip-path="url(#terminal-699299816-line-44)">CONTEXT</text><text class="terminal-699299816-r2" x="109.8" y="1093.6" textLength="61" clip-path="url(#terminal-699299816-line-44)">&#160;(17)</text><text class="terminal-699299816-r2" x="1342" y="1093.6" textLength="12.2" clip-path="url(#terminal-699299816-line-44)"> 250 + </text><text class="terminal-699299816-r3" x="48.8" y="1118" textLength="97.6" clip-path="url(#terminal-699299816-line-45)">466f68de</text><text class="terminal-699299816-r2" x="146.4" y="1118" textLength="854" clip-path="url(#terminal-699299816-line-45)">&#160;get&#160;/groups/:id&#160;—&#160;get&#160;group&#160;details&#160;including&#160;member&#160;list&#160;and&#160;summary</text><text class="terminal-699299816-r3" x="1000.4" y="1118" textLength="122" clip-path="url(#terminal-699299816-line-45)">&#160;←&#160;8&#160;links</text><text class="terminal-699299816-r2" x="1342" y="1118" textLength="12.2" clip-path="url(#terminal-699299816-line-45)"> 251 + </text><text class="terminal-699299816-r3" x="48.8" y="1142.4" textLength="97.6" clip-path="url(#terminal-699299816-line-46)">661e9621</text><text class="terminal-699299816-r2" x="146.4" y="1142.4" textLength="622.2" clip-path="url(#terminal-699299816-line-46)">&#160;post&#160;/groups/:id/members&#160;—&#160;add&#160;a&#160;member&#160;to&#160;a&#160;group</text><text class="terminal-699299816-r3" x="768.6" y="1142.4" textLength="122" clip-path="url(#terminal-699299816-line-46)">&#160;←&#160;8&#160;links</text><text class="terminal-699299816-r2" x="1342" y="1142.4" textLength="12.2" clip-path="url(#terminal-699299816-line-46)"> 252 + </text><text class="terminal-699299816-r3" x="48.8" y="1166.8" textLength="97.6" clip-path="url(#terminal-699299816-line-47)">0eefcc35</text><text class="terminal-699299816-r2" x="146.4" y="1166.8" textLength="1000.4" clip-path="url(#terminal-699299816-line-47)">&#160;post&#160;/groups/:id/expenses&#160;—&#160;add&#160;an&#160;expense&#160;(body:&#160;description,&#160;amount,&#160;payer,&#160;pa…</text><text class="terminal-699299816-r3" x="1146.8" y="1166.8" textLength="122" clip-path="url(#terminal-699299816-line-47)">&#160;←&#160;8&#160;links</text><text class="terminal-699299816-r2" x="1342" y="1166.8" textLength="12.2" clip-path="url(#terminal-699299816-line-47)"> 253 + </text><text class="terminal-699299816-r3" x="48.8" y="1191.2" textLength="97.6" clip-path="url(#terminal-699299816-line-48)">b2da4cb7</text><text class="terminal-699299816-r2" x="146.4" y="1191.2" textLength="1000.4" clip-path="url(#terminal-699299816-line-48)">&#160;get&#160;/groups/:id/expenses&#160;—&#160;list&#160;expenses&#160;with&#160;optional&#160;filters&#160;(payer,&#160;participa…</text><text class="terminal-699299816-r3" x="1146.8" y="1191.2" textLength="122" clip-path="url(#terminal-699299816-line-48)">&#160;←&#160;4&#160;links</text><text class="terminal-699299816-r2" x="1342" y="1191.2" textLength="12.2" clip-path="url(#terminal-699299816-line-48)"> 254 + </text><text class="terminal-699299816-r3" x="48.8" y="1215.6" textLength="97.6" clip-path="url(#terminal-699299816-line-49)">ad9807e6</text><text class="terminal-699299816-r2" x="146.4" y="1215.6" textLength="768.6" clip-path="url(#terminal-699299816-line-49)">&#160;get&#160;/groups/:id/balances&#160;—&#160;get&#160;all&#160;member&#160;balances&#160;for&#160;a&#160;group</text><text class="terminal-699299816-r3" x="915" y="1215.6" textLength="122" clip-path="url(#terminal-699299816-line-49)">&#160;←&#160;8&#160;links</text><text class="terminal-699299816-r2" x="1342" y="1215.6" textLength="12.2" clip-path="url(#terminal-699299816-line-49)"> 255 + </text> 256 + </g> 257 + </g> 258 + </svg>
examples/settle-up/screenshots/05-drift.png

This is a binary file and will not be displayed.

+120
examples/settle-up/screenshots/05-drift.svg
··· 1 + <svg class="rich-terminal" viewBox="0 0 1238 440.4" xmlns="http://www.w3.org/2000/svg"> 2 + <!-- Generated with Rich https://www.textualize.io --> 3 + <style> 4 + 5 + @font-face { 6 + font-family: "Fira Code"; 7 + src: local("FiraCode-Regular"), 8 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), 9 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); 10 + font-style: normal; 11 + font-weight: 400; 12 + } 13 + @font-face { 14 + font-family: "Fira Code"; 15 + src: local("FiraCode-Bold"), 16 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), 17 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); 18 + font-style: bold; 19 + font-weight: 700; 20 + } 21 + 22 + .terminal-570366777-matrix { 23 + font-family: Fira Code, monospace; 24 + font-size: 20px; 25 + line-height: 24.4px; 26 + font-variant-east-asian: full-width; 27 + } 28 + 29 + .terminal-570366777-title { 30 + font-size: 18px; 31 + font-weight: bold; 32 + font-family: arial; 33 + } 34 + 35 + .terminal-570366777-r1 { fill: #c5c8c6;font-weight: bold } 36 + .terminal-570366777-r2 { fill: #c5c8c6 } 37 + .terminal-570366777-r3 { fill: #98a84b } 38 + </style> 39 + 40 + <defs> 41 + <clipPath id="terminal-570366777-clip-terminal"> 42 + <rect x="0" y="0" width="1219.0" height="389.4" /> 43 + </clipPath> 44 + <clipPath id="terminal-570366777-line-0"> 45 + <rect x="0" y="1.5" width="1220" height="24.65"/> 46 + </clipPath> 47 + <clipPath id="terminal-570366777-line-1"> 48 + <rect x="0" y="25.9" width="1220" height="24.65"/> 49 + </clipPath> 50 + <clipPath id="terminal-570366777-line-2"> 51 + <rect x="0" y="50.3" width="1220" height="24.65"/> 52 + </clipPath> 53 + <clipPath id="terminal-570366777-line-3"> 54 + <rect x="0" y="74.7" width="1220" height="24.65"/> 55 + </clipPath> 56 + <clipPath id="terminal-570366777-line-4"> 57 + <rect x="0" y="99.1" width="1220" height="24.65"/> 58 + </clipPath> 59 + <clipPath id="terminal-570366777-line-5"> 60 + <rect x="0" y="123.5" width="1220" height="24.65"/> 61 + </clipPath> 62 + <clipPath id="terminal-570366777-line-6"> 63 + <rect x="0" y="147.9" width="1220" height="24.65"/> 64 + </clipPath> 65 + <clipPath id="terminal-570366777-line-7"> 66 + <rect x="0" y="172.3" width="1220" height="24.65"/> 67 + </clipPath> 68 + <clipPath id="terminal-570366777-line-8"> 69 + <rect x="0" y="196.7" width="1220" height="24.65"/> 70 + </clipPath> 71 + <clipPath id="terminal-570366777-line-9"> 72 + <rect x="0" y="221.1" width="1220" height="24.65"/> 73 + </clipPath> 74 + <clipPath id="terminal-570366777-line-10"> 75 + <rect x="0" y="245.5" width="1220" height="24.65"/> 76 + </clipPath> 77 + <clipPath id="terminal-570366777-line-11"> 78 + <rect x="0" y="269.9" width="1220" height="24.65"/> 79 + </clipPath> 80 + <clipPath id="terminal-570366777-line-12"> 81 + <rect x="0" y="294.3" width="1220" height="24.65"/> 82 + </clipPath> 83 + <clipPath id="terminal-570366777-line-13"> 84 + <rect x="0" y="318.7" width="1220" height="24.65"/> 85 + </clipPath> 86 + <clipPath id="terminal-570366777-line-14"> 87 + <rect x="0" y="343.1" width="1220" height="24.65"/> 88 + </clipPath> 89 + </defs> 90 + 91 + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="438.4" rx="8"/><text class="terminal-570366777-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">phoenix&#160;—&#160;05-drift</text> 92 + <g transform="translate(26,22)"> 93 + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> 94 + <circle cx="22" cy="0" r="7" fill="#febc2e"/> 95 + <circle cx="44" cy="0" r="7" fill="#28c840"/> 96 + </g> 97 + 98 + <g transform="translate(9, 41)" clip-path="url(#terminal-570366777-clip-terminal)"> 99 + 100 + <g class="terminal-570366777-matrix"> 101 + <text class="terminal-570366777-r1" x="0" y="20" textLength="207.4" clip-path="url(#terminal-570366777-line-0)">🔍&#160;Drift&#160;Detection</text><text class="terminal-570366777-r2" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-570366777-line-0)"> 102 + </text><text class="terminal-570366777-r2" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-570366777-line-1)"> 103 + </text><text class="terminal-570366777-r3" x="24.4" y="68.8" textLength="12.2" clip-path="url(#terminal-570366777-line-2)">✔</text><text class="terminal-570366777-r2" x="36.6" y="68.8" textLength="414.8" clip-path="url(#terminal-570366777-line-2)">&#160;All&#160;12&#160;generated&#160;files&#160;are&#160;clean.</text><text class="terminal-570366777-r2" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-570366777-line-2)"> 104 + </text><text class="terminal-570366777-r2" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-570366777-line-3)"> 105 + </text><text class="terminal-570366777-r3" x="24.4" y="117.6" textLength="12.2" clip-path="url(#terminal-570366777-line-4)">✔</text><text class="terminal-570366777-r2" x="36.6" y="117.6" textLength="378.2" clip-path="url(#terminal-570366777-line-4)">&#160;src/generated/api/endpoints.ts</text><text class="terminal-570366777-r2" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-570366777-line-4)"> 106 + </text><text class="terminal-570366777-r3" x="24.4" y="142" textLength="12.2" clip-path="url(#terminal-570366777-line-5)">✔</text><text class="terminal-570366777-r2" x="36.6" y="142" textLength="439.2" clip-path="url(#terminal-570366777-line-5)">&#160;src/generated/api/error-handling.ts</text><text class="terminal-570366777-r2" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-570366777-line-5)"> 107 + </text><text class="terminal-570366777-r3" x="24.4" y="166.4" textLength="12.2" clip-path="url(#terminal-570366777-line-6)">✔</text><text class="terminal-570366777-r2" x="36.6" y="166.4" textLength="451.4" clip-path="url(#terminal-570366777-line-6)">&#160;src/generated/api/response-format.ts</text><text class="terminal-570366777-r2" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-570366777-line-6)"> 108 + </text><text class="terminal-570366777-r3" x="24.4" y="190.8" textLength="12.2" clip-path="url(#terminal-570366777-line-7)">✔</text><text class="terminal-570366777-r2" x="36.6" y="190.8" textLength="561.2" clip-path="url(#terminal-570366777-line-7)">&#160;src/generated/expenses/balance-calculation.ts</text><text class="terminal-570366777-r2" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-570366777-line-7)"> 109 + </text><text class="terminal-570366777-r3" x="24.4" y="215.2" textLength="12.2" clip-path="url(#terminal-570366777-line-8)">✔</text><text class="terminal-570366777-r2" x="36.6" y="215.2" textLength="561.2" clip-path="url(#terminal-570366777-line-8)">&#160;src/generated/expenses/creating-an-expense.ts</text><text class="terminal-570366777-r2" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-570366777-line-8)"> 110 + </text><text class="terminal-570366777-r3" x="24.4" y="239.6" textLength="12.2" clip-path="url(#terminal-570366777-line-9)">✔</text><text class="terminal-570366777-r2" x="36.6" y="239.6" textLength="512.4" clip-path="url(#terminal-570366777-line-9)">&#160;src/generated/expenses/expense-history.ts</text><text class="terminal-570366777-r2" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-570366777-line-9)"> 111 + </text><text class="terminal-570366777-r3" x="24.4" y="264" textLength="12.2" clip-path="url(#terminal-570366777-line-10)">✔</text><text class="terminal-570366777-r2" x="36.6" y="264" textLength="524.6" clip-path="url(#terminal-570366777-line-10)">&#160;src/generated/expenses/split-strategies.ts</text><text class="terminal-570366777-r2" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-570366777-line-10)"> 112 + </text><text class="terminal-570366777-r3" x="24.4" y="288.4" textLength="12.2" clip-path="url(#terminal-570366777-line-11)">✔</text><text class="terminal-570366777-r2" x="36.6" y="288.4" textLength="500.2" clip-path="url(#terminal-570366777-line-11)">&#160;src/generated/groups/group-management.ts</text><text class="terminal-570366777-r2" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-570366777-line-11)"> 113 + </text><text class="terminal-570366777-r3" x="24.4" y="312.8" textLength="12.2" clip-path="url(#terminal-570366777-line-12)">✔</text><text class="terminal-570366777-r2" x="36.6" y="312.8" textLength="463.6" clip-path="url(#terminal-570366777-line-12)">&#160;src/generated/groups/group-summary.ts</text><text class="terminal-570366777-r2" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-570366777-line-12)"> 114 + </text><text class="terminal-570366777-r3" x="24.4" y="337.2" textLength="12.2" clip-path="url(#terminal-570366777-line-13)">✔</text><text class="terminal-570366777-r2" x="36.6" y="337.2" textLength="597.8" clip-path="url(#terminal-570366777-line-13)">&#160;src/generated/settlements/debt-simplification.ts</text><text class="terminal-570366777-r2" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-570366777-line-13)"> 115 + </text><text class="terminal-570366777-r3" x="24.4" y="361.6" textLength="12.2" clip-path="url(#terminal-570366777-line-14)">✔</text><text class="terminal-570366777-r2" x="36.6" y="361.6" textLength="622.2" clip-path="url(#terminal-570366777-line-14)">&#160;src/generated/settlements/recording-settlements.ts</text><text class="terminal-570366777-r2" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-570366777-line-14)"> 116 + </text><text class="terminal-570366777-r3" x="24.4" y="386" textLength="12.2" clip-path="url(#terminal-570366777-line-15)">✔</text><text class="terminal-570366777-r2" x="36.6" y="386" textLength="573.4" clip-path="url(#terminal-570366777-line-15)">&#160;src/generated/settlements/settlement-status.ts</text><text class="terminal-570366777-r2" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-570366777-line-15)"> 117 + </text> 118 + </g> 119 + </g> 120 + </svg>
examples/settle-up/screenshots/06-audit-top.png

This is a binary file and will not be displayed.

+381
examples/settle-up/screenshots/06-audit-top.svg
··· 1 + <svg class="rich-terminal" viewBox="0 0 1421 2002.0" xmlns="http://www.w3.org/2000/svg"> 2 + <!-- Generated with Rich https://www.textualize.io --> 3 + <style> 4 + 5 + @font-face { 6 + font-family: "Fira Code"; 7 + src: local("FiraCode-Regular"), 8 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), 9 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); 10 + font-style: normal; 11 + font-weight: 400; 12 + } 13 + @font-face { 14 + font-family: "Fira Code"; 15 + src: local("FiraCode-Bold"), 16 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), 17 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); 18 + font-style: bold; 19 + font-weight: 700; 20 + } 21 + 22 + .terminal-892087028-matrix { 23 + font-family: Fira Code, monospace; 24 + font-size: 20px; 25 + line-height: 24.4px; 26 + font-variant-east-asian: full-width; 27 + } 28 + 29 + .terminal-892087028-title { 30 + font-size: 18px; 31 + font-weight: bold; 32 + font-family: arial; 33 + } 34 + 35 + .terminal-892087028-r1 { fill: #c5c8c6 } 36 + .terminal-892087028-r2 { fill: #c5c8c6;font-weight: bold } 37 + .terminal-892087028-r3 { fill: #868887 } 38 + .terminal-892087028-r4 { fill: #98a84b } 39 + .terminal-892087028-r5 { fill: #608ab1 } 40 + .terminal-892087028-r6 { fill: #d0b344 } 41 + .terminal-892087028-r7 { fill: #cc555a } 42 + .terminal-892087028-r8 { fill: #68a0b3 } 43 + </style> 44 + 45 + <defs> 46 + <clipPath id="terminal-892087028-clip-terminal"> 47 + <rect x="0" y="0" width="1402.0" height="1951.0" /> 48 + </clipPath> 49 + <clipPath id="terminal-892087028-line-0"> 50 + <rect x="0" y="1.5" width="1403" height="24.65"/> 51 + </clipPath> 52 + <clipPath id="terminal-892087028-line-1"> 53 + <rect x="0" y="25.9" width="1403" height="24.65"/> 54 + </clipPath> 55 + <clipPath id="terminal-892087028-line-2"> 56 + <rect x="0" y="50.3" width="1403" height="24.65"/> 57 + </clipPath> 58 + <clipPath id="terminal-892087028-line-3"> 59 + <rect x="0" y="74.7" width="1403" height="24.65"/> 60 + </clipPath> 61 + <clipPath id="terminal-892087028-line-4"> 62 + <rect x="0" y="99.1" width="1403" height="24.65"/> 63 + </clipPath> 64 + <clipPath id="terminal-892087028-line-5"> 65 + <rect x="0" y="123.5" width="1403" height="24.65"/> 66 + </clipPath> 67 + <clipPath id="terminal-892087028-line-6"> 68 + <rect x="0" y="147.9" width="1403" height="24.65"/> 69 + </clipPath> 70 + <clipPath id="terminal-892087028-line-7"> 71 + <rect x="0" y="172.3" width="1403" height="24.65"/> 72 + </clipPath> 73 + <clipPath id="terminal-892087028-line-8"> 74 + <rect x="0" y="196.7" width="1403" height="24.65"/> 75 + </clipPath> 76 + <clipPath id="terminal-892087028-line-9"> 77 + <rect x="0" y="221.1" width="1403" height="24.65"/> 78 + </clipPath> 79 + <clipPath id="terminal-892087028-line-10"> 80 + <rect x="0" y="245.5" width="1403" height="24.65"/> 81 + </clipPath> 82 + <clipPath id="terminal-892087028-line-11"> 83 + <rect x="0" y="269.9" width="1403" height="24.65"/> 84 + </clipPath> 85 + <clipPath id="terminal-892087028-line-12"> 86 + <rect x="0" y="294.3" width="1403" height="24.65"/> 87 + </clipPath> 88 + <clipPath id="terminal-892087028-line-13"> 89 + <rect x="0" y="318.7" width="1403" height="24.65"/> 90 + </clipPath> 91 + <clipPath id="terminal-892087028-line-14"> 92 + <rect x="0" y="343.1" width="1403" height="24.65"/> 93 + </clipPath> 94 + <clipPath id="terminal-892087028-line-15"> 95 + <rect x="0" y="367.5" width="1403" height="24.65"/> 96 + </clipPath> 97 + <clipPath id="terminal-892087028-line-16"> 98 + <rect x="0" y="391.9" width="1403" height="24.65"/> 99 + </clipPath> 100 + <clipPath id="terminal-892087028-line-17"> 101 + <rect x="0" y="416.3" width="1403" height="24.65"/> 102 + </clipPath> 103 + <clipPath id="terminal-892087028-line-18"> 104 + <rect x="0" y="440.7" width="1403" height="24.65"/> 105 + </clipPath> 106 + <clipPath id="terminal-892087028-line-19"> 107 + <rect x="0" y="465.1" width="1403" height="24.65"/> 108 + </clipPath> 109 + <clipPath id="terminal-892087028-line-20"> 110 + <rect x="0" y="489.5" width="1403" height="24.65"/> 111 + </clipPath> 112 + <clipPath id="terminal-892087028-line-21"> 113 + <rect x="0" y="513.9" width="1403" height="24.65"/> 114 + </clipPath> 115 + <clipPath id="terminal-892087028-line-22"> 116 + <rect x="0" y="538.3" width="1403" height="24.65"/> 117 + </clipPath> 118 + <clipPath id="terminal-892087028-line-23"> 119 + <rect x="0" y="562.7" width="1403" height="24.65"/> 120 + </clipPath> 121 + <clipPath id="terminal-892087028-line-24"> 122 + <rect x="0" y="587.1" width="1403" height="24.65"/> 123 + </clipPath> 124 + <clipPath id="terminal-892087028-line-25"> 125 + <rect x="0" y="611.5" width="1403" height="24.65"/> 126 + </clipPath> 127 + <clipPath id="terminal-892087028-line-26"> 128 + <rect x="0" y="635.9" width="1403" height="24.65"/> 129 + </clipPath> 130 + <clipPath id="terminal-892087028-line-27"> 131 + <rect x="0" y="660.3" width="1403" height="24.65"/> 132 + </clipPath> 133 + <clipPath id="terminal-892087028-line-28"> 134 + <rect x="0" y="684.7" width="1403" height="24.65"/> 135 + </clipPath> 136 + <clipPath id="terminal-892087028-line-29"> 137 + <rect x="0" y="709.1" width="1403" height="24.65"/> 138 + </clipPath> 139 + <clipPath id="terminal-892087028-line-30"> 140 + <rect x="0" y="733.5" width="1403" height="24.65"/> 141 + </clipPath> 142 + <clipPath id="terminal-892087028-line-31"> 143 + <rect x="0" y="757.9" width="1403" height="24.65"/> 144 + </clipPath> 145 + <clipPath id="terminal-892087028-line-32"> 146 + <rect x="0" y="782.3" width="1403" height="24.65"/> 147 + </clipPath> 148 + <clipPath id="terminal-892087028-line-33"> 149 + <rect x="0" y="806.7" width="1403" height="24.65"/> 150 + </clipPath> 151 + <clipPath id="terminal-892087028-line-34"> 152 + <rect x="0" y="831.1" width="1403" height="24.65"/> 153 + </clipPath> 154 + <clipPath id="terminal-892087028-line-35"> 155 + <rect x="0" y="855.5" width="1403" height="24.65"/> 156 + </clipPath> 157 + <clipPath id="terminal-892087028-line-36"> 158 + <rect x="0" y="879.9" width="1403" height="24.65"/> 159 + </clipPath> 160 + <clipPath id="terminal-892087028-line-37"> 161 + <rect x="0" y="904.3" width="1403" height="24.65"/> 162 + </clipPath> 163 + <clipPath id="terminal-892087028-line-38"> 164 + <rect x="0" y="928.7" width="1403" height="24.65"/> 165 + </clipPath> 166 + <clipPath id="terminal-892087028-line-39"> 167 + <rect x="0" y="953.1" width="1403" height="24.65"/> 168 + </clipPath> 169 + <clipPath id="terminal-892087028-line-40"> 170 + <rect x="0" y="977.5" width="1403" height="24.65"/> 171 + </clipPath> 172 + <clipPath id="terminal-892087028-line-41"> 173 + <rect x="0" y="1001.9" width="1403" height="24.65"/> 174 + </clipPath> 175 + <clipPath id="terminal-892087028-line-42"> 176 + <rect x="0" y="1026.3" width="1403" height="24.65"/> 177 + </clipPath> 178 + <clipPath id="terminal-892087028-line-43"> 179 + <rect x="0" y="1050.7" width="1403" height="24.65"/> 180 + </clipPath> 181 + <clipPath id="terminal-892087028-line-44"> 182 + <rect x="0" y="1075.1" width="1403" height="24.65"/> 183 + </clipPath> 184 + <clipPath id="terminal-892087028-line-45"> 185 + <rect x="0" y="1099.5" width="1403" height="24.65"/> 186 + </clipPath> 187 + <clipPath id="terminal-892087028-line-46"> 188 + <rect x="0" y="1123.9" width="1403" height="24.65"/> 189 + </clipPath> 190 + <clipPath id="terminal-892087028-line-47"> 191 + <rect x="0" y="1148.3" width="1403" height="24.65"/> 192 + </clipPath> 193 + <clipPath id="terminal-892087028-line-48"> 194 + <rect x="0" y="1172.7" width="1403" height="24.65"/> 195 + </clipPath> 196 + <clipPath id="terminal-892087028-line-49"> 197 + <rect x="0" y="1197.1" width="1403" height="24.65"/> 198 + </clipPath> 199 + <clipPath id="terminal-892087028-line-50"> 200 + <rect x="0" y="1221.5" width="1403" height="24.65"/> 201 + </clipPath> 202 + <clipPath id="terminal-892087028-line-51"> 203 + <rect x="0" y="1245.9" width="1403" height="24.65"/> 204 + </clipPath> 205 + <clipPath id="terminal-892087028-line-52"> 206 + <rect x="0" y="1270.3" width="1403" height="24.65"/> 207 + </clipPath> 208 + <clipPath id="terminal-892087028-line-53"> 209 + <rect x="0" y="1294.7" width="1403" height="24.65"/> 210 + </clipPath> 211 + <clipPath id="terminal-892087028-line-54"> 212 + <rect x="0" y="1319.1" width="1403" height="24.65"/> 213 + </clipPath> 214 + <clipPath id="terminal-892087028-line-55"> 215 + <rect x="0" y="1343.5" width="1403" height="24.65"/> 216 + </clipPath> 217 + <clipPath id="terminal-892087028-line-56"> 218 + <rect x="0" y="1367.9" width="1403" height="24.65"/> 219 + </clipPath> 220 + <clipPath id="terminal-892087028-line-57"> 221 + <rect x="0" y="1392.3" width="1403" height="24.65"/> 222 + </clipPath> 223 + <clipPath id="terminal-892087028-line-58"> 224 + <rect x="0" y="1416.7" width="1403" height="24.65"/> 225 + </clipPath> 226 + <clipPath id="terminal-892087028-line-59"> 227 + <rect x="0" y="1441.1" width="1403" height="24.65"/> 228 + </clipPath> 229 + <clipPath id="terminal-892087028-line-60"> 230 + <rect x="0" y="1465.5" width="1403" height="24.65"/> 231 + </clipPath> 232 + <clipPath id="terminal-892087028-line-61"> 233 + <rect x="0" y="1489.9" width="1403" height="24.65"/> 234 + </clipPath> 235 + <clipPath id="terminal-892087028-line-62"> 236 + <rect x="0" y="1514.3" width="1403" height="24.65"/> 237 + </clipPath> 238 + <clipPath id="terminal-892087028-line-63"> 239 + <rect x="0" y="1538.7" width="1403" height="24.65"/> 240 + </clipPath> 241 + <clipPath id="terminal-892087028-line-64"> 242 + <rect x="0" y="1563.1" width="1403" height="24.65"/> 243 + </clipPath> 244 + <clipPath id="terminal-892087028-line-65"> 245 + <rect x="0" y="1587.5" width="1403" height="24.65"/> 246 + </clipPath> 247 + <clipPath id="terminal-892087028-line-66"> 248 + <rect x="0" y="1611.9" width="1403" height="24.65"/> 249 + </clipPath> 250 + <clipPath id="terminal-892087028-line-67"> 251 + <rect x="0" y="1636.3" width="1403" height="24.65"/> 252 + </clipPath> 253 + <clipPath id="terminal-892087028-line-68"> 254 + <rect x="0" y="1660.7" width="1403" height="24.65"/> 255 + </clipPath> 256 + <clipPath id="terminal-892087028-line-69"> 257 + <rect x="0" y="1685.1" width="1403" height="24.65"/> 258 + </clipPath> 259 + <clipPath id="terminal-892087028-line-70"> 260 + <rect x="0" y="1709.5" width="1403" height="24.65"/> 261 + </clipPath> 262 + <clipPath id="terminal-892087028-line-71"> 263 + <rect x="0" y="1733.9" width="1403" height="24.65"/> 264 + </clipPath> 265 + <clipPath id="terminal-892087028-line-72"> 266 + <rect x="0" y="1758.3" width="1403" height="24.65"/> 267 + </clipPath> 268 + <clipPath id="terminal-892087028-line-73"> 269 + <rect x="0" y="1782.7" width="1403" height="24.65"/> 270 + </clipPath> 271 + <clipPath id="terminal-892087028-line-74"> 272 + <rect x="0" y="1807.1" width="1403" height="24.65"/> 273 + </clipPath> 274 + <clipPath id="terminal-892087028-line-75"> 275 + <rect x="0" y="1831.5" width="1403" height="24.65"/> 276 + </clipPath> 277 + <clipPath id="terminal-892087028-line-76"> 278 + <rect x="0" y="1855.9" width="1403" height="24.65"/> 279 + </clipPath> 280 + <clipPath id="terminal-892087028-line-77"> 281 + <rect x="0" y="1880.3" width="1403" height="24.65"/> 282 + </clipPath> 283 + <clipPath id="terminal-892087028-line-78"> 284 + <rect x="0" y="1904.7" width="1403" height="24.65"/> 285 + </clipPath> 286 + </defs> 287 + 288 + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1419" height="2000" rx="8"/><text class="terminal-892087028-title" fill="#c5c8c6" text-anchor="middle" x="709" y="27">phoenix&#160;—&#160;06-audit-top</text> 289 + <g transform="translate(26,22)"> 290 + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> 291 + <circle cx="22" cy="0" r="7" fill="#febc2e"/> 292 + <circle cx="44" cy="0" r="7" fill="#28c840"/> 293 + </g> 294 + 295 + <g transform="translate(9, 41)" clip-path="url(#terminal-892087028-clip-terminal)"> 296 + 297 + <g class="terminal-892087028-matrix"> 298 + <text class="terminal-892087028-r1" x="1403" y="20" textLength="12.2" clip-path="url(#terminal-892087028-line-0)"> 299 + </text><text class="terminal-892087028-r2" x="0" y="44.4" textLength="329.4" clip-path="url(#terminal-892087028-line-1)">🔥&#160;Phoenix&#160;Replacement&#160;Audit</text><text class="terminal-892087028-r1" x="1403" y="44.4" textLength="12.2" clip-path="url(#terminal-892087028-line-1)"> 300 + </text><text class="terminal-892087028-r3" x="0" y="68.8" textLength="1024.8" clip-path="url(#terminal-892087028-line-2)">&#160;&#160;&quot;Could&#160;I&#160;replace&#160;this&#160;implementation&#160;entirely&#160;and&#160;have&#160;its&#160;dependents&#160;not&#160;notice?&quot;</text><text class="terminal-892087028-r1" x="1403" y="68.8" textLength="12.2" clip-path="url(#terminal-892087028-line-2)"> 301 + </text><text class="terminal-892087028-r1" x="1403" y="93.2" textLength="12.2" clip-path="url(#terminal-892087028-line-3)"> 302 + </text><text class="terminal-892087028-r4" x="24.4" y="117.6" textLength="183" clip-path="url(#terminal-892087028-line-4)">●&#160;0&#160;regenerable</text><text class="terminal-892087028-r5" x="231.8" y="117.6" textLength="158.6" clip-path="url(#terminal-892087028-line-4)">◐&#160;0&#160;evaluable</text><text class="terminal-892087028-r6" x="414.8" y="117.6" textLength="170.8" clip-path="url(#terminal-892087028-line-4)">○&#160;0&#160;observable</text><text class="terminal-892087028-r7" x="610" y="117.6" textLength="134.2" clip-path="url(#terminal-892087028-line-4)">◌&#160;12&#160;opaque</text><text class="terminal-892087028-r1" x="1403" y="117.6" textLength="12.2" clip-path="url(#terminal-892087028-line-4)"> 303 + </text><text class="terminal-892087028-r1" x="1403" y="142" textLength="12.2" clip-path="url(#terminal-892087028-line-5)"> 304 + </text><text class="terminal-892087028-r7" x="24.4" y="166.4" textLength="12.2" clip-path="url(#terminal-892087028-line-6)">◌</text><text class="terminal-892087028-r2" x="48.8" y="166.4" textLength="109.8" clip-path="url(#terminal-892087028-line-6)">Endpoints</text><text class="terminal-892087028-r3" x="170.8" y="166.4" textLength="805.2" clip-path="url(#terminal-892087028-line-6)">(85e17ce4d231ac2072e788df85a14c112d87bf7358ac68b778f77687c52f0ca6)</text><text class="terminal-892087028-r1" x="976" y="166.4" textLength="36.6" clip-path="url(#terminal-892087028-line-6)">&#160;—&#160;</text><text class="terminal-892087028-r7" x="1012.6" y="166.4" textLength="73.2" clip-path="url(#terminal-892087028-line-6)">43/100</text><text class="terminal-892087028-r3" x="1098" y="166.4" textLength="73.2" clip-path="url(#terminal-892087028-line-6)">opaque</text><text class="terminal-892087028-r1" x="1403" y="166.4" textLength="12.2" clip-path="url(#terminal-892087028-line-6)"> 305 + </text><text class="terminal-892087028-r6" x="48.8" y="190.8" textLength="12.2" clip-path="url(#terminal-892087028-line-7)">⚠</text><text class="terminal-892087028-r3" x="73.2" y="190.8" textLength="207.4" clip-path="url(#terminal-892087028-line-7)">Boundary&#160;Clarity:</text><text class="terminal-892087028-r1" x="280.6" y="190.8" textLength="536.8" clip-path="url(#terminal-892087028-line-7)">&#160;Contract:&#160;1&#160;inputs,&#160;0&#160;outputs,&#160;1&#160;invariants</text><text class="terminal-892087028-r1" x="1403" y="190.8" textLength="12.2" clip-path="url(#terminal-892087028-line-7)"> 306 + </text><text class="terminal-892087028-r7" x="48.8" y="215.2" textLength="12.2" clip-path="url(#terminal-892087028-line-8)">✖</text><text class="terminal-892087028-r3" x="73.2" y="215.2" textLength="244" clip-path="url(#terminal-892087028-line-8)">Evaluation&#160;Coverage:</text><text class="terminal-892087028-r1" x="317.2" y="215.2" textLength="500.2" clip-path="url(#terminal-892087028-line-8)">&#160;0&#160;evaluations,&#160;0%&#160;canon&#160;coverage,&#160;3&#160;gaps</text><text class="terminal-892087028-r1" x="1403" y="215.2" textLength="12.2" clip-path="url(#terminal-892087028-line-8)"> 307 + </text><text class="terminal-892087028-r4" x="48.8" y="239.6" textLength="12.2" clip-path="url(#terminal-892087028-line-9)">✓</text><text class="terminal-892087028-r3" x="73.2" y="239.6" textLength="158.6" clip-path="url(#terminal-892087028-line-9)">Blast&#160;Radius:</text><text class="terminal-892087028-r1" x="231.8" y="239.6" textLength="219.6" clip-path="url(#terminal-892087028-line-9)">&#160;0&#160;dependent&#160;IU(s)</text><text class="terminal-892087028-r1" x="1403" y="239.6" textLength="12.2" clip-path="url(#terminal-892087028-line-9)"> 308 + </text><text class="terminal-892087028-r7" x="48.8" y="264" textLength="12.2" clip-path="url(#terminal-892087028-line-10)">✖</text><text class="terminal-892087028-r3" x="73.2" y="264" textLength="195.2" clip-path="url(#terminal-892087028-line-10)">Deletion&#160;Safety:</text><text class="terminal-892087028-r1" x="268.4" y="264" textLength="536.8" clip-path="url(#terminal-892087028-line-10)">&#160;Min&#160;of&#160;boundary&#160;(50),&#160;eval&#160;(0),&#160;blast&#160;(100)</text><text class="terminal-892087028-r1" x="1403" y="264" textLength="12.2" clip-path="url(#terminal-892087028-line-10)"> 309 + </text><text class="terminal-892087028-r6" x="48.8" y="288.4" textLength="12.2" clip-path="url(#terminal-892087028-line-11)">⚠</text><text class="terminal-892087028-r3" x="73.2" y="288.4" textLength="134.2" clip-path="url(#terminal-892087028-line-11)">Pace&#160;Layer:</text><text class="terminal-892087028-r1" x="207.4" y="288.4" textLength="353.8" clip-path="url(#terminal-892087028-line-11)">&#160;No&#160;pace&#160;layer&#160;classification</text><text class="terminal-892087028-r1" x="1403" y="288.4" textLength="12.2" clip-path="url(#terminal-892087028-line-11)"> 310 + </text><text class="terminal-892087028-r4" x="48.8" y="312.8" textLength="12.2" clip-path="url(#terminal-892087028-line-12)">✓</text><text class="terminal-892087028-r3" x="73.2" y="312.8" textLength="195.2" clip-path="url(#terminal-892087028-line-12)">Conceptual&#160;Mass:</text><text class="terminal-892087028-r1" x="268.4" y="312.8" textLength="317.2" clip-path="url(#terminal-892087028-line-12)">&#160;Mass:&#160;6,&#160;interactions:&#160;15</text><text class="terminal-892087028-r1" x="1403" y="312.8" textLength="12.2" clip-path="url(#terminal-892087028-line-12)"> 311 + </text><text class="terminal-892087028-r6" x="48.8" y="337.2" textLength="12.2" clip-path="url(#terminal-892087028-line-13)">⚠</text><text class="terminal-892087028-r3" x="73.2" y="337.2" textLength="231.8" clip-path="url(#terminal-892087028-line-13)">Negative&#160;Knowledge:</text><text class="terminal-892087028-r1" x="305" y="337.2" textLength="378.2" clip-path="url(#terminal-892087028-line-13)">&#160;No&#160;negative&#160;knowledge&#160;recorded</text><text class="terminal-892087028-r1" x="1403" y="337.2" textLength="12.2" clip-path="url(#terminal-892087028-line-13)"> 312 + </text><text class="terminal-892087028-r7" x="48.8" y="361.6" textLength="109.8" clip-path="url(#terminal-892087028-line-14)">Blockers:</text><text class="terminal-892087028-r1" x="1403" y="361.6" textLength="12.2" clip-path="url(#terminal-892087028-line-14)"> 313 + </text><text class="terminal-892087028-r7" x="73.2" y="386" textLength="12.2" clip-path="url(#terminal-892087028-line-15)">✖</text><text class="terminal-892087028-r1" x="85.4" y="386" textLength="488" clip-path="url(#terminal-892087028-line-15)">&#160;Endpoints&#160;has&#160;no&#160;behavioral&#160;evaluations</text><text class="terminal-892087028-r1" x="1403" y="386" textLength="12.2" clip-path="url(#terminal-892087028-line-15)"> 314 + </text><text class="terminal-892087028-r3" x="97.6" y="410.4" textLength="707.6" clip-path="url(#terminal-892087028-line-16)">→&#160;Write&#160;evaluations&#160;at&#160;the&#160;IU&#160;boundary&#160;before&#160;regenerating</text><text class="terminal-892087028-r1" x="1403" y="410.4" textLength="12.2" clip-path="url(#terminal-892087028-line-16)"> 315 + </text><text class="terminal-892087028-r6" x="73.2" y="434.8" textLength="12.2" clip-path="url(#terminal-892087028-line-17)">⚠</text><text class="terminal-892087028-r1" x="85.4" y="434.8" textLength="524.6" clip-path="url(#terminal-892087028-line-17)">&#160;Endpoints&#160;has&#160;no&#160;pace&#160;layer&#160;classification</text><text class="terminal-892087028-r1" x="1403" y="434.8" textLength="12.2" clip-path="url(#terminal-892087028-line-17)"> 316 + </text><text class="terminal-892087028-r3" x="97.6" y="459.2" textLength="878.4" clip-path="url(#terminal-892087028-line-18)">→&#160;Classify&#160;IU&#160;into&#160;a&#160;pace&#160;layer:&#160;surface,&#160;service,&#160;domain,&#160;or&#160;foundation</text><text class="terminal-892087028-r1" x="1403" y="459.2" textLength="12.2" clip-path="url(#terminal-892087028-line-18)"> 317 + </text><text class="terminal-892087028-r8" x="48.8" y="483.6" textLength="195.2" clip-path="url(#terminal-892087028-line-19)">Recommendations:</text><text class="terminal-892087028-r1" x="1403" y="483.6" textLength="12.2" clip-path="url(#terminal-892087028-line-19)"> 318 + </text><text class="terminal-892087028-r3" x="73.2" y="508" textLength="12.2" clip-path="url(#terminal-892087028-line-20)">→</text><text class="terminal-892087028-r1" x="85.4" y="508" textLength="585.6" clip-path="url(#terminal-892087028-line-20)">&#160;Address&#160;3&#160;evaluation&#160;gap(s)&#160;before&#160;regenerating</text><text class="terminal-892087028-r1" x="1403" y="508" textLength="12.2" clip-path="url(#terminal-892087028-line-20)"> 319 + </text><text class="terminal-892087028-r1" x="1403" y="532.4" textLength="12.2" clip-path="url(#terminal-892087028-line-21)"> 320 + </text><text class="terminal-892087028-r7" x="24.4" y="556.8" textLength="12.2" clip-path="url(#terminal-892087028-line-22)">◌</text><text class="terminal-892087028-r2" x="48.8" y="556.8" textLength="170.8" clip-path="url(#terminal-892087028-line-22)">Error&#160;Handling</text><text class="terminal-892087028-r3" x="231.8" y="556.8" textLength="805.2" clip-path="url(#terminal-892087028-line-22)">(5b06984fefd4888f917d13b13c21bb66490bb8c66bb61fc86a457bcc59694904)</text><text class="terminal-892087028-r1" x="1037" y="556.8" textLength="36.6" clip-path="url(#terminal-892087028-line-22)">&#160;—&#160;</text><text class="terminal-892087028-r7" x="1073.6" y="556.8" textLength="73.2" clip-path="url(#terminal-892087028-line-22)">40/100</text><text class="terminal-892087028-r3" x="1159" y="556.8" textLength="73.2" clip-path="url(#terminal-892087028-line-22)">opaque</text><text class="terminal-892087028-r1" x="1403" y="556.8" textLength="12.2" clip-path="url(#terminal-892087028-line-22)"> 321 + </text><text class="terminal-892087028-r7" x="48.8" y="581.2" textLength="12.2" clip-path="url(#terminal-892087028-line-23)">✖</text><text class="terminal-892087028-r3" x="73.2" y="581.2" textLength="207.4" clip-path="url(#terminal-892087028-line-23)">Boundary&#160;Clarity:</text><text class="terminal-892087028-r1" x="280.6" y="581.2" textLength="536.8" clip-path="url(#terminal-892087028-line-23)">&#160;Contract:&#160;1&#160;inputs,&#160;0&#160;outputs,&#160;0&#160;invariants</text><text class="terminal-892087028-r1" x="1403" y="581.2" textLength="12.2" clip-path="url(#terminal-892087028-line-23)"> 322 + </text><text class="terminal-892087028-r7" x="48.8" y="605.6" textLength="12.2" clip-path="url(#terminal-892087028-line-24)">✖</text><text class="terminal-892087028-r3" x="73.2" y="605.6" textLength="244" clip-path="url(#terminal-892087028-line-24)">Evaluation&#160;Coverage:</text><text class="terminal-892087028-r1" x="317.2" y="605.6" textLength="500.2" clip-path="url(#terminal-892087028-line-24)">&#160;0&#160;evaluations,&#160;0%&#160;canon&#160;coverage,&#160;2&#160;gaps</text><text class="terminal-892087028-r1" x="1403" y="605.6" textLength="12.2" clip-path="url(#terminal-892087028-line-24)"> 323 + </text><text class="terminal-892087028-r4" x="48.8" y="630" textLength="12.2" clip-path="url(#terminal-892087028-line-25)">✓</text><text class="terminal-892087028-r3" x="73.2" y="630" textLength="158.6" clip-path="url(#terminal-892087028-line-25)">Blast&#160;Radius:</text><text class="terminal-892087028-r1" x="231.8" y="630" textLength="219.6" clip-path="url(#terminal-892087028-line-25)">&#160;0&#160;dependent&#160;IU(s)</text><text class="terminal-892087028-r1" x="1403" y="630" textLength="12.2" clip-path="url(#terminal-892087028-line-25)"> 324 + </text><text class="terminal-892087028-r7" x="48.8" y="654.4" textLength="12.2" clip-path="url(#terminal-892087028-line-26)">✖</text><text class="terminal-892087028-r3" x="73.2" y="654.4" textLength="195.2" clip-path="url(#terminal-892087028-line-26)">Deletion&#160;Safety:</text><text class="terminal-892087028-r1" x="268.4" y="654.4" textLength="536.8" clip-path="url(#terminal-892087028-line-26)">&#160;Min&#160;of&#160;boundary&#160;(35),&#160;eval&#160;(0),&#160;blast&#160;(100)</text><text class="terminal-892087028-r1" x="1403" y="654.4" textLength="12.2" clip-path="url(#terminal-892087028-line-26)"> 325 + </text><text class="terminal-892087028-r6" x="48.8" y="678.8" textLength="12.2" clip-path="url(#terminal-892087028-line-27)">⚠</text><text class="terminal-892087028-r3" x="73.2" y="678.8" textLength="134.2" clip-path="url(#terminal-892087028-line-27)">Pace&#160;Layer:</text><text class="terminal-892087028-r1" x="207.4" y="678.8" textLength="353.8" clip-path="url(#terminal-892087028-line-27)">&#160;No&#160;pace&#160;layer&#160;classification</text><text class="terminal-892087028-r1" x="1403" y="678.8" textLength="12.2" clip-path="url(#terminal-892087028-line-27)"> 326 + </text><text class="terminal-892087028-r4" x="48.8" y="703.2" textLength="12.2" clip-path="url(#terminal-892087028-line-28)">✓</text><text class="terminal-892087028-r3" x="73.2" y="703.2" textLength="195.2" clip-path="url(#terminal-892087028-line-28)">Conceptual&#160;Mass:</text><text class="terminal-892087028-r1" x="268.4" y="703.2" textLength="317.2" clip-path="url(#terminal-892087028-line-28)">&#160;Mass:&#160;6,&#160;interactions:&#160;15</text><text class="terminal-892087028-r1" x="1403" y="703.2" textLength="12.2" clip-path="url(#terminal-892087028-line-28)"> 327 + </text><text class="terminal-892087028-r6" x="48.8" y="727.6" textLength="12.2" clip-path="url(#terminal-892087028-line-29)">⚠</text><text class="terminal-892087028-r3" x="73.2" y="727.6" textLength="231.8" clip-path="url(#terminal-892087028-line-29)">Negative&#160;Knowledge:</text><text class="terminal-892087028-r1" x="305" y="727.6" textLength="378.2" clip-path="url(#terminal-892087028-line-29)">&#160;No&#160;negative&#160;knowledge&#160;recorded</text><text class="terminal-892087028-r1" x="1403" y="727.6" textLength="12.2" clip-path="url(#terminal-892087028-line-29)"> 328 + </text><text class="terminal-892087028-r7" x="48.8" y="752" textLength="109.8" clip-path="url(#terminal-892087028-line-30)">Blockers:</text><text class="terminal-892087028-r1" x="1403" y="752" textLength="12.2" clip-path="url(#terminal-892087028-line-30)"> 329 + </text><text class="terminal-892087028-r7" x="73.2" y="776.4" textLength="12.2" clip-path="url(#terminal-892087028-line-31)">✖</text><text class="terminal-892087028-r1" x="85.4" y="776.4" textLength="732" clip-path="url(#terminal-892087028-line-31)">&#160;Error&#160;Handling&#160;has&#160;weak&#160;boundary&#160;definition&#160;(score:&#160;35/100)</text><text class="terminal-892087028-r1" x="1403" y="776.4" textLength="12.2" clip-path="url(#terminal-892087028-line-31)"> 330 + </text><text class="terminal-892087028-r3" x="97.6" y="800.8" textLength="805.2" clip-path="url(#terminal-892087028-line-32)">→&#160;Define&#160;explicit&#160;inputs,&#160;outputs,&#160;invariants,&#160;and&#160;boundary&#160;policy</text><text class="terminal-892087028-r1" x="1403" y="800.8" textLength="12.2" clip-path="url(#terminal-892087028-line-32)"> 331 + </text><text class="terminal-892087028-r7" x="73.2" y="825.2" textLength="12.2" clip-path="url(#terminal-892087028-line-33)">✖</text><text class="terminal-892087028-r1" x="85.4" y="825.2" textLength="549" clip-path="url(#terminal-892087028-line-33)">&#160;Error&#160;Handling&#160;has&#160;no&#160;behavioral&#160;evaluations</text><text class="terminal-892087028-r1" x="1403" y="825.2" textLength="12.2" clip-path="url(#terminal-892087028-line-33)"> 332 + </text><text class="terminal-892087028-r3" x="97.6" y="849.6" textLength="707.6" clip-path="url(#terminal-892087028-line-34)">→&#160;Write&#160;evaluations&#160;at&#160;the&#160;IU&#160;boundary&#160;before&#160;regenerating</text><text class="terminal-892087028-r1" x="1403" y="849.6" textLength="12.2" clip-path="url(#terminal-892087028-line-34)"> 333 + </text><text class="terminal-892087028-r6" x="73.2" y="874" textLength="12.2" clip-path="url(#terminal-892087028-line-35)">⚠</text><text class="terminal-892087028-r1" x="85.4" y="874" textLength="585.6" clip-path="url(#terminal-892087028-line-35)">&#160;Error&#160;Handling&#160;has&#160;no&#160;pace&#160;layer&#160;classification</text><text class="terminal-892087028-r1" x="1403" y="874" textLength="12.2" clip-path="url(#terminal-892087028-line-35)"> 334 + </text><text class="terminal-892087028-r3" x="97.6" y="898.4" textLength="878.4" clip-path="url(#terminal-892087028-line-36)">→&#160;Classify&#160;IU&#160;into&#160;a&#160;pace&#160;layer:&#160;surface,&#160;service,&#160;domain,&#160;or&#160;foundation</text><text class="terminal-892087028-r1" x="1403" y="898.4" textLength="12.2" clip-path="url(#terminal-892087028-line-36)"> 335 + </text><text class="terminal-892087028-r8" x="48.8" y="922.8" textLength="195.2" clip-path="url(#terminal-892087028-line-37)">Recommendations:</text><text class="terminal-892087028-r1" x="1403" y="922.8" textLength="12.2" clip-path="url(#terminal-892087028-line-37)"> 336 + </text><text class="terminal-892087028-r3" x="73.2" y="947.2" textLength="12.2" clip-path="url(#terminal-892087028-line-38)">→</text><text class="terminal-892087028-r1" x="85.4" y="947.2" textLength="585.6" clip-path="url(#terminal-892087028-line-38)">&#160;Address&#160;2&#160;evaluation&#160;gap(s)&#160;before&#160;regenerating</text><text class="terminal-892087028-r1" x="1403" y="947.2" textLength="12.2" clip-path="url(#terminal-892087028-line-38)"> 337 + </text><text class="terminal-892087028-r3" x="73.2" y="971.6" textLength="12.2" clip-path="url(#terminal-892087028-line-39)">→</text><text class="terminal-892087028-r1" x="85.4" y="971.6" textLength="805.2" clip-path="url(#terminal-892087028-line-39)">&#160;Define&#160;explicit&#160;boundary&#160;contracts&#160;before&#160;attempting&#160;regeneration</text><text class="terminal-892087028-r1" x="1403" y="971.6" textLength="12.2" clip-path="url(#terminal-892087028-line-39)"> 338 + </text><text class="terminal-892087028-r1" x="1403" y="996" textLength="12.2" clip-path="url(#terminal-892087028-line-40)"> 339 + </text><text class="terminal-892087028-r7" x="24.4" y="1020.4" textLength="12.2" clip-path="url(#terminal-892087028-line-41)">◌</text><text class="terminal-892087028-r2" x="48.8" y="1020.4" textLength="183" clip-path="url(#terminal-892087028-line-41)">Response&#160;Format</text><text class="terminal-892087028-r3" x="244" y="1020.4" textLength="805.2" clip-path="url(#terminal-892087028-line-41)">(b517e8f546c99de3fc504064d309c37cafff62e07f0c0cb70a873fbc12467de4)</text><text class="terminal-892087028-r1" x="1049.2" y="1020.4" textLength="36.6" clip-path="url(#terminal-892087028-line-41)">&#160;—&#160;</text><text class="terminal-892087028-r7" x="1085.8" y="1020.4" textLength="73.2" clip-path="url(#terminal-892087028-line-41)">40/100</text><text class="terminal-892087028-r3" x="1171.2" y="1020.4" textLength="73.2" clip-path="url(#terminal-892087028-line-41)">opaque</text><text class="terminal-892087028-r1" x="1403" y="1020.4" textLength="12.2" clip-path="url(#terminal-892087028-line-41)"> 340 + </text><text class="terminal-892087028-r7" x="48.8" y="1044.8" textLength="12.2" clip-path="url(#terminal-892087028-line-42)">✖</text><text class="terminal-892087028-r3" x="73.2" y="1044.8" textLength="207.4" clip-path="url(#terminal-892087028-line-42)">Boundary&#160;Clarity:</text><text class="terminal-892087028-r1" x="280.6" y="1044.8" textLength="536.8" clip-path="url(#terminal-892087028-line-42)">&#160;Contract:&#160;1&#160;inputs,&#160;0&#160;outputs,&#160;0&#160;invariants</text><text class="terminal-892087028-r1" x="1403" y="1044.8" textLength="12.2" clip-path="url(#terminal-892087028-line-42)"> 341 + </text><text class="terminal-892087028-r7" x="48.8" y="1069.2" textLength="12.2" clip-path="url(#terminal-892087028-line-43)">✖</text><text class="terminal-892087028-r3" x="73.2" y="1069.2" textLength="244" clip-path="url(#terminal-892087028-line-43)">Evaluation&#160;Coverage:</text><text class="terminal-892087028-r1" x="317.2" y="1069.2" textLength="500.2" clip-path="url(#terminal-892087028-line-43)">&#160;0&#160;evaluations,&#160;0%&#160;canon&#160;coverage,&#160;2&#160;gaps</text><text class="terminal-892087028-r1" x="1403" y="1069.2" textLength="12.2" clip-path="url(#terminal-892087028-line-43)"> 342 + </text><text class="terminal-892087028-r4" x="48.8" y="1093.6" textLength="12.2" clip-path="url(#terminal-892087028-line-44)">✓</text><text class="terminal-892087028-r3" x="73.2" y="1093.6" textLength="158.6" clip-path="url(#terminal-892087028-line-44)">Blast&#160;Radius:</text><text class="terminal-892087028-r1" x="231.8" y="1093.6" textLength="219.6" clip-path="url(#terminal-892087028-line-44)">&#160;0&#160;dependent&#160;IU(s)</text><text class="terminal-892087028-r1" x="1403" y="1093.6" textLength="12.2" clip-path="url(#terminal-892087028-line-44)"> 343 + </text><text class="terminal-892087028-r7" x="48.8" y="1118" textLength="12.2" clip-path="url(#terminal-892087028-line-45)">✖</text><text class="terminal-892087028-r3" x="73.2" y="1118" textLength="195.2" clip-path="url(#terminal-892087028-line-45)">Deletion&#160;Safety:</text><text class="terminal-892087028-r1" x="268.4" y="1118" textLength="536.8" clip-path="url(#terminal-892087028-line-45)">&#160;Min&#160;of&#160;boundary&#160;(35),&#160;eval&#160;(0),&#160;blast&#160;(100)</text><text class="terminal-892087028-r1" x="1403" y="1118" textLength="12.2" clip-path="url(#terminal-892087028-line-45)"> 344 + </text><text class="terminal-892087028-r6" x="48.8" y="1142.4" textLength="12.2" clip-path="url(#terminal-892087028-line-46)">⚠</text><text class="terminal-892087028-r3" x="73.2" y="1142.4" textLength="134.2" clip-path="url(#terminal-892087028-line-46)">Pace&#160;Layer:</text><text class="terminal-892087028-r1" x="207.4" y="1142.4" textLength="353.8" clip-path="url(#terminal-892087028-line-46)">&#160;No&#160;pace&#160;layer&#160;classification</text><text class="terminal-892087028-r1" x="1403" y="1142.4" textLength="12.2" clip-path="url(#terminal-892087028-line-46)"> 345 + </text><text class="terminal-892087028-r4" x="48.8" y="1166.8" textLength="12.2" clip-path="url(#terminal-892087028-line-47)">✓</text><text class="terminal-892087028-r3" x="73.2" y="1166.8" textLength="195.2" clip-path="url(#terminal-892087028-line-47)">Conceptual&#160;Mass:</text><text class="terminal-892087028-r1" x="268.4" y="1166.8" textLength="305" clip-path="url(#terminal-892087028-line-47)">&#160;Mass:&#160;4,&#160;interactions:&#160;6</text><text class="terminal-892087028-r1" x="1403" y="1166.8" textLength="12.2" clip-path="url(#terminal-892087028-line-47)"> 346 + </text><text class="terminal-892087028-r6" x="48.8" y="1191.2" textLength="12.2" clip-path="url(#terminal-892087028-line-48)">⚠</text><text class="terminal-892087028-r3" x="73.2" y="1191.2" textLength="231.8" clip-path="url(#terminal-892087028-line-48)">Negative&#160;Knowledge:</text><text class="terminal-892087028-r1" x="305" y="1191.2" textLength="378.2" clip-path="url(#terminal-892087028-line-48)">&#160;No&#160;negative&#160;knowledge&#160;recorded</text><text class="terminal-892087028-r1" x="1403" y="1191.2" textLength="12.2" clip-path="url(#terminal-892087028-line-48)"> 347 + </text><text class="terminal-892087028-r7" x="48.8" y="1215.6" textLength="109.8" clip-path="url(#terminal-892087028-line-49)">Blockers:</text><text class="terminal-892087028-r1" x="1403" y="1215.6" textLength="12.2" clip-path="url(#terminal-892087028-line-49)"> 348 + </text><text class="terminal-892087028-r7" x="73.2" y="1240" textLength="12.2" clip-path="url(#terminal-892087028-line-50)">✖</text><text class="terminal-892087028-r1" x="85.4" y="1240" textLength="744.2" clip-path="url(#terminal-892087028-line-50)">&#160;Response&#160;Format&#160;has&#160;weak&#160;boundary&#160;definition&#160;(score:&#160;35/100)</text><text class="terminal-892087028-r1" x="1403" y="1240" textLength="12.2" clip-path="url(#terminal-892087028-line-50)"> 349 + </text><text class="terminal-892087028-r3" x="97.6" y="1264.4" textLength="805.2" clip-path="url(#terminal-892087028-line-51)">→&#160;Define&#160;explicit&#160;inputs,&#160;outputs,&#160;invariants,&#160;and&#160;boundary&#160;policy</text><text class="terminal-892087028-r1" x="1403" y="1264.4" textLength="12.2" clip-path="url(#terminal-892087028-line-51)"> 350 + </text><text class="terminal-892087028-r7" x="73.2" y="1288.8" textLength="12.2" clip-path="url(#terminal-892087028-line-52)">✖</text><text class="terminal-892087028-r1" x="85.4" y="1288.8" textLength="561.2" clip-path="url(#terminal-892087028-line-52)">&#160;Response&#160;Format&#160;has&#160;no&#160;behavioral&#160;evaluations</text><text class="terminal-892087028-r1" x="1403" y="1288.8" textLength="12.2" clip-path="url(#terminal-892087028-line-52)"> 351 + </text><text class="terminal-892087028-r3" x="97.6" y="1313.2" textLength="707.6" clip-path="url(#terminal-892087028-line-53)">→&#160;Write&#160;evaluations&#160;at&#160;the&#160;IU&#160;boundary&#160;before&#160;regenerating</text><text class="terminal-892087028-r1" x="1403" y="1313.2" textLength="12.2" clip-path="url(#terminal-892087028-line-53)"> 352 + </text><text class="terminal-892087028-r6" x="73.2" y="1337.6" textLength="12.2" clip-path="url(#terminal-892087028-line-54)">⚠</text><text class="terminal-892087028-r1" x="85.4" y="1337.6" textLength="597.8" clip-path="url(#terminal-892087028-line-54)">&#160;Response&#160;Format&#160;has&#160;no&#160;pace&#160;layer&#160;classification</text><text class="terminal-892087028-r1" x="1403" y="1337.6" textLength="12.2" clip-path="url(#terminal-892087028-line-54)"> 353 + </text><text class="terminal-892087028-r3" x="97.6" y="1362" textLength="878.4" clip-path="url(#terminal-892087028-line-55)">→&#160;Classify&#160;IU&#160;into&#160;a&#160;pace&#160;layer:&#160;surface,&#160;service,&#160;domain,&#160;or&#160;foundation</text><text class="terminal-892087028-r1" x="1403" y="1362" textLength="12.2" clip-path="url(#terminal-892087028-line-55)"> 354 + </text><text class="terminal-892087028-r8" x="48.8" y="1386.4" textLength="195.2" clip-path="url(#terminal-892087028-line-56)">Recommendations:</text><text class="terminal-892087028-r1" x="1403" y="1386.4" textLength="12.2" clip-path="url(#terminal-892087028-line-56)"> 355 + </text><text class="terminal-892087028-r3" x="73.2" y="1410.8" textLength="12.2" clip-path="url(#terminal-892087028-line-57)">→</text><text class="terminal-892087028-r1" x="85.4" y="1410.8" textLength="585.6" clip-path="url(#terminal-892087028-line-57)">&#160;Address&#160;2&#160;evaluation&#160;gap(s)&#160;before&#160;regenerating</text><text class="terminal-892087028-r1" x="1403" y="1410.8" textLength="12.2" clip-path="url(#terminal-892087028-line-57)"> 356 + </text><text class="terminal-892087028-r3" x="73.2" y="1435.2" textLength="12.2" clip-path="url(#terminal-892087028-line-58)">→</text><text class="terminal-892087028-r1" x="85.4" y="1435.2" textLength="805.2" clip-path="url(#terminal-892087028-line-58)">&#160;Define&#160;explicit&#160;boundary&#160;contracts&#160;before&#160;attempting&#160;regeneration</text><text class="terminal-892087028-r1" x="1403" y="1435.2" textLength="12.2" clip-path="url(#terminal-892087028-line-58)"> 357 + </text><text class="terminal-892087028-r1" x="1403" y="1459.6" textLength="12.2" clip-path="url(#terminal-892087028-line-59)"> 358 + </text><text class="terminal-892087028-r7" x="24.4" y="1484" textLength="12.2" clip-path="url(#terminal-892087028-line-60)">◌</text><text class="terminal-892087028-r2" x="48.8" y="1484" textLength="231.8" clip-path="url(#terminal-892087028-line-60)">Balance&#160;Calculation</text><text class="terminal-892087028-r3" x="292.8" y="1484" textLength="805.2" clip-path="url(#terminal-892087028-line-60)">(e443e41a77db31335cdc72e416297b42a40385bbfafcb1bff58c7a51688d6927)</text><text class="terminal-892087028-r1" x="1098" y="1484" textLength="36.6" clip-path="url(#terminal-892087028-line-60)">&#160;—&#160;</text><text class="terminal-892087028-r7" x="1134.6" y="1484" textLength="73.2" clip-path="url(#terminal-892087028-line-60)">39/100</text><text class="terminal-892087028-r3" x="1220" y="1484" textLength="73.2" clip-path="url(#terminal-892087028-line-60)">opaque</text><text class="terminal-892087028-r1" x="1403" y="1484" textLength="12.2" clip-path="url(#terminal-892087028-line-60)"> 359 + </text><text class="terminal-892087028-r7" x="48.8" y="1508.4" textLength="12.2" clip-path="url(#terminal-892087028-line-61)">✖</text><text class="terminal-892087028-r3" x="73.2" y="1508.4" textLength="207.4" clip-path="url(#terminal-892087028-line-61)">Boundary&#160;Clarity:</text><text class="terminal-892087028-r1" x="280.6" y="1508.4" textLength="536.8" clip-path="url(#terminal-892087028-line-61)">&#160;Contract:&#160;0&#160;inputs,&#160;0&#160;outputs,&#160;1&#160;invariants</text><text class="terminal-892087028-r1" x="1403" y="1508.4" textLength="12.2" clip-path="url(#terminal-892087028-line-61)"> 360 + </text><text class="terminal-892087028-r7" x="48.8" y="1532.8" textLength="12.2" clip-path="url(#terminal-892087028-line-62)">✖</text><text class="terminal-892087028-r3" x="73.2" y="1532.8" textLength="244" clip-path="url(#terminal-892087028-line-62)">Evaluation&#160;Coverage:</text><text class="terminal-892087028-r1" x="317.2" y="1532.8" textLength="500.2" clip-path="url(#terminal-892087028-line-62)">&#160;0&#160;evaluations,&#160;0%&#160;canon&#160;coverage,&#160;3&#160;gaps</text><text class="terminal-892087028-r1" x="1403" y="1532.8" textLength="12.2" clip-path="url(#terminal-892087028-line-62)"> 361 + </text><text class="terminal-892087028-r4" x="48.8" y="1557.2" textLength="12.2" clip-path="url(#terminal-892087028-line-63)">✓</text><text class="terminal-892087028-r3" x="73.2" y="1557.2" textLength="158.6" clip-path="url(#terminal-892087028-line-63)">Blast&#160;Radius:</text><text class="terminal-892087028-r1" x="231.8" y="1557.2" textLength="219.6" clip-path="url(#terminal-892087028-line-63)">&#160;0&#160;dependent&#160;IU(s)</text><text class="terminal-892087028-r1" x="1403" y="1557.2" textLength="12.2" clip-path="url(#terminal-892087028-line-63)"> 362 + </text><text class="terminal-892087028-r7" x="48.8" y="1581.6" textLength="12.2" clip-path="url(#terminal-892087028-line-64)">✖</text><text class="terminal-892087028-r3" x="73.2" y="1581.6" textLength="195.2" clip-path="url(#terminal-892087028-line-64)">Deletion&#160;Safety:</text><text class="terminal-892087028-r1" x="268.4" y="1581.6" textLength="536.8" clip-path="url(#terminal-892087028-line-64)">&#160;Min&#160;of&#160;boundary&#160;(30),&#160;eval&#160;(0),&#160;blast&#160;(100)</text><text class="terminal-892087028-r1" x="1403" y="1581.6" textLength="12.2" clip-path="url(#terminal-892087028-line-64)"> 363 + </text><text class="terminal-892087028-r6" x="48.8" y="1606" textLength="12.2" clip-path="url(#terminal-892087028-line-65)">⚠</text><text class="terminal-892087028-r3" x="73.2" y="1606" textLength="134.2" clip-path="url(#terminal-892087028-line-65)">Pace&#160;Layer:</text><text class="terminal-892087028-r1" x="207.4" y="1606" textLength="353.8" clip-path="url(#terminal-892087028-line-65)">&#160;No&#160;pace&#160;layer&#160;classification</text><text class="terminal-892087028-r1" x="1403" y="1606" textLength="12.2" clip-path="url(#terminal-892087028-line-65)"> 364 + </text><text class="terminal-892087028-r4" x="48.8" y="1630.4" textLength="12.2" clip-path="url(#terminal-892087028-line-66)">✓</text><text class="terminal-892087028-r3" x="73.2" y="1630.4" textLength="195.2" clip-path="url(#terminal-892087028-line-66)">Conceptual&#160;Mass:</text><text class="terminal-892087028-r1" x="268.4" y="1630.4" textLength="305" clip-path="url(#terminal-892087028-line-66)">&#160;Mass:&#160;4,&#160;interactions:&#160;6</text><text class="terminal-892087028-r1" x="1403" y="1630.4" textLength="12.2" clip-path="url(#terminal-892087028-line-66)"> 365 + </text><text class="terminal-892087028-r6" x="48.8" y="1654.8" textLength="12.2" clip-path="url(#terminal-892087028-line-67)">⚠</text><text class="terminal-892087028-r3" x="73.2" y="1654.8" textLength="231.8" clip-path="url(#terminal-892087028-line-67)">Negative&#160;Knowledge:</text><text class="terminal-892087028-r1" x="305" y="1654.8" textLength="378.2" clip-path="url(#terminal-892087028-line-67)">&#160;No&#160;negative&#160;knowledge&#160;recorded</text><text class="terminal-892087028-r1" x="1403" y="1654.8" textLength="12.2" clip-path="url(#terminal-892087028-line-67)"> 366 + </text><text class="terminal-892087028-r7" x="48.8" y="1679.2" textLength="109.8" clip-path="url(#terminal-892087028-line-68)">Blockers:</text><text class="terminal-892087028-r1" x="1403" y="1679.2" textLength="12.2" clip-path="url(#terminal-892087028-line-68)"> 367 + </text><text class="terminal-892087028-r7" x="73.2" y="1703.6" textLength="12.2" clip-path="url(#terminal-892087028-line-69)">✖</text><text class="terminal-892087028-r1" x="85.4" y="1703.6" textLength="793" clip-path="url(#terminal-892087028-line-69)">&#160;Balance&#160;Calculation&#160;has&#160;weak&#160;boundary&#160;definition&#160;(score:&#160;30/100)</text><text class="terminal-892087028-r1" x="1403" y="1703.6" textLength="12.2" clip-path="url(#terminal-892087028-line-69)"> 368 + </text><text class="terminal-892087028-r3" x="97.6" y="1728" textLength="805.2" clip-path="url(#terminal-892087028-line-70)">→&#160;Define&#160;explicit&#160;inputs,&#160;outputs,&#160;invariants,&#160;and&#160;boundary&#160;policy</text><text class="terminal-892087028-r1" x="1403" y="1728" textLength="12.2" clip-path="url(#terminal-892087028-line-70)"> 369 + </text><text class="terminal-892087028-r7" x="73.2" y="1752.4" textLength="12.2" clip-path="url(#terminal-892087028-line-71)">✖</text><text class="terminal-892087028-r1" x="85.4" y="1752.4" textLength="610" clip-path="url(#terminal-892087028-line-71)">&#160;Balance&#160;Calculation&#160;has&#160;no&#160;behavioral&#160;evaluations</text><text class="terminal-892087028-r1" x="1403" y="1752.4" textLength="12.2" clip-path="url(#terminal-892087028-line-71)"> 370 + </text><text class="terminal-892087028-r3" x="97.6" y="1776.8" textLength="707.6" clip-path="url(#terminal-892087028-line-72)">→&#160;Write&#160;evaluations&#160;at&#160;the&#160;IU&#160;boundary&#160;before&#160;regenerating</text><text class="terminal-892087028-r1" x="1403" y="1776.8" textLength="12.2" clip-path="url(#terminal-892087028-line-72)"> 371 + </text><text class="terminal-892087028-r6" x="73.2" y="1801.2" textLength="12.2" clip-path="url(#terminal-892087028-line-73)">⚠</text><text class="terminal-892087028-r1" x="85.4" y="1801.2" textLength="646.6" clip-path="url(#terminal-892087028-line-73)">&#160;Balance&#160;Calculation&#160;has&#160;no&#160;pace&#160;layer&#160;classification</text><text class="terminal-892087028-r1" x="1403" y="1801.2" textLength="12.2" clip-path="url(#terminal-892087028-line-73)"> 372 + </text><text class="terminal-892087028-r3" x="97.6" y="1825.6" textLength="878.4" clip-path="url(#terminal-892087028-line-74)">→&#160;Classify&#160;IU&#160;into&#160;a&#160;pace&#160;layer:&#160;surface,&#160;service,&#160;domain,&#160;or&#160;foundation</text><text class="terminal-892087028-r1" x="1403" y="1825.6" textLength="12.2" clip-path="url(#terminal-892087028-line-74)"> 373 + </text><text class="terminal-892087028-r8" x="48.8" y="1850" textLength="195.2" clip-path="url(#terminal-892087028-line-75)">Recommendations:</text><text class="terminal-892087028-r1" x="1403" y="1850" textLength="12.2" clip-path="url(#terminal-892087028-line-75)"> 374 + </text><text class="terminal-892087028-r3" x="73.2" y="1874.4" textLength="12.2" clip-path="url(#terminal-892087028-line-76)">→</text><text class="terminal-892087028-r1" x="85.4" y="1874.4" textLength="585.6" clip-path="url(#terminal-892087028-line-76)">&#160;Address&#160;3&#160;evaluation&#160;gap(s)&#160;before&#160;regenerating</text><text class="terminal-892087028-r1" x="1403" y="1874.4" textLength="12.2" clip-path="url(#terminal-892087028-line-76)"> 375 + </text><text class="terminal-892087028-r3" x="73.2" y="1898.8" textLength="12.2" clip-path="url(#terminal-892087028-line-77)">→</text><text class="terminal-892087028-r1" x="85.4" y="1898.8" textLength="805.2" clip-path="url(#terminal-892087028-line-77)">&#160;Define&#160;explicit&#160;boundary&#160;contracts&#160;before&#160;attempting&#160;regeneration</text><text class="terminal-892087028-r1" x="1403" y="1898.8" textLength="12.2" clip-path="url(#terminal-892087028-line-77)"> 376 + </text><text class="terminal-892087028-r1" x="1403" y="1923.2" textLength="12.2" clip-path="url(#terminal-892087028-line-78)"> 377 + </text><text class="terminal-892087028-r7" x="24.4" y="1947.6" textLength="12.2" clip-path="url(#terminal-892087028-line-79)">◌</text><text class="terminal-892087028-r2" x="48.8" y="1947.6" textLength="231.8" clip-path="url(#terminal-892087028-line-79)">Creating&#160;an&#160;Expense</text><text class="terminal-892087028-r3" x="292.8" y="1947.6" textLength="805.2" clip-path="url(#terminal-892087028-line-79)">(60c1351d6276e69e296bd2b41f38faf4722726598340b1f380f51af67af156cf)</text><text class="terminal-892087028-r1" x="1098" y="1947.6" textLength="36.6" clip-path="url(#terminal-892087028-line-79)">&#160;—&#160;</text><text class="terminal-892087028-r7" x="1134.6" y="1947.6" textLength="73.2" clip-path="url(#terminal-892087028-line-79)">36/100</text><text class="terminal-892087028-r3" x="1220" y="1947.6" textLength="73.2" clip-path="url(#terminal-892087028-line-79)">opaque</text><text class="terminal-892087028-r1" x="1403" y="1947.6" textLength="12.2" clip-path="url(#terminal-892087028-line-79)"> 378 + </text> 379 + </g> 380 + </g> 381 + </svg>
examples/settle-up/screenshots/07-diff.png

This is a binary file and will not be displayed.

+110
examples/settle-up/screenshots/07-diff.svg
··· 1 + <svg class="rich-terminal" viewBox="0 0 1116 367.2" xmlns="http://www.w3.org/2000/svg"> 2 + <!-- Generated with Rich https://www.textualize.io --> 3 + <style> 4 + 5 + @font-face { 6 + font-family: "Fira Code"; 7 + src: local("FiraCode-Regular"), 8 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), 9 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); 10 + font-style: normal; 11 + font-weight: 400; 12 + } 13 + @font-face { 14 + font-family: "Fira Code"; 15 + src: local("FiraCode-Bold"), 16 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), 17 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); 18 + font-style: bold; 19 + font-weight: 700; 20 + } 21 + 22 + .terminal-2111018051-matrix { 23 + font-family: Fira Code, monospace; 24 + font-size: 20px; 25 + line-height: 24.4px; 26 + font-variant-east-asian: full-width; 27 + } 28 + 29 + .terminal-2111018051-title { 30 + font-size: 18px; 31 + font-weight: bold; 32 + font-family: arial; 33 + } 34 + 35 + .terminal-2111018051-r1 { fill: #c5c8c6;font-weight: bold } 36 + .terminal-2111018051-r2 { fill: #c5c8c6 } 37 + .terminal-2111018051-r3 { fill: #98a84b } 38 + .terminal-2111018051-r4 { fill: #d0b344 } 39 + .terminal-2111018051-r5 { fill: #868887 } 40 + </style> 41 + 42 + <defs> 43 + <clipPath id="terminal-2111018051-clip-terminal"> 44 + <rect x="0" y="0" width="1097.0" height="316.2" /> 45 + </clipPath> 46 + <clipPath id="terminal-2111018051-line-0"> 47 + <rect x="0" y="1.5" width="1098" height="24.65"/> 48 + </clipPath> 49 + <clipPath id="terminal-2111018051-line-1"> 50 + <rect x="0" y="25.9" width="1098" height="24.65"/> 51 + </clipPath> 52 + <clipPath id="terminal-2111018051-line-2"> 53 + <rect x="0" y="50.3" width="1098" height="24.65"/> 54 + </clipPath> 55 + <clipPath id="terminal-2111018051-line-3"> 56 + <rect x="0" y="74.7" width="1098" height="24.65"/> 57 + </clipPath> 58 + <clipPath id="terminal-2111018051-line-4"> 59 + <rect x="0" y="99.1" width="1098" height="24.65"/> 60 + </clipPath> 61 + <clipPath id="terminal-2111018051-line-5"> 62 + <rect x="0" y="123.5" width="1098" height="24.65"/> 63 + </clipPath> 64 + <clipPath id="terminal-2111018051-line-6"> 65 + <rect x="0" y="147.9" width="1098" height="24.65"/> 66 + </clipPath> 67 + <clipPath id="terminal-2111018051-line-7"> 68 + <rect x="0" y="172.3" width="1098" height="24.65"/> 69 + </clipPath> 70 + <clipPath id="terminal-2111018051-line-8"> 71 + <rect x="0" y="196.7" width="1098" height="24.65"/> 72 + </clipPath> 73 + <clipPath id="terminal-2111018051-line-9"> 74 + <rect x="0" y="221.1" width="1098" height="24.65"/> 75 + </clipPath> 76 + <clipPath id="terminal-2111018051-line-10"> 77 + <rect x="0" y="245.5" width="1098" height="24.65"/> 78 + </clipPath> 79 + <clipPath id="terminal-2111018051-line-11"> 80 + <rect x="0" y="269.9" width="1098" height="24.65"/> 81 + </clipPath> 82 + </defs> 83 + 84 + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1114" height="365.2" rx="8"/><text class="terminal-2111018051-title" fill="#c5c8c6" text-anchor="middle" x="557" y="27">phoenix&#160;—&#160;07-diff</text> 85 + <g transform="translate(26,22)"> 86 + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> 87 + <circle cx="22" cy="0" r="7" fill="#febc2e"/> 88 + <circle cx="44" cy="0" r="7" fill="#28c840"/> 89 + </g> 90 + 91 + <g transform="translate(9, 41)" clip-path="url(#terminal-2111018051-clip-terminal)"> 92 + 93 + <g class="terminal-2111018051-matrix"> 94 + <text class="terminal-2111018051-r1" x="0" y="20" textLength="158.6" clip-path="url(#terminal-2111018051-line-0)">📊&#160;Clause&#160;Diff</text><text class="terminal-2111018051-r2" x="1098" y="20" textLength="12.2" clip-path="url(#terminal-2111018051-line-0)"> 95 + </text><text class="terminal-2111018051-r2" x="1098" y="44.4" textLength="12.2" clip-path="url(#terminal-2111018051-line-1)"> 96 + </text><text class="terminal-2111018051-r1" x="24.4" y="68.8" textLength="134.2" clip-path="url(#terminal-2111018051-line-2)">spec/api.md</text><text class="terminal-2111018051-r2" x="1098" y="68.8" textLength="12.2" clip-path="url(#terminal-2111018051-line-2)"> 97 + </text><text class="terminal-2111018051-r3" x="48.8" y="93.2" textLength="12.2" clip-path="url(#terminal-2111018051-line-3)">✔</text><text class="terminal-2111018051-r2" x="61" y="93.2" textLength="280.6" clip-path="url(#terminal-2111018051-line-3)">&#160;No&#160;changes&#160;(4&#160;clauses)</text><text class="terminal-2111018051-r2" x="1098" y="93.2" textLength="12.2" clip-path="url(#terminal-2111018051-line-3)"> 98 + </text><text class="terminal-2111018051-r1" x="24.4" y="117.6" textLength="195.2" clip-path="url(#terminal-2111018051-line-4)">spec/expenses.md</text><text class="terminal-2111018051-r2" x="1098" y="117.6" textLength="12.2" clip-path="url(#terminal-2111018051-line-4)"> 99 + </text><text class="terminal-2111018051-r4" x="48.8" y="142" textLength="134.2" clip-path="url(#terminal-2111018051-line-5)">~1&#160;modified</text><text class="terminal-2111018051-r2" x="1098" y="142" textLength="12.2" clip-path="url(#terminal-2111018051-line-5)"> 100 + </text><text class="terminal-2111018051-r5" x="48.8" y="166.4" textLength="134.2" clip-path="url(#terminal-2111018051-line-6)">4&#160;unchanged</text><text class="terminal-2111018051-r2" x="1098" y="166.4" textLength="12.2" clip-path="url(#terminal-2111018051-line-6)"> 101 + </text><text class="terminal-2111018051-r4" x="73.2" y="190.8" textLength="12.2" clip-path="url(#terminal-2111018051-line-7)">~</text><text class="terminal-2111018051-r2" x="85.4" y="190.8" textLength="329.4" clip-path="url(#terminal-2111018051-line-7)">&#160;Expenses&#160;&gt;&#160;Expense&#160;History</text><text class="terminal-2111018051-r2" x="1098" y="190.8" textLength="12.2" clip-path="url(#terminal-2111018051-line-7)"> 102 + </text><text class="terminal-2111018051-r2" x="1098" y="215.2" textLength="12.2" clip-path="url(#terminal-2111018051-line-8)"> 103 + </text><text class="terminal-2111018051-r1" x="24.4" y="239.6" textLength="170.8" clip-path="url(#terminal-2111018051-line-9)">spec/groups.md</text><text class="terminal-2111018051-r2" x="1098" y="239.6" textLength="12.2" clip-path="url(#terminal-2111018051-line-9)"> 104 + </text><text class="terminal-2111018051-r3" x="48.8" y="264" textLength="12.2" clip-path="url(#terminal-2111018051-line-10)">✔</text><text class="terminal-2111018051-r2" x="61" y="264" textLength="280.6" clip-path="url(#terminal-2111018051-line-10)">&#160;No&#160;changes&#160;(4&#160;clauses)</text><text class="terminal-2111018051-r2" x="1098" y="264" textLength="12.2" clip-path="url(#terminal-2111018051-line-10)"> 105 + </text><text class="terminal-2111018051-r1" x="24.4" y="288.4" textLength="231.8" clip-path="url(#terminal-2111018051-line-11)">spec/settlements.md</text><text class="terminal-2111018051-r2" x="1098" y="288.4" textLength="12.2" clip-path="url(#terminal-2111018051-line-11)"> 106 + </text><text class="terminal-2111018051-r3" x="48.8" y="312.8" textLength="12.2" clip-path="url(#terminal-2111018051-line-12)">✔</text><text class="terminal-2111018051-r2" x="61" y="312.8" textLength="280.6" clip-path="url(#terminal-2111018051-line-12)">&#160;No&#160;changes&#160;(4&#160;clauses)</text><text class="terminal-2111018051-r2" x="1098" y="312.8" textLength="12.2" clip-path="url(#terminal-2111018051-line-12)"> 107 + </text> 108 + </g> 109 + </g> 110 + </svg>
examples/settle-up/screenshots/08-evaluate.png

This is a binary file and will not be displayed.

+209
examples/settle-up/screenshots/08-evaluate.svg
··· 1 + <svg class="rich-terminal" viewBox="0 0 1238 977.1999999999999" xmlns="http://www.w3.org/2000/svg"> 2 + <!-- Generated with Rich https://www.textualize.io --> 3 + <style> 4 + 5 + @font-face { 6 + font-family: "Fira Code"; 7 + src: local("FiraCode-Regular"), 8 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"), 9 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff"); 10 + font-style: normal; 11 + font-weight: 400; 12 + } 13 + @font-face { 14 + font-family: "Fira Code"; 15 + src: local("FiraCode-Bold"), 16 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"), 17 + url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff"); 18 + font-style: bold; 19 + font-weight: 700; 20 + } 21 + 22 + .terminal-1020805156-matrix { 23 + font-family: Fira Code, monospace; 24 + font-size: 20px; 25 + line-height: 24.4px; 26 + font-variant-east-asian: full-width; 27 + } 28 + 29 + .terminal-1020805156-title { 30 + font-size: 18px; 31 + font-weight: bold; 32 + font-family: arial; 33 + } 34 + 35 + .terminal-1020805156-r1 { fill: #c5c8c6;font-weight: bold } 36 + .terminal-1020805156-r2 { fill: #c5c8c6 } 37 + .terminal-1020805156-r3 { fill: #d0b344 } 38 + .terminal-1020805156-r4 { fill: #868887 } 39 + </style> 40 + 41 + <defs> 42 + <clipPath id="terminal-1020805156-clip-terminal"> 43 + <rect x="0" y="0" width="1219.0" height="926.1999999999999" /> 44 + </clipPath> 45 + <clipPath id="terminal-1020805156-line-0"> 46 + <rect x="0" y="1.5" width="1220" height="24.65"/> 47 + </clipPath> 48 + <clipPath id="terminal-1020805156-line-1"> 49 + <rect x="0" y="25.9" width="1220" height="24.65"/> 50 + </clipPath> 51 + <clipPath id="terminal-1020805156-line-2"> 52 + <rect x="0" y="50.3" width="1220" height="24.65"/> 53 + </clipPath> 54 + <clipPath id="terminal-1020805156-line-3"> 55 + <rect x="0" y="74.7" width="1220" height="24.65"/> 56 + </clipPath> 57 + <clipPath id="terminal-1020805156-line-4"> 58 + <rect x="0" y="99.1" width="1220" height="24.65"/> 59 + </clipPath> 60 + <clipPath id="terminal-1020805156-line-5"> 61 + <rect x="0" y="123.5" width="1220" height="24.65"/> 62 + </clipPath> 63 + <clipPath id="terminal-1020805156-line-6"> 64 + <rect x="0" y="147.9" width="1220" height="24.65"/> 65 + </clipPath> 66 + <clipPath id="terminal-1020805156-line-7"> 67 + <rect x="0" y="172.3" width="1220" height="24.65"/> 68 + </clipPath> 69 + <clipPath id="terminal-1020805156-line-8"> 70 + <rect x="0" y="196.7" width="1220" height="24.65"/> 71 + </clipPath> 72 + <clipPath id="terminal-1020805156-line-9"> 73 + <rect x="0" y="221.1" width="1220" height="24.65"/> 74 + </clipPath> 75 + <clipPath id="terminal-1020805156-line-10"> 76 + <rect x="0" y="245.5" width="1220" height="24.65"/> 77 + </clipPath> 78 + <clipPath id="terminal-1020805156-line-11"> 79 + <rect x="0" y="269.9" width="1220" height="24.65"/> 80 + </clipPath> 81 + <clipPath id="terminal-1020805156-line-12"> 82 + <rect x="0" y="294.3" width="1220" height="24.65"/> 83 + </clipPath> 84 + <clipPath id="terminal-1020805156-line-13"> 85 + <rect x="0" y="318.7" width="1220" height="24.65"/> 86 + </clipPath> 87 + <clipPath id="terminal-1020805156-line-14"> 88 + <rect x="0" y="343.1" width="1220" height="24.65"/> 89 + </clipPath> 90 + <clipPath id="terminal-1020805156-line-15"> 91 + <rect x="0" y="367.5" width="1220" height="24.65"/> 92 + </clipPath> 93 + <clipPath id="terminal-1020805156-line-16"> 94 + <rect x="0" y="391.9" width="1220" height="24.65"/> 95 + </clipPath> 96 + <clipPath id="terminal-1020805156-line-17"> 97 + <rect x="0" y="416.3" width="1220" height="24.65"/> 98 + </clipPath> 99 + <clipPath id="terminal-1020805156-line-18"> 100 + <rect x="0" y="440.7" width="1220" height="24.65"/> 101 + </clipPath> 102 + <clipPath id="terminal-1020805156-line-19"> 103 + <rect x="0" y="465.1" width="1220" height="24.65"/> 104 + </clipPath> 105 + <clipPath id="terminal-1020805156-line-20"> 106 + <rect x="0" y="489.5" width="1220" height="24.65"/> 107 + </clipPath> 108 + <clipPath id="terminal-1020805156-line-21"> 109 + <rect x="0" y="513.9" width="1220" height="24.65"/> 110 + </clipPath> 111 + <clipPath id="terminal-1020805156-line-22"> 112 + <rect x="0" y="538.3" width="1220" height="24.65"/> 113 + </clipPath> 114 + <clipPath id="terminal-1020805156-line-23"> 115 + <rect x="0" y="562.7" width="1220" height="24.65"/> 116 + </clipPath> 117 + <clipPath id="terminal-1020805156-line-24"> 118 + <rect x="0" y="587.1" width="1220" height="24.65"/> 119 + </clipPath> 120 + <clipPath id="terminal-1020805156-line-25"> 121 + <rect x="0" y="611.5" width="1220" height="24.65"/> 122 + </clipPath> 123 + <clipPath id="terminal-1020805156-line-26"> 124 + <rect x="0" y="635.9" width="1220" height="24.65"/> 125 + </clipPath> 126 + <clipPath id="terminal-1020805156-line-27"> 127 + <rect x="0" y="660.3" width="1220" height="24.65"/> 128 + </clipPath> 129 + <clipPath id="terminal-1020805156-line-28"> 130 + <rect x="0" y="684.7" width="1220" height="24.65"/> 131 + </clipPath> 132 + <clipPath id="terminal-1020805156-line-29"> 133 + <rect x="0" y="709.1" width="1220" height="24.65"/> 134 + </clipPath> 135 + <clipPath id="terminal-1020805156-line-30"> 136 + <rect x="0" y="733.5" width="1220" height="24.65"/> 137 + </clipPath> 138 + <clipPath id="terminal-1020805156-line-31"> 139 + <rect x="0" y="757.9" width="1220" height="24.65"/> 140 + </clipPath> 141 + <clipPath id="terminal-1020805156-line-32"> 142 + <rect x="0" y="782.3" width="1220" height="24.65"/> 143 + </clipPath> 144 + <clipPath id="terminal-1020805156-line-33"> 145 + <rect x="0" y="806.7" width="1220" height="24.65"/> 146 + </clipPath> 147 + <clipPath id="terminal-1020805156-line-34"> 148 + <rect x="0" y="831.1" width="1220" height="24.65"/> 149 + </clipPath> 150 + <clipPath id="terminal-1020805156-line-35"> 151 + <rect x="0" y="855.5" width="1220" height="24.65"/> 152 + </clipPath> 153 + <clipPath id="terminal-1020805156-line-36"> 154 + <rect x="0" y="879.9" width="1220" height="24.65"/> 155 + </clipPath> 156 + </defs> 157 + 158 + <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1236" height="975.2" rx="8"/><text class="terminal-1020805156-title" fill="#c5c8c6" text-anchor="middle" x="618" y="27">phoenix&#160;—&#160;08-evaluate</text> 159 + <g transform="translate(26,22)"> 160 + <circle cx="0" cy="0" r="7" fill="#ff5f57"/> 161 + <circle cx="22" cy="0" r="7" fill="#febc2e"/> 162 + <circle cx="44" cy="0" r="7" fill="#28c840"/> 163 + </g> 164 + 165 + <g transform="translate(9, 41)" clip-path="url(#terminal-1020805156-clip-terminal)"> 166 + 167 + <g class="terminal-1020805156-matrix"> 168 + <text class="terminal-1020805156-r1" x="0" y="20" textLength="231.8" clip-path="url(#terminal-1020805156-line-0)">📋&#160;Policy&#160;Evaluation</text><text class="terminal-1020805156-r2" x="1220" y="20" textLength="12.2" clip-path="url(#terminal-1020805156-line-0)"> 169 + </text><text class="terminal-1020805156-r2" x="1220" y="44.4" textLength="12.2" clip-path="url(#terminal-1020805156-line-1)"> 170 + </text><text class="terminal-1020805156-r3" x="24.4" y="68.8" textLength="122" clip-path="url(#terminal-1020805156-line-2)">INCOMPLETE</text><text class="terminal-1020805156-r1" x="158.6" y="68.8" textLength="109.8" clip-path="url(#terminal-1020805156-line-2)">Endpoints</text><text class="terminal-1020805156-r4" x="280.6" y="68.8" textLength="73.2" clip-path="url(#terminal-1020805156-line-2)">(high)</text><text class="terminal-1020805156-r2" x="1220" y="68.8" textLength="12.2" clip-path="url(#terminal-1020805156-line-2)"> 171 + </text><text class="terminal-1020805156-r3" x="48.8" y="93.2" textLength="12.2" clip-path="url(#terminal-1020805156-line-3)">○</text><text class="terminal-1020805156-r2" x="61" y="93.2" textLength="1110.2" clip-path="url(#terminal-1020805156-line-3)">&#160;Missing:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;static_analysis</text><text class="terminal-1020805156-r2" x="1220" y="93.2" textLength="12.2" clip-path="url(#terminal-1020805156-line-3)"> 172 + </text><text class="terminal-1020805156-r2" x="1220" y="117.6" textLength="12.2" clip-path="url(#terminal-1020805156-line-4)"> 173 + </text><text class="terminal-1020805156-r3" x="24.4" y="142" textLength="122" clip-path="url(#terminal-1020805156-line-5)">INCOMPLETE</text><text class="terminal-1020805156-r1" x="158.6" y="142" textLength="170.8" clip-path="url(#terminal-1020805156-line-5)">Error&#160;Handling</text><text class="terminal-1020805156-r4" x="341.6" y="142" textLength="97.6" clip-path="url(#terminal-1020805156-line-5)">(medium)</text><text class="terminal-1020805156-r2" x="1220" y="142" textLength="12.2" clip-path="url(#terminal-1020805156-line-5)"> 174 + </text><text class="terminal-1020805156-r3" x="48.8" y="166.4" textLength="12.2" clip-path="url(#terminal-1020805156-line-6)">○</text><text class="terminal-1020805156-r2" x="61" y="166.4" textLength="707.6" clip-path="url(#terminal-1020805156-line-6)">&#160;Missing:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests</text><text class="terminal-1020805156-r2" x="1220" y="166.4" textLength="12.2" clip-path="url(#terminal-1020805156-line-6)"> 175 + </text><text class="terminal-1020805156-r2" x="1220" y="190.8" textLength="12.2" clip-path="url(#terminal-1020805156-line-7)"> 176 + </text><text class="terminal-1020805156-r3" x="24.4" y="215.2" textLength="122" clip-path="url(#terminal-1020805156-line-8)">INCOMPLETE</text><text class="terminal-1020805156-r1" x="158.6" y="215.2" textLength="183" clip-path="url(#terminal-1020805156-line-8)">Response&#160;Format</text><text class="terminal-1020805156-r4" x="353.8" y="215.2" textLength="61" clip-path="url(#terminal-1020805156-line-8)">(low)</text><text class="terminal-1020805156-r2" x="1220" y="215.2" textLength="12.2" clip-path="url(#terminal-1020805156-line-8)"> 177 + </text><text class="terminal-1020805156-r3" x="48.8" y="239.6" textLength="12.2" clip-path="url(#terminal-1020805156-line-9)">○</text><text class="terminal-1020805156-r2" x="61" y="239.6" textLength="561.2" clip-path="url(#terminal-1020805156-line-9)">&#160;Missing:&#160;typecheck,&#160;lint,&#160;boundary_validation</text><text class="terminal-1020805156-r2" x="1220" y="239.6" textLength="12.2" clip-path="url(#terminal-1020805156-line-9)"> 178 + </text><text class="terminal-1020805156-r2" x="1220" y="264" textLength="12.2" clip-path="url(#terminal-1020805156-line-10)"> 179 + </text><text class="terminal-1020805156-r3" x="24.4" y="288.4" textLength="122" clip-path="url(#terminal-1020805156-line-11)">INCOMPLETE</text><text class="terminal-1020805156-r1" x="158.6" y="288.4" textLength="231.8" clip-path="url(#terminal-1020805156-line-11)">Balance&#160;Calculation</text><text class="terminal-1020805156-r4" x="402.6" y="288.4" textLength="73.2" clip-path="url(#terminal-1020805156-line-11)">(high)</text><text class="terminal-1020805156-r2" x="1220" y="288.4" textLength="12.2" clip-path="url(#terminal-1020805156-line-11)"> 180 + </text><text class="terminal-1020805156-r3" x="48.8" y="312.8" textLength="12.2" clip-path="url(#terminal-1020805156-line-12)">○</text><text class="terminal-1020805156-r2" x="61" y="312.8" textLength="1110.2" clip-path="url(#terminal-1020805156-line-12)">&#160;Missing:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;static_analysis</text><text class="terminal-1020805156-r2" x="1220" y="312.8" textLength="12.2" clip-path="url(#terminal-1020805156-line-12)"> 181 + </text><text class="terminal-1020805156-r2" x="1220" y="337.2" textLength="12.2" clip-path="url(#terminal-1020805156-line-13)"> 182 + </text><text class="terminal-1020805156-r3" x="24.4" y="361.6" textLength="122" clip-path="url(#terminal-1020805156-line-14)">INCOMPLETE</text><text class="terminal-1020805156-r1" x="158.6" y="361.6" textLength="231.8" clip-path="url(#terminal-1020805156-line-14)">Creating&#160;an&#160;Expense</text><text class="terminal-1020805156-r4" x="402.6" y="361.6" textLength="73.2" clip-path="url(#terminal-1020805156-line-14)">(high)</text><text class="terminal-1020805156-r2" x="1220" y="361.6" textLength="12.2" clip-path="url(#terminal-1020805156-line-14)"> 183 + </text><text class="terminal-1020805156-r3" x="48.8" y="386" textLength="12.2" clip-path="url(#terminal-1020805156-line-15)">○</text><text class="terminal-1020805156-r2" x="61" y="386" textLength="1110.2" clip-path="url(#terminal-1020805156-line-15)">&#160;Missing:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;static_analysis</text><text class="terminal-1020805156-r2" x="1220" y="386" textLength="12.2" clip-path="url(#terminal-1020805156-line-15)"> 184 + </text><text class="terminal-1020805156-r2" x="1220" y="410.4" textLength="12.2" clip-path="url(#terminal-1020805156-line-16)"> 185 + </text><text class="terminal-1020805156-r3" x="24.4" y="434.8" textLength="122" clip-path="url(#terminal-1020805156-line-17)">INCOMPLETE</text><text class="terminal-1020805156-r1" x="158.6" y="434.8" textLength="183" clip-path="url(#terminal-1020805156-line-17)">Expense&#160;History</text><text class="terminal-1020805156-r4" x="353.8" y="434.8" textLength="97.6" clip-path="url(#terminal-1020805156-line-17)">(medium)</text><text class="terminal-1020805156-r2" x="1220" y="434.8" textLength="12.2" clip-path="url(#terminal-1020805156-line-17)"> 186 + </text><text class="terminal-1020805156-r3" x="48.8" y="459.2" textLength="12.2" clip-path="url(#terminal-1020805156-line-18)">○</text><text class="terminal-1020805156-r2" x="61" y="459.2" textLength="707.6" clip-path="url(#terminal-1020805156-line-18)">&#160;Missing:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests</text><text class="terminal-1020805156-r2" x="1220" y="459.2" textLength="12.2" clip-path="url(#terminal-1020805156-line-18)"> 187 + </text><text class="terminal-1020805156-r2" x="1220" y="483.6" textLength="12.2" clip-path="url(#terminal-1020805156-line-19)"> 188 + </text><text class="terminal-1020805156-r3" x="24.4" y="508" textLength="122" clip-path="url(#terminal-1020805156-line-20)">INCOMPLETE</text><text class="terminal-1020805156-r1" x="158.6" y="508" textLength="195.2" clip-path="url(#terminal-1020805156-line-20)">Split&#160;Strategies</text><text class="terminal-1020805156-r4" x="366" y="508" textLength="73.2" clip-path="url(#terminal-1020805156-line-20)">(high)</text><text class="terminal-1020805156-r2" x="1220" y="508" textLength="12.2" clip-path="url(#terminal-1020805156-line-20)"> 189 + </text><text class="terminal-1020805156-r3" x="48.8" y="532.4" textLength="12.2" clip-path="url(#terminal-1020805156-line-21)">○</text><text class="terminal-1020805156-r2" x="61" y="532.4" textLength="1110.2" clip-path="url(#terminal-1020805156-line-21)">&#160;Missing:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;static_analysis</text><text class="terminal-1020805156-r2" x="1220" y="532.4" textLength="12.2" clip-path="url(#terminal-1020805156-line-21)"> 190 + </text><text class="terminal-1020805156-r2" x="1220" y="556.8" textLength="12.2" clip-path="url(#terminal-1020805156-line-22)"> 191 + </text><text class="terminal-1020805156-r3" x="24.4" y="581.2" textLength="122" clip-path="url(#terminal-1020805156-line-23)">INCOMPLETE</text><text class="terminal-1020805156-r1" x="158.6" y="581.2" textLength="195.2" clip-path="url(#terminal-1020805156-line-23)">Group&#160;Management</text><text class="terminal-1020805156-r4" x="366" y="581.2" textLength="73.2" clip-path="url(#terminal-1020805156-line-23)">(high)</text><text class="terminal-1020805156-r2" x="1220" y="581.2" textLength="12.2" clip-path="url(#terminal-1020805156-line-23)"> 192 + </text><text class="terminal-1020805156-r3" x="48.8" y="605.6" textLength="12.2" clip-path="url(#terminal-1020805156-line-24)">○</text><text class="terminal-1020805156-r2" x="61" y="605.6" textLength="1110.2" clip-path="url(#terminal-1020805156-line-24)">&#160;Missing:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;static_analysis</text><text class="terminal-1020805156-r2" x="1220" y="605.6" textLength="12.2" clip-path="url(#terminal-1020805156-line-24)"> 193 + </text><text class="terminal-1020805156-r2" x="1220" y="630" textLength="12.2" clip-path="url(#terminal-1020805156-line-25)"> 194 + </text><text class="terminal-1020805156-r3" x="24.4" y="654.4" textLength="122" clip-path="url(#terminal-1020805156-line-26)">INCOMPLETE</text><text class="terminal-1020805156-r1" x="158.6" y="654.4" textLength="158.6" clip-path="url(#terminal-1020805156-line-26)">Group&#160;Summary</text><text class="terminal-1020805156-r4" x="329.4" y="654.4" textLength="73.2" clip-path="url(#terminal-1020805156-line-26)">(high)</text><text class="terminal-1020805156-r2" x="1220" y="654.4" textLength="12.2" clip-path="url(#terminal-1020805156-line-26)"> 195 + </text><text class="terminal-1020805156-r3" x="48.8" y="678.8" textLength="12.2" clip-path="url(#terminal-1020805156-line-27)">○</text><text class="terminal-1020805156-r2" x="61" y="678.8" textLength="1110.2" clip-path="url(#terminal-1020805156-line-27)">&#160;Missing:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;static_analysis</text><text class="terminal-1020805156-r2" x="1220" y="678.8" textLength="12.2" clip-path="url(#terminal-1020805156-line-27)"> 196 + </text><text class="terminal-1020805156-r2" x="1220" y="703.2" textLength="12.2" clip-path="url(#terminal-1020805156-line-28)"> 197 + </text><text class="terminal-1020805156-r3" x="24.4" y="727.6" textLength="122" clip-path="url(#terminal-1020805156-line-29)">INCOMPLETE</text><text class="terminal-1020805156-r1" x="158.6" y="727.6" textLength="231.8" clip-path="url(#terminal-1020805156-line-29)">Debt&#160;Simplification</text><text class="terminal-1020805156-r4" x="402.6" y="727.6" textLength="73.2" clip-path="url(#terminal-1020805156-line-29)">(high)</text><text class="terminal-1020805156-r2" x="1220" y="727.6" textLength="12.2" clip-path="url(#terminal-1020805156-line-29)"> 198 + </text><text class="terminal-1020805156-r3" x="48.8" y="752" textLength="12.2" clip-path="url(#terminal-1020805156-line-30)">○</text><text class="terminal-1020805156-r2" x="61" y="752" textLength="1110.2" clip-path="url(#terminal-1020805156-line-30)">&#160;Missing:&#160;typecheck,&#160;lint,&#160;boundary_validation,&#160;unit_tests,&#160;property_tests,&#160;static_analysis</text><text class="terminal-1020805156-r2" x="1220" y="752" textLength="12.2" clip-path="url(#terminal-1020805156-line-30)"> 199 + </text><text class="terminal-1020805156-r2" x="1220" y="776.4" textLength="12.2" clip-path="url(#terminal-1020805156-line-31)"> 200 + </text><text class="terminal-1020805156-r3" x="24.4" y="800.8" textLength="122" clip-path="url(#terminal-1020805156-line-32)">INCOMPLETE</text><text class="terminal-1020805156-r1" x="158.6" y="800.8" textLength="256.2" clip-path="url(#terminal-1020805156-line-32)">Recording&#160;Settlements</text><text class="terminal-1020805156-r4" x="427" y="800.8" textLength="61" clip-path="url(#terminal-1020805156-line-32)">(low)</text><text class="terminal-1020805156-r2" x="1220" y="800.8" textLength="12.2" clip-path="url(#terminal-1020805156-line-32)"> 201 + </text><text class="terminal-1020805156-r3" x="48.8" y="825.2" textLength="12.2" clip-path="url(#terminal-1020805156-line-33)">○</text><text class="terminal-1020805156-r2" x="61" y="825.2" textLength="561.2" clip-path="url(#terminal-1020805156-line-33)">&#160;Missing:&#160;typecheck,&#160;lint,&#160;boundary_validation</text><text class="terminal-1020805156-r2" x="1220" y="825.2" textLength="12.2" clip-path="url(#terminal-1020805156-line-33)"> 202 + </text><text class="terminal-1020805156-r2" x="1220" y="849.6" textLength="12.2" clip-path="url(#terminal-1020805156-line-34)"> 203 + </text><text class="terminal-1020805156-r3" x="24.4" y="874" textLength="122" clip-path="url(#terminal-1020805156-line-35)">INCOMPLETE</text><text class="terminal-1020805156-r1" x="158.6" y="874" textLength="207.4" clip-path="url(#terminal-1020805156-line-35)">Settlement&#160;Status</text><text class="terminal-1020805156-r4" x="378.2" y="874" textLength="61" clip-path="url(#terminal-1020805156-line-35)">(low)</text><text class="terminal-1020805156-r2" x="1220" y="874" textLength="12.2" clip-path="url(#terminal-1020805156-line-35)"> 204 + </text><text class="terminal-1020805156-r3" x="48.8" y="898.4" textLength="12.2" clip-path="url(#terminal-1020805156-line-36)">○</text><text class="terminal-1020805156-r2" x="61" y="898.4" textLength="561.2" clip-path="url(#terminal-1020805156-line-36)">&#160;Missing:&#160;typecheck,&#160;lint,&#160;boundary_validation</text><text class="terminal-1020805156-r2" x="1220" y="898.4" textLength="12.2" clip-path="url(#terminal-1020805156-line-36)"> 205 + </text><text class="terminal-1020805156-r2" x="1220" y="922.8" textLength="12.2" clip-path="url(#terminal-1020805156-line-37)"> 206 + </text> 207 + </g> 208 + </g> 209 + </svg>
+49
examples/settle-up/screenshots/capture.py
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Capture terminal output as SVG screenshots using Rich. 4 + Usage: echo "output" | python3 capture.py <name> [--width=N] 5 + or: python3 capture.py <name> --cmd="phoenix status" [--width=N] 6 + """ 7 + import sys 8 + import subprocess 9 + import os 10 + 11 + from rich.console import Console 12 + from rich.text import Text 13 + 14 + def main(): 15 + name = sys.argv[1] if len(sys.argv) > 1 else "screenshot" 16 + width = 100 17 + cmd = None 18 + 19 + for arg in sys.argv[2:]: 20 + if arg.startswith("--width="): 21 + width = int(arg.split("=")[1]) 22 + elif arg.startswith("--cmd="): 23 + cmd = arg.split("=", 1)[1] 24 + 25 + # Get the output 26 + if cmd: 27 + result = subprocess.run( 28 + cmd, shell=True, capture_output=True, text=True, 29 + env={**os.environ, "FORCE_COLOR": "1", "TERM": "xterm-256color"} 30 + ) 31 + raw = result.stdout + result.stderr 32 + else: 33 + raw = sys.stdin.read() 34 + 35 + out_dir = os.path.dirname(os.path.abspath(__file__)) 36 + svg_path = os.path.join(out_dir, f"{name}.svg") 37 + 38 + console = Console(record=True, width=width, force_terminal=True) 39 + text = Text.from_ansi(raw) 40 + console.print(text) 41 + 42 + svg = console.export_svg(title=f"phoenix — {name}") 43 + with open(svg_path, "w") as f: 44 + f.write(svg) 45 + 46 + print(f"✔ {svg_path}", file=sys.stderr) 47 + 48 + if __name__ == "__main__": 49 + main()
+92
examples/settle-up/src/generated/api/__tests__/api.test.ts
··· 1 + /** 2 + * Api — Generated Tests 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Tests module structure, server health, and Phoenix traceability. 6 + */ 7 + 8 + import { describe, it, expect, afterAll } from 'vitest'; 9 + import { startServer } from '../server.js'; 10 + 11 + import * as endpoints from '../endpoints.js'; 12 + import * as errorHandling from '../error-handling.js'; 13 + import * as responseFormat from '../response-format.js'; 14 + 15 + describe('Api modules', () => { 16 + describe('Endpoints', () => { 17 + it('exports Phoenix traceability metadata', () => { 18 + expect(endpoints._phoenix).toBeDefined(); 19 + expect(endpoints._phoenix.name).toBe('Endpoints'); 20 + expect(endpoints._phoenix.risk_tier).toBeTruthy(); 21 + }); 22 + 23 + it('has exported functions', () => { 24 + const exports = Object.keys(endpoints).filter(k => k !== '_phoenix'); 25 + expect(exports.length).toBeGreaterThan(0); 26 + }); 27 + }); 28 + 29 + describe('Error Handling', () => { 30 + it('exports Phoenix traceability metadata', () => { 31 + expect(errorHandling._phoenix).toBeDefined(); 32 + expect(errorHandling._phoenix.name).toBe('Error Handling'); 33 + expect(errorHandling._phoenix.risk_tier).toBeTruthy(); 34 + }); 35 + 36 + it('has exported functions', () => { 37 + const exports = Object.keys(errorHandling).filter(k => k !== '_phoenix'); 38 + expect(exports.length).toBeGreaterThan(0); 39 + }); 40 + }); 41 + 42 + describe('Response Format', () => { 43 + it('exports Phoenix traceability metadata', () => { 44 + expect(responseFormat._phoenix).toBeDefined(); 45 + expect(responseFormat._phoenix.name).toBe('Response Format'); 46 + expect(responseFormat._phoenix.risk_tier).toBeTruthy(); 47 + }); 48 + 49 + it('has exported functions', () => { 50 + const exports = Object.keys(responseFormat).filter(k => k !== '_phoenix'); 51 + expect(exports.length).toBeGreaterThan(0); 52 + }); 53 + }); 54 + 55 + }); 56 + 57 + describe('Api server', () => { 58 + const instance = startServer(0); // random port 59 + 60 + afterAll(() => new Promise<void>(resolve => instance.server.close(() => resolve()))); 61 + 62 + it('GET /health returns 200', async () => { 63 + await instance.ready; 64 + const res = await fetch(`http://localhost:${instance.port}/health`); 65 + expect(res.status).toBe(200); 66 + const body = await res.json() as Record<string, unknown>; 67 + expect(body.status).toBe('ok'); 68 + expect(body.service).toBe('Api'); 69 + }); 70 + 71 + it('GET /metrics returns request counts', async () => { 72 + await instance.ready; 73 + const res = await fetch(`http://localhost:${instance.port}/metrics`); 74 + expect(res.status).toBe(200); 75 + const body = await res.json() as Record<string, unknown>; 76 + expect(typeof body.requests_total).toBe('number'); 77 + }); 78 + 79 + it('GET /modules lists all registered modules', async () => { 80 + await instance.ready; 81 + const res = await fetch(`http://localhost:${instance.port}/modules`); 82 + expect(res.status).toBe(200); 83 + const body = await res.json() as Array<Record<string, unknown>>; 84 + expect(body.length).toBe(3); 85 + }); 86 + 87 + it('GET /unknown returns 404', async () => { 88 + await instance.ready; 89 + const res = await fetch(`http://localhost:${instance.port}/unknown`); 90 + expect(res.status).toBe(404); 91 + }); 92 + });
+260
examples/settle-up/src/generated/api/endpoints.ts
··· 1 + import { IncomingMessage, ServerResponse } from 'node:http'; 2 + import { parse } from 'node:url'; 3 + 4 + export interface Member { 5 + id: string; 6 + name: string; 7 + email: string; 8 + } 9 + 10 + export interface Group { 11 + id: string; 12 + name: string; 13 + currency: string; 14 + creator: Member; 15 + members: Member[]; 16 + expenses: Expense[]; 17 + } 18 + 19 + export interface Expense { 20 + id: string; 21 + description: string; 22 + amount: number; 23 + paidBy: string; 24 + splitAmong: string[]; 25 + date: Date; 26 + } 27 + 28 + export interface Settlement { 29 + from: string; 30 + to: string; 31 + amount: number; 32 + } 33 + 34 + export interface CreateGroupRequest { 35 + name: string; 36 + currency: string; 37 + creator: Member; 38 + } 39 + 40 + class GroupStore { 41 + private groups = new Map<string, Group>(); 42 + private nextId = 1; 43 + 44 + createGroup(request: CreateGroupRequest): Group { 45 + const id = (this.nextId++).toString(); 46 + const group: Group = { 47 + id, 48 + name: request.name, 49 + currency: request.currency, 50 + creator: request.creator, 51 + members: [request.creator], 52 + expenses: [] 53 + }; 54 + this.groups.set(id, group); 55 + return group; 56 + } 57 + 58 + getGroup(id: string): Group | undefined { 59 + return this.groups.get(id); 60 + } 61 + 62 + removeMember(groupId: string, memberId: string): boolean { 63 + const group = this.groups.get(groupId); 64 + if (!group) return false; 65 + 66 + const memberIndex = group.members.findIndex(m => m.id === memberId); 67 + if (memberIndex === -1) return false; 68 + 69 + group.members.splice(memberIndex, 1); 70 + return true; 71 + } 72 + 73 + deleteExpense(groupId: string, expenseId: string): boolean { 74 + const group = this.groups.get(groupId); 75 + if (!group) return false; 76 + 77 + const expenseIndex = group.expenses.findIndex(e => e.id === expenseId); 78 + if (expenseIndex === -1) return false; 79 + 80 + group.expenses.splice(expenseIndex, 1); 81 + return true; 82 + } 83 + 84 + computeSettlements(groupId: string): Settlement[] { 85 + const group = this.groups.get(groupId); 86 + if (!group) return []; 87 + 88 + const balances = new Map<string, number>(); 89 + 90 + // Initialize balances 91 + group.members.forEach(member => { 92 + balances.set(member.id, 0); 93 + }); 94 + 95 + // Calculate net balances from expenses 96 + group.expenses.forEach(expense => { 97 + const paidAmount = expense.amount; 98 + const shareAmount = paidAmount / expense.splitAmong.length; 99 + 100 + // Person who paid gets credited 101 + const currentBalance = balances.get(expense.paidBy) || 0; 102 + balances.set(expense.paidBy, currentBalance + paidAmount); 103 + 104 + // People who owe get debited 105 + expense.splitAmong.forEach(memberId => { 106 + const memberBalance = balances.get(memberId) || 0; 107 + balances.set(memberId, memberBalance - shareAmount); 108 + }); 109 + }); 110 + 111 + // Convert to settlements using greedy algorithm 112 + const settlements: Settlement[] = []; 113 + const creditors: Array<{id: string, amount: number}> = []; 114 + const debtors: Array<{id: string, amount: number}> = []; 115 + 116 + balances.forEach((balance, memberId) => { 117 + if (balance > 0.01) { 118 + creditors.push({id: memberId, amount: balance}); 119 + } else if (balance < -0.01) { 120 + debtors.push({id: memberId, amount: -balance}); 121 + } 122 + }); 123 + 124 + // Sort for consistent results 125 + creditors.sort((a, b) => b.amount - a.amount); 126 + debtors.sort((a, b) => b.amount - a.amount); 127 + 128 + let i = 0, j = 0; 129 + while (i < creditors.length && j < debtors.length) { 130 + const creditor = creditors[i]; 131 + const debtor = debtors[j]; 132 + 133 + const settleAmount = Math.min(creditor.amount, debtor.amount); 134 + 135 + if (settleAmount > 0.01) { 136 + settlements.push({ 137 + from: debtor.id, 138 + to: creditor.id, 139 + amount: Math.round(settleAmount * 100) / 100 140 + }); 141 + } 142 + 143 + creditor.amount -= settleAmount; 144 + debtor.amount -= settleAmount; 145 + 146 + if (creditor.amount < 0.01) i++; 147 + if (debtor.amount < 0.01) j++; 148 + } 149 + 150 + return settlements; 151 + } 152 + } 153 + 154 + const store = new GroupStore(); 155 + 156 + export function handleRequest(req: IncomingMessage, res: ServerResponse): void { 157 + const url = parse(req.url || '', true); 158 + const pathname = url.pathname || ''; 159 + const method = req.method || 'GET'; 160 + 161 + try { 162 + if (method === 'POST' && pathname === '/groups') { 163 + handleCreateGroup(req, res); 164 + } else if (method === 'DELETE' && pathname.match(/^\/groups\/[^\/]+\/members\/[^\/]+$/)) { 165 + handleRemoveMember(req, res, pathname); 166 + } else if (method === 'DELETE' && pathname.match(/^\/groups\/[^\/]+\/expenses\/[^\/]+$/)) { 167 + handleDeleteExpense(req, res, pathname); 168 + } else if (method === 'GET' && pathname.match(/^\/groups\/[^\/]+\/settlements$/)) { 169 + handleGetSettlements(req, res, pathname); 170 + } else { 171 + res.writeHead(404, { 'Content-Type': 'application/json' }); 172 + res.end(JSON.stringify({ error: 'Not found' })); 173 + } 174 + } catch (error) { 175 + res.writeHead(500, { 'Content-Type': 'application/json' }); 176 + res.end(JSON.stringify({ error: 'Internal server error' })); 177 + } 178 + } 179 + 180 + function handleCreateGroup(req: IncomingMessage, res: ServerResponse): void { 181 + let body = ''; 182 + req.on('data', chunk => { 183 + body += chunk.toString(); 184 + }); 185 + 186 + req.on('end', () => { 187 + try { 188 + const data = JSON.parse(body) as CreateGroupRequest; 189 + 190 + if (!data.name || !data.currency || !data.creator) { 191 + res.writeHead(400, { 'Content-Type': 'application/json' }); 192 + res.end(JSON.stringify({ error: 'Missing required fields: name, currency, creator' })); 193 + return; 194 + } 195 + 196 + const group = store.createGroup(data); 197 + res.writeHead(201, { 'Content-Type': 'application/json' }); 198 + res.end(JSON.stringify(group)); 199 + } catch (error) { 200 + res.writeHead(400, { 'Content-Type': 'application/json' }); 201 + res.end(JSON.stringify({ error: 'Invalid JSON' })); 202 + } 203 + }); 204 + } 205 + 206 + function handleRemoveMember(req: IncomingMessage, res: ServerResponse, pathname: string): void { 207 + const parts = pathname.split('/'); 208 + const groupId = parts[2]; 209 + const memberId = parts[4]; 210 + 211 + const success = store.removeMember(groupId, memberId); 212 + 213 + if (success) { 214 + res.writeHead(204); 215 + res.end(); 216 + } else { 217 + res.writeHead(404, { 'Content-Type': 'application/json' }); 218 + res.end(JSON.stringify({ error: 'Group or member not found' })); 219 + } 220 + } 221 + 222 + function handleDeleteExpense(req: IncomingMessage, res: ServerResponse, pathname: string): void { 223 + const parts = pathname.split('/'); 224 + const groupId = parts[2]; 225 + const expenseId = parts[4]; 226 + 227 + const success = store.deleteExpense(groupId, expenseId); 228 + 229 + if (success) { 230 + res.writeHead(204); 231 + res.end(); 232 + } else { 233 + res.writeHead(404, { 'Content-Type': 'application/json' }); 234 + res.end(JSON.stringify({ error: 'Group or expense not found' })); 235 + } 236 + } 237 + 238 + function handleGetSettlements(req: IncomingMessage, res: ServerResponse, pathname: string): void { 239 + const parts = pathname.split('/'); 240 + const groupId = parts[2]; 241 + 242 + const group = store.getGroup(groupId); 243 + if (!group) { 244 + res.writeHead(404, { 'Content-Type': 'application/json' }); 245 + res.end(JSON.stringify({ error: 'Group not found' })); 246 + return; 247 + } 248 + 249 + const settlements = store.computeSettlements(groupId); 250 + res.writeHead(200, { 'Content-Type': 'application/json' }); 251 + res.end(JSON.stringify({ settlements })); 252 + } 253 + 254 + /** @internal Phoenix VCS traceability — do not remove. */ 255 + export const _phoenix = { 256 + iu_id: '85e17ce4d231ac2072e788df85a14c112d87bf7358ac68b778f77687c52f0ca6', 257 + name: 'Endpoints', 258 + risk_tier: 'high', 259 + canon_ids: [4 as const], 260 + } as const;
+196
examples/settle-up/src/generated/api/error-handling.ts
··· 1 + export interface ErrorResponse { 2 + error: { 3 + code: string; 4 + message: string; 5 + }; 6 + } 7 + 8 + export interface ValidationError { 9 + field: string; 10 + message: string; 11 + } 12 + 13 + export interface DetailedErrorResponse extends ErrorResponse { 14 + error: { 15 + code: string; 16 + message: string; 17 + details?: ValidationError[]; 18 + }; 19 + } 20 + 21 + export class PhoenixError extends Error { 22 + constructor( 23 + public readonly code: string, 24 + public readonly statusCode: number, 25 + message: string, 26 + public readonly details?: ValidationError[] 27 + ) { 28 + super(message); 29 + this.name = 'PhoenixError'; 30 + } 31 + 32 + toJSON(): ErrorResponse | DetailedErrorResponse { 33 + const response: ErrorResponse | DetailedErrorResponse = { 34 + error: { 35 + code: this.code, 36 + message: this.message, 37 + }, 38 + }; 39 + 40 + if (this.details && this.details.length > 0) { 41 + (response as DetailedErrorResponse).error.details = this.details; 42 + } 43 + 44 + return response; 45 + } 46 + } 47 + 48 + export function createNotFoundError(resource: string = 'Resource'): PhoenixError { 49 + return new PhoenixError( 50 + 'NOT_FOUND', 51 + 404, 52 + `${resource} not found` 53 + ); 54 + } 55 + 56 + export function createGroupNotFoundError(): PhoenixError { 57 + return new PhoenixError( 58 + 'GROUP_NOT_FOUND', 59 + 404, 60 + 'Group identifier is invalid' 61 + ); 62 + } 63 + 64 + export function createForbiddenError(reason: string = 'Access denied'): PhoenixError { 65 + return new PhoenixError( 66 + 'FORBIDDEN', 67 + 403, 68 + reason 69 + ); 70 + } 71 + 72 + export function createMemberAccessError(): PhoenixError { 73 + return new PhoenixError( 74 + 'MEMBER_ACCESS_DENIED', 75 + 403, 76 + 'Invalid member who is not in the group cannot access this resource' 77 + ); 78 + } 79 + 80 + export function createBadRequestError(message: string, details?: ValidationError[]): PhoenixError { 81 + return new PhoenixError( 82 + 'BAD_REQUEST', 83 + 400, 84 + message, 85 + details 86 + ); 87 + } 88 + 89 + export function createExpenseValidationError(issues: ValidationError[]): PhoenixError { 90 + return new PhoenixError( 91 + 'EXPENSE_VALIDATION_ERROR', 92 + 400, 93 + 'Expense data validation failed', 94 + issues 95 + ); 96 + } 97 + 98 + export function createNegativeAmountError(): PhoenixError { 99 + return new PhoenixError( 100 + 'NEGATIVE_AMOUNT', 101 + 400, 102 + 'Expense amounts cannot be negative', 103 + [{ field: 'amount', message: 'Amount must be greater than zero' }] 104 + ); 105 + } 106 + 107 + export function createMissingParticipantsError(): PhoenixError { 108 + return new PhoenixError( 109 + 'MISSING_PARTICIPANTS', 110 + 400, 111 + 'Expense must have at least one participant', 112 + [{ field: 'participants', message: 'At least one participant is required' }] 113 + ); 114 + } 115 + 116 + export function createConflictError(message: string): PhoenixError { 117 + return new PhoenixError( 118 + 'CONFLICT', 119 + 409, 120 + message 121 + ); 122 + } 123 + 124 + export function createNonZeroBalanceError(memberName: string, balance: number): PhoenixError { 125 + return new PhoenixError( 126 + 'NON_ZERO_BALANCE', 127 + 409, 128 + `Cannot remove member ${memberName} with non-zero balance of ${balance}` 129 + ); 130 + } 131 + 132 + export function validateExpenseData(expenseData: { 133 + amount?: number; 134 + participants?: string[]; 135 + }): ValidationError[] { 136 + const errors: ValidationError[] = []; 137 + 138 + if (typeof expenseData.amount === 'number' && expenseData.amount < 0) { 139 + errors.push({ 140 + field: 'amount', 141 + message: 'Amount must be greater than zero' 142 + }); 143 + } 144 + 145 + if (!expenseData.participants || expenseData.participants.length === 0) { 146 + errors.push({ 147 + field: 'participants', 148 + message: 'At least one participant is required' 149 + }); 150 + } 151 + 152 + return errors; 153 + } 154 + 155 + export function handleExpenseValidation(expenseData: { 156 + amount?: number; 157 + participants?: string[]; 158 + }): void { 159 + const validationErrors = validateExpenseData(expenseData); 160 + 161 + if (validationErrors.length > 0) { 162 + throw createExpenseValidationError(validationErrors); 163 + } 164 + } 165 + 166 + export function isPhoenixError(error: unknown): error is PhoenixError { 167 + return error instanceof PhoenixError; 168 + } 169 + 170 + export function formatErrorResponse(error: unknown): ErrorResponse | DetailedErrorResponse { 171 + if (isPhoenixError(error)) { 172 + return error.toJSON(); 173 + } 174 + 175 + return { 176 + error: { 177 + code: 'INTERNAL_ERROR', 178 + message: 'An unexpected error occurred' 179 + } 180 + }; 181 + } 182 + 183 + export function getStatusCode(error: unknown): number { 184 + if (isPhoenixError(error)) { 185 + return error.statusCode; 186 + } 187 + return 500; 188 + } 189 + 190 + /** @internal Phoenix VCS traceability — do not remove. */ 191 + export const _phoenix = { 192 + iu_id: '5b06984fefd4888f917d13b13c21bb66490bb8c66bb61fc86a457bcc59694904', 193 + name: 'Error Handling', 194 + risk_tier: 'medium', 195 + canon_ids: [5 as const], 196 + } as const;
+10
examples/settle-up/src/generated/api/index.ts
··· 1 + /** 2 + * Api 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Barrel export for all Api modules. 6 + */ 7 + 8 + export * as endpoints from './endpoints.js'; 9 + export * as errorHandling from './error-handling.js'; 10 + export * as responseFormat from './response-format.js';
+136
examples/settle-up/src/generated/api/response-format.ts
··· 1 + export interface ResponseEnvelope<T = unknown> { 2 + ok: boolean; 3 + data?: T; 4 + error?: { 5 + code: string; 6 + message: string; 7 + }; 8 + } 9 + 10 + export interface PaginationParams { 11 + limit?: number; 12 + offset?: number; 13 + } 14 + 15 + export interface PaginatedData<T> { 16 + items: T[]; 17 + total: number; 18 + limit: number; 19 + offset: number; 20 + hasMore: boolean; 21 + } 22 + 23 + export interface MonetaryAmount { 24 + cents: number; 25 + currency?: string; 26 + } 27 + 28 + export function createSuccessResponse<T>(data: T): ResponseEnvelope<T> { 29 + return { 30 + ok: true, 31 + data, 32 + }; 33 + } 34 + 35 + export function createErrorResponse(code: string, message: string): ResponseEnvelope<never> { 36 + return { 37 + ok: false, 38 + error: { 39 + code, 40 + message, 41 + }, 42 + }; 43 + } 44 + 45 + export function createPaginatedResponse<T>( 46 + items: T[], 47 + total: number, 48 + params: PaginationParams 49 + ): ResponseEnvelope<PaginatedData<T>> { 50 + const limit = Math.max(1, params.limit || 20); 51 + const offset = Math.max(0, params.offset || 0); 52 + 53 + return createSuccessResponse({ 54 + items, 55 + total, 56 + limit, 57 + offset, 58 + hasMore: offset + items.length < total, 59 + }); 60 + } 61 + 62 + export function parsePaginationParams(query: Record<string, string | undefined>): PaginationParams { 63 + const limit = query.limit ? parseInt(query.limit, 10) : undefined; 64 + const offset = query.offset ? parseInt(query.offset, 10) : undefined; 65 + 66 + return { 67 + limit: limit && limit > 0 ? Math.min(limit, 1000) : undefined, 68 + offset: offset && offset >= 0 ? offset : undefined, 69 + }; 70 + } 71 + 72 + export function formatMonetaryAmount(cents: number, currency = 'USD'): MonetaryAmount { 73 + if (!Number.isInteger(cents)) { 74 + throw new Error('Monetary amount must be an integer representing cents'); 75 + } 76 + 77 + return { 78 + cents, 79 + currency, 80 + }; 81 + } 82 + 83 + export function dollarsToCents(dollars: number): number { 84 + return Math.round(dollars * 100); 85 + } 86 + 87 + export function centsToDollars(cents: number): number { 88 + return cents / 100; 89 + } 90 + 91 + export function isResponseEnvelope(obj: unknown): obj is ResponseEnvelope { 92 + if (typeof obj !== 'object' || obj === null) { 93 + return false; 94 + } 95 + 96 + const envelope = obj as Record<string, unknown>; 97 + 98 + if (typeof envelope.ok !== 'boolean') { 99 + return false; 100 + } 101 + 102 + if (envelope.error !== undefined) { 103 + if (typeof envelope.error !== 'object' || envelope.error === null) { 104 + return false; 105 + } 106 + 107 + const error = envelope.error as Record<string, unknown>; 108 + if (typeof error.code !== 'string' || typeof error.message !== 'string') { 109 + return false; 110 + } 111 + } 112 + 113 + return true; 114 + } 115 + 116 + export function validatePaginationParams(params: PaginationParams): void { 117 + if (params.limit !== undefined) { 118 + if (!Number.isInteger(params.limit) || params.limit < 1 || params.limit > 1000) { 119 + throw new Error('Limit must be an integer between 1 and 1000'); 120 + } 121 + } 122 + 123 + if (params.offset !== undefined) { 124 + if (!Number.isInteger(params.offset) || params.offset < 0) { 125 + throw new Error('Offset must be a non-negative integer'); 126 + } 127 + } 128 + } 129 + 130 + /** @internal Phoenix VCS traceability — do not remove. */ 131 + export const _phoenix = { 132 + iu_id: 'b517e8f546c99de3fc504064d309c37cafff62e07f0c0cb70a873fbc12467de4', 133 + name: 'Response Format', 134 + risk_tier: 'low', 135 + canon_ids: [3 as const], 136 + } as const;
+123
examples/settle-up/src/generated/api/server.ts
··· 1 + /** 2 + * Api — HTTP Server 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Provides health check, metrics, and module endpoints. 6 + */ 7 + 8 + import { createServer, IncomingMessage, ServerResponse } from 'node:http'; 9 + 10 + import * as endpoints from './endpoints.js'; 11 + import * as errorHandling from './error-handling.js'; 12 + import * as responseFormat from './response-format.js'; 13 + 14 + // ─── Metrics ───────────────────────────────────────────────────────────────── 15 + 16 + const _svcMetrics = { 17 + requests_total: 0, 18 + requests_by_path: {} as Record<string, number>, 19 + errors_total: 0, 20 + uptime_start: Date.now(), 21 + }; 22 + 23 + // ─── Module Registry ───────────────────────────────────────────────────────── 24 + 25 + const _svcModules = { 26 + 'endpoints': endpoints, 27 + 'error-handling': errorHandling, 28 + 'response-format': responseFormat, 29 + }; 30 + 31 + // ─── Router ────────────────────────────────────────────────────────────────── 32 + 33 + type Handler = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>; 34 + 35 + const routes: Record<string, Handler> = { 36 + '/health': (_req, res) => { 37 + res.writeHead(200, { 'Content-Type': 'application/json' }); 38 + res.end(JSON.stringify({ 39 + status: 'ok', 40 + service: 'Api', 41 + uptime: Math.floor((Date.now() - _svcMetrics.uptime_start) / 1000), 42 + modules: Object.keys(_svcModules), 43 + })); 44 + }, 45 + 46 + '/metrics': (_req, res) => { 47 + res.writeHead(200, { 'Content-Type': 'application/json' }); 48 + res.end(JSON.stringify({ 49 + ..._svcMetrics, 50 + uptime_seconds: Math.floor((Date.now() - _svcMetrics.uptime_start) / 1000), 51 + }, null, 2)); 52 + }, 53 + 54 + '/modules': (_req, res) => { 55 + const info = Object.entries(_svcModules).map(([name, mod]) => { 56 + const phoenix = (mod as Record<string, unknown>)._phoenix as Record<string, unknown> | undefined; 57 + return { 58 + name, 59 + risk_tier: phoenix?.risk_tier ?? 'unknown', 60 + exports: Object.keys(mod).filter(k => k !== '_phoenix'), 61 + }; 62 + }); 63 + res.writeHead(200, { 'Content-Type': 'application/json' }); 64 + res.end(JSON.stringify(info, null, 2)); 65 + }, 66 + }; 67 + 68 + // ─── Server ────────────────────────────────────────────────────────────────── 69 + 70 + function handleRequest(req: IncomingMessage, res: ServerResponse): void { 71 + const url = req.url ?? '/'; 72 + const path = url.split('?')[0]; 73 + 74 + _svcMetrics.requests_total++; 75 + _svcMetrics.requests_by_path[path] = (_svcMetrics.requests_by_path[path] ?? 0) + 1; 76 + 77 + const handler = routes[path]; 78 + if (handler) { 79 + try { 80 + handler(req, res); 81 + } catch (err) { 82 + _svcMetrics.errors_total++; 83 + res.writeHead(500, { 'Content-Type': 'application/json' }); 84 + res.end(JSON.stringify({ error: String(err) })); 85 + } 86 + } else { 87 + res.writeHead(404, { 'Content-Type': 'application/json' }); 88 + res.end(JSON.stringify({ 89 + error: 'Not Found', 90 + path, 91 + available: Object.keys(routes), 92 + })); 93 + } 94 + } 95 + 96 + export function startServer(port?: number): { server: ReturnType<typeof createServer>; port: number; ready: Promise<void> } { 97 + const requestedPort = port ?? parseInt(process.env.API_PORT ?? process.env.PORT ?? '3000', 10); 98 + const server = createServer(handleRequest); 99 + let actualPort = requestedPort; 100 + 101 + const ready = new Promise<void>(resolve => { 102 + server.listen(requestedPort, () => { 103 + const addr = server.address(); 104 + if (addr && typeof addr === 'object') actualPort = addr.port; 105 + result.port = actualPort; 106 + console.log(`Api listening on http://localhost:${actualPort}`); 107 + console.log(` /health — health check`); 108 + console.log(` /metrics — request metrics`); 109 + console.log(` /modules — registered modules`); 110 + resolve(); 111 + }); 112 + }); 113 + 114 + const result = { server, port: actualPort, ready }; 115 + return result; 116 + } 117 + 118 + // Start when run directly 119 + const isMain = process.argv[1]?.endsWith('/api/server.js') || 120 + process.argv[1]?.endsWith('/api/server.ts'); 121 + if (isMain) { 122 + startServer(); 123 + }
+106
examples/settle-up/src/generated/expenses/__tests__/expenses.test.ts
··· 1 + /** 2 + * Expenses — Generated Tests 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Tests module structure, server health, and Phoenix traceability. 6 + */ 7 + 8 + import { describe, it, expect, afterAll } from 'vitest'; 9 + import { startServer } from '../server.js'; 10 + 11 + import * as balanceCalculation from '../balance-calculation.js'; 12 + import * as creatingAnExpense from '../creating-an-expense.js'; 13 + import * as expenseHistory from '../expense-history.js'; 14 + import * as splitStrategies from '../split-strategies.js'; 15 + 16 + describe('Expenses modules', () => { 17 + describe('Balance Calculation', () => { 18 + it('exports Phoenix traceability metadata', () => { 19 + expect(balanceCalculation._phoenix).toBeDefined(); 20 + expect(balanceCalculation._phoenix.name).toBe('Balance Calculation'); 21 + expect(balanceCalculation._phoenix.risk_tier).toBeTruthy(); 22 + }); 23 + 24 + it('has exported functions', () => { 25 + const exports = Object.keys(balanceCalculation).filter(k => k !== '_phoenix'); 26 + expect(exports.length).toBeGreaterThan(0); 27 + }); 28 + }); 29 + 30 + describe('Creating an Expense', () => { 31 + it('exports Phoenix traceability metadata', () => { 32 + expect(creatingAnExpense._phoenix).toBeDefined(); 33 + expect(creatingAnExpense._phoenix.name).toBe('Creating an Expense'); 34 + expect(creatingAnExpense._phoenix.risk_tier).toBeTruthy(); 35 + }); 36 + 37 + it('has exported functions', () => { 38 + const exports = Object.keys(creatingAnExpense).filter(k => k !== '_phoenix'); 39 + expect(exports.length).toBeGreaterThan(0); 40 + }); 41 + }); 42 + 43 + describe('Expense History', () => { 44 + it('exports Phoenix traceability metadata', () => { 45 + expect(expenseHistory._phoenix).toBeDefined(); 46 + expect(expenseHistory._phoenix.name).toBe('Expense History'); 47 + expect(expenseHistory._phoenix.risk_tier).toBeTruthy(); 48 + }); 49 + 50 + it('has exported functions', () => { 51 + const exports = Object.keys(expenseHistory).filter(k => k !== '_phoenix'); 52 + expect(exports.length).toBeGreaterThan(0); 53 + }); 54 + }); 55 + 56 + describe('Split Strategies', () => { 57 + it('exports Phoenix traceability metadata', () => { 58 + expect(splitStrategies._phoenix).toBeDefined(); 59 + expect(splitStrategies._phoenix.name).toBe('Split Strategies'); 60 + expect(splitStrategies._phoenix.risk_tier).toBeTruthy(); 61 + }); 62 + 63 + it('has exported functions', () => { 64 + const exports = Object.keys(splitStrategies).filter(k => k !== '_phoenix'); 65 + expect(exports.length).toBeGreaterThan(0); 66 + }); 67 + }); 68 + 69 + }); 70 + 71 + describe('Expenses server', () => { 72 + const instance = startServer(0); // random port 73 + 74 + afterAll(() => new Promise<void>(resolve => instance.server.close(() => resolve()))); 75 + 76 + it('GET /health returns 200', async () => { 77 + await instance.ready; 78 + const res = await fetch(`http://localhost:${instance.port}/health`); 79 + expect(res.status).toBe(200); 80 + const body = await res.json() as Record<string, unknown>; 81 + expect(body.status).toBe('ok'); 82 + expect(body.service).toBe('Expenses'); 83 + }); 84 + 85 + it('GET /metrics returns request counts', async () => { 86 + await instance.ready; 87 + const res = await fetch(`http://localhost:${instance.port}/metrics`); 88 + expect(res.status).toBe(200); 89 + const body = await res.json() as Record<string, unknown>; 90 + expect(typeof body.requests_total).toBe('number'); 91 + }); 92 + 93 + it('GET /modules lists all registered modules', async () => { 94 + await instance.ready; 95 + const res = await fetch(`http://localhost:${instance.port}/modules`); 96 + expect(res.status).toBe(200); 97 + const body = await res.json() as Array<Record<string, unknown>>; 98 + expect(body.length).toBe(4); 99 + }); 100 + 101 + it('GET /unknown returns 404', async () => { 102 + await instance.ready; 103 + const res = await fetch(`http://localhost:${instance.port}/unknown`); 104 + expect(res.status).toBe(404); 105 + }); 106 + });
+213
examples/settle-up/src/generated/expenses/balance-calculation.ts
··· 1 + export interface Expense { 2 + id: string; 3 + amount: number; 4 + paidBy: string; 5 + participants: string[]; 6 + splitStrategy: 'equal' | 'exact' | 'percentage'; 7 + splits?: Record<string, number>; 8 + description?: string; 9 + date: Date; 10 + } 11 + 12 + export interface Balance { 13 + memberId: string; 14 + amount: number; 15 + } 16 + 17 + export interface BalanceCalculationResult { 18 + balances: Balance[]; 19 + totalExpenses: number; 20 + lastCalculated: Date; 21 + } 22 + 23 + export class BalanceCalculator { 24 + private expenses: Map<string, Expense> = new Map(); 25 + private cachedResult: BalanceCalculationResult | null = null; 26 + private isDirty = false; 27 + 28 + addExpense(expense: Expense): void { 29 + this.expenses.set(expense.id, { ...expense }); 30 + this.invalidateCache(); 31 + } 32 + 33 + removeExpense(expenseId: string): boolean { 34 + const removed = this.expenses.delete(expenseId); 35 + if (removed) { 36 + this.invalidateCache(); 37 + } 38 + return removed; 39 + } 40 + 41 + updateExpense(expense: Expense): void { 42 + if (this.expenses.has(expense.id)) { 43 + this.expenses.set(expense.id, { ...expense }); 44 + this.invalidateCache(); 45 + } 46 + } 47 + 48 + calculateBalances(): BalanceCalculationResult { 49 + if (this.cachedResult && !this.isDirty) { 50 + return this.cachedResult; 51 + } 52 + 53 + const memberBalances = new Map<string, number>(); 54 + let totalExpenses = 0; 55 + 56 + // Process each expense 57 + for (const expense of this.expenses.values()) { 58 + totalExpenses += expense.amount; 59 + 60 + // Initialize balances for all participants and payer 61 + const allMembers = new Set([expense.paidBy, ...expense.participants]); 62 + for (const member of allMembers) { 63 + if (!memberBalances.has(member)) { 64 + memberBalances.set(member, 0); 65 + } 66 + } 67 + 68 + // Add the amount paid to the payer's balance 69 + const currentPaidBalance = memberBalances.get(expense.paidBy) || 0; 70 + memberBalances.set(expense.paidBy, currentPaidBalance + expense.amount); 71 + 72 + // Calculate and subtract each participant's share 73 + const shares = this.calculateShares(expense); 74 + for (const [memberId, share] of shares) { 75 + const currentBalance = memberBalances.get(memberId) || 0; 76 + memberBalances.set(memberId, currentBalance - share); 77 + } 78 + } 79 + 80 + // Convert to result format 81 + const balances: Balance[] = Array.from(memberBalances.entries()) 82 + .map(([memberId, amount]) => ({ 83 + memberId, 84 + amount: Math.round(amount * 100) / 100 // Round to 2 decimal places 85 + })) 86 + .sort((a, b) => a.memberId.localeCompare(b.memberId)); 87 + 88 + this.cachedResult = { 89 + balances, 90 + totalExpenses: Math.round(totalExpenses * 100) / 100, 91 + lastCalculated: new Date() 92 + }; 93 + 94 + this.isDirty = false; 95 + return this.cachedResult; 96 + } 97 + 98 + private calculateShares(expense: Expense): Map<string, number> { 99 + const shares = new Map<string, number>(); 100 + 101 + switch (expense.splitStrategy) { 102 + case 'equal': 103 + const equalShare = expense.amount / expense.participants.length; 104 + for (const participant of expense.participants) { 105 + shares.set(participant, equalShare); 106 + } 107 + break; 108 + 109 + case 'exact': 110 + if (!expense.splits) { 111 + throw new Error(`Expense ${expense.id} uses exact split but has no splits defined`); 112 + } 113 + let exactTotal = 0; 114 + for (const participant of expense.participants) { 115 + const share = expense.splits[participant]; 116 + if (share === undefined) { 117 + throw new Error(`Expense ${expense.id} missing exact split for participant ${participant}`); 118 + } 119 + shares.set(participant, share); 120 + exactTotal += share; 121 + } 122 + if (Math.abs(exactTotal - expense.amount) > 0.01) { 123 + throw new Error(`Expense ${expense.id} exact splits (${exactTotal}) don't match amount (${expense.amount})`); 124 + } 125 + break; 126 + 127 + case 'percentage': 128 + if (!expense.splits) { 129 + throw new Error(`Expense ${expense.id} uses percentage split but has no splits defined`); 130 + } 131 + let percentageTotal = 0; 132 + for (const participant of expense.participants) { 133 + const percentage = expense.splits[participant]; 134 + if (percentage === undefined) { 135 + throw new Error(`Expense ${expense.id} missing percentage split for participant ${participant}`); 136 + } 137 + const share = (expense.amount * percentage) / 100; 138 + shares.set(participant, share); 139 + percentageTotal += percentage; 140 + } 141 + if (Math.abs(percentageTotal - 100) > 0.01) { 142 + throw new Error(`Expense ${expense.id} percentages don't sum to 100% (got ${percentageTotal}%)`); 143 + } 144 + break; 145 + 146 + default: 147 + throw new Error(`Unknown split strategy: ${expense.splitStrategy}`); 148 + } 149 + 150 + return shares; 151 + } 152 + 153 + private invalidateCache(): void { 154 + this.isDirty = true; 155 + } 156 + 157 + getExpenseCount(): number { 158 + return this.expenses.size; 159 + } 160 + 161 + hasExpense(expenseId: string): boolean { 162 + return this.expenses.has(expenseId); 163 + } 164 + 165 + clear(): void { 166 + this.expenses.clear(); 167 + this.invalidateCache(); 168 + } 169 + } 170 + 171 + export function createBalanceCalculator(): BalanceCalculator { 172 + return new BalanceCalculator(); 173 + } 174 + 175 + export function calculateBalancesFromExpenses(expenses: Expense[]): BalanceCalculationResult { 176 + const calculator = createBalanceCalculator(); 177 + 178 + for (const expense of expenses) { 179 + calculator.addExpense(expense); 180 + } 181 + 182 + return calculator.calculateBalances(); 183 + } 184 + 185 + export function validateBalanceInvariants(result1: BalanceCalculationResult, result2: BalanceCalculationResult): boolean { 186 + if (result1.balances.length !== result2.balances.length) { 187 + return false; 188 + } 189 + 190 + if (Math.abs(result1.totalExpenses - result2.totalExpenses) > 0.01) { 191 + return false; 192 + } 193 + 194 + const balances1 = new Map(result1.balances.map(b => [b.memberId, b.amount])); 195 + const balances2 = new Map(result2.balances.map(b => [b.memberId, b.amount])); 196 + 197 + for (const [memberId, amount1] of balances1) { 198 + const amount2 = balances2.get(memberId); 199 + if (amount2 === undefined || Math.abs(amount1 - amount2) > 0.01) { 200 + return false; 201 + } 202 + } 203 + 204 + return true; 205 + } 206 + 207 + /** @internal Phoenix VCS traceability — do not remove. */ 208 + export const _phoenix = { 209 + iu_id: 'e443e41a77db31335cdc72e416297b42a40385bbfafcb1bff58c7a51688d6927', 210 + name: 'Balance Calculation', 211 + risk_tier: 'high', 212 + canon_ids: [3 as const], 213 + } as const;
+137
examples/settle-up/src/generated/expenses/creating-an-expense.ts
··· 1 + import { randomUUID } from 'node:crypto'; 2 + 3 + export interface Member { 4 + id: string; 5 + name: string; 6 + } 7 + 8 + export interface Group { 9 + id: string; 10 + name: string; 11 + members: Member[]; 12 + } 13 + 14 + export interface Expense { 15 + id: string; 16 + description: string; 17 + amount: number; 18 + date: Date; 19 + payerId: string; 20 + participantIds: string[]; 21 + groupId: string; 22 + } 23 + 24 + export interface CreateExpenseRequest { 25 + description: string; 26 + amount: number; 27 + date?: Date; 28 + payerId: string; 29 + participantIds: string[]; 30 + groupId: string; 31 + } 32 + 33 + export class ExpenseCreationError extends Error { 34 + constructor(message: string) { 35 + super(message); 36 + this.name = 'ExpenseCreationError'; 37 + } 38 + } 39 + 40 + export class ExpenseCreator { 41 + private groups: Map<string, Group> = new Map(); 42 + 43 + addGroup(group: Group): void { 44 + this.groups.set(group.id, group); 45 + } 46 + 47 + removeGroup(groupId: string): void { 48 + this.groups.delete(groupId); 49 + } 50 + 51 + getGroup(groupId: string): Group | undefined { 52 + return this.groups.get(groupId); 53 + } 54 + 55 + createExpense(request: CreateExpenseRequest): Expense { 56 + this.validateExpenseRequest(request); 57 + 58 + const expense: Expense = { 59 + id: randomUUID(), 60 + description: request.description.trim(), 61 + amount: this.normalizeAmount(request.amount), 62 + date: request.date || new Date(), 63 + payerId: request.payerId, 64 + participantIds: [...new Set(request.participantIds)], // Remove duplicates 65 + groupId: request.groupId, 66 + }; 67 + 68 + return expense; 69 + } 70 + 71 + private validateExpenseRequest(request: CreateExpenseRequest): void { 72 + if (!request.description || request.description.trim().length === 0) { 73 + throw new ExpenseCreationError('Expense description is required'); 74 + } 75 + 76 + if (!this.isValidAmount(request.amount)) { 77 + throw new ExpenseCreationError('Expense amount must be a positive number with at most two decimal places'); 78 + } 79 + 80 + if (!request.groupId) { 81 + throw new ExpenseCreationError('Group identifier is required'); 82 + } 83 + 84 + const group = this.groups.get(request.groupId); 85 + if (!group) { 86 + throw new ExpenseCreationError('Specified group does not exist'); 87 + } 88 + 89 + if (!request.payerId) { 90 + throw new ExpenseCreationError('Payer identifier is required'); 91 + } 92 + 93 + if (!this.isMemberOfGroup(request.payerId, group)) { 94 + throw new ExpenseCreationError('Payer must be a member of the group'); 95 + } 96 + 97 + if (!request.participantIds || request.participantIds.length === 0) { 98 + throw new ExpenseCreationError('At least one participant is required'); 99 + } 100 + 101 + for (const participantId of request.participantIds) { 102 + if (!this.isMemberOfGroup(participantId, group)) { 103 + throw new ExpenseCreationError(`Participant ${participantId} is not a member of the group`); 104 + } 105 + } 106 + } 107 + 108 + private isValidAmount(amount: number): boolean { 109 + if (typeof amount !== 'number' || isNaN(amount) || amount <= 0) { 110 + return false; 111 + } 112 + 113 + // Check for at most two decimal places 114 + const decimalPlaces = (amount.toString().split('.')[1] || '').length; 115 + return decimalPlaces <= 2; 116 + } 117 + 118 + private normalizeAmount(amount: number): number { 119 + return Math.round(amount * 100) / 100; 120 + } 121 + 122 + private isMemberOfGroup(memberId: string, group: Group): boolean { 123 + return group.members.some(member => member.id === memberId); 124 + } 125 + } 126 + 127 + export function createExpenseCreator(): ExpenseCreator { 128 + return new ExpenseCreator(); 129 + } 130 + 131 + /** @internal Phoenix VCS traceability — do not remove. */ 132 + export const _phoenix = { 133 + iu_id: '60c1351d6276e69e296bd2b41f38faf4722726598340b1f380f51af67af156cf', 134 + name: 'Creating an Expense', 135 + risk_tier: 'high', 136 + canon_ids: [7 as const], 137 + } as const;
+164
examples/settle-up/src/generated/expenses/expense-history.ts
··· 1 + export interface Expense { 2 + id: string; 3 + groupId: string; 4 + description: string; 5 + amount: number; 6 + payerId: string; 7 + participantIds: string[]; 8 + splitStrategy: 'equal' | 'exact' | 'percentage'; 9 + splitDetails?: Record<string, number>; 10 + createdBy: string; 11 + createdAt: Date; 12 + updatedAt: Date; 13 + } 14 + 15 + export interface ExpenseFilter { 16 + payerId?: string; 17 + participantId?: string; 18 + startDate?: Date; 19 + endDate?: Date; 20 + } 21 + 22 + export interface ExpenseHistoryEntry { 23 + expense: Expense; 24 + balanceChanges: Record<string, number>; 25 + } 26 + 27 + export class ExpenseHistory { 28 + private expenses = new Map<string, Expense>(); 29 + private groupExpenses = new Map<string, Set<string>>(); 30 + private balanceChanges = new Map<string, Record<string, number>>(); 31 + 32 + addExpense(expense: Expense, balanceChanges: Record<string, number>): void { 33 + if (!expense.id || !expense.groupId) { 34 + throw new Error('Expense must have valid id and groupId'); 35 + } 36 + 37 + this.expenses.set(expense.id, { ...expense }); 38 + this.balanceChanges.set(expense.id, { ...balanceChanges }); 39 + 40 + if (!this.groupExpenses.has(expense.groupId)) { 41 + this.groupExpenses.set(expense.groupId, new Set()); 42 + } 43 + this.groupExpenses.get(expense.groupId)!.add(expense.id); 44 + } 45 + 46 + getExpenseHistory(groupId: string, filter?: ExpenseFilter): ExpenseHistoryEntry[] { 47 + const expenseIds = this.groupExpenses.get(groupId) || new Set(); 48 + const expenses: ExpenseHistoryEntry[] = []; 49 + 50 + for (const expenseId of expenseIds) { 51 + const expense = this.expenses.get(expenseId); 52 + const balanceChanges = this.balanceChanges.get(expenseId); 53 + 54 + if (!expense || !balanceChanges) continue; 55 + 56 + if (this.matchesFilter(expense, filter)) { 57 + expenses.push({ 58 + expense: { ...expense }, 59 + balanceChanges: { ...balanceChanges } 60 + }); 61 + } 62 + } 63 + 64 + return expenses.sort((a, b) => b.expense.createdAt.getTime() - a.expense.createdAt.getTime()); 65 + } 66 + 67 + deleteExpense(expenseId: string): { expense: Expense; reversalChanges: Record<string, number> } | null { 68 + const expense = this.expenses.get(expenseId); 69 + const originalChanges = this.balanceChanges.get(expenseId); 70 + 71 + if (!expense || !originalChanges) { 72 + return null; 73 + } 74 + 75 + // Remove from storage 76 + this.expenses.delete(expenseId); 77 + this.balanceChanges.delete(expenseId); 78 + 79 + // Remove from group index 80 + const groupExpenses = this.groupExpenses.get(expense.groupId); 81 + if (groupExpenses) { 82 + groupExpenses.delete(expenseId); 83 + if (groupExpenses.size === 0) { 84 + this.groupExpenses.delete(expense.groupId); 85 + } 86 + } 87 + 88 + // Calculate reversal changes (opposite of original) 89 + const reversalChanges: Record<string, number> = {}; 90 + for (const [memberId, change] of Object.entries(originalChanges)) { 91 + reversalChanges[memberId] = -change; 92 + } 93 + 94 + return { 95 + expense: { ...expense }, 96 + reversalChanges 97 + }; 98 + } 99 + 100 + getExpenseById(expenseId: string): Expense | null { 101 + const expense = this.expenses.get(expenseId); 102 + return expense ? { ...expense } : null; 103 + } 104 + 105 + getExpensesByPayer(groupId: string, payerId: string): ExpenseHistoryEntry[] { 106 + return this.getExpenseHistory(groupId, { payerId }); 107 + } 108 + 109 + getExpensesByParticipant(groupId: string, participantId: string): ExpenseHistoryEntry[] { 110 + return this.getExpenseHistory(groupId, { participantId }); 111 + } 112 + 113 + getExpensesByDateRange(groupId: string, startDate: Date, endDate: Date): ExpenseHistoryEntry[] { 114 + return this.getExpenseHistory(groupId, { startDate, endDate }); 115 + } 116 + 117 + private matchesFilter(expense: Expense, filter?: ExpenseFilter): boolean { 118 + if (!filter) return true; 119 + 120 + if (filter.payerId && expense.payerId !== filter.payerId) { 121 + return false; 122 + } 123 + 124 + if (filter.participantId && !expense.participantIds.includes(filter.participantId)) { 125 + return false; 126 + } 127 + 128 + if (filter.startDate && expense.createdAt < filter.startDate) { 129 + return false; 130 + } 131 + 132 + if (filter.endDate && expense.createdAt > filter.endDate) { 133 + return false; 134 + } 135 + 136 + return true; 137 + } 138 + 139 + getAllExpenses(groupId: string): Expense[] { 140 + const expenseIds = this.groupExpenses.get(groupId) || new Set(); 141 + const expenses: Expense[] = []; 142 + 143 + for (const expenseId of expenseIds) { 144 + const expense = this.expenses.get(expenseId); 145 + if (expense) { 146 + expenses.push({ ...expense }); 147 + } 148 + } 149 + 150 + return expenses.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); 151 + } 152 + 153 + getExpenseCount(groupId: string): number { 154 + return this.groupExpenses.get(groupId)?.size || 0; 155 + } 156 + } 157 + 158 + /** @internal Phoenix VCS traceability — do not remove. */ 159 + export const _phoenix = { 160 + iu_id: '45d7e97e8e17b3b07632284b5d7580f5858cbc9a256ace230c192c73f8587c12', 161 + name: 'Expense History', 162 + risk_tier: 'medium', 163 + canon_ids: [4 as const], 164 + } as const;
+11
examples/settle-up/src/generated/expenses/index.ts
··· 1 + /** 2 + * Expenses 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Barrel export for all Expenses modules. 6 + */ 7 + 8 + export * as balanceCalculation from './balance-calculation.js'; 9 + export * as creatingAnExpense from './creating-an-expense.js'; 10 + export * as expenseHistory from './expense-history.js'; 11 + export * as splitStrategies from './split-strategies.js';
+125
examples/settle-up/src/generated/expenses/server.ts
··· 1 + /** 2 + * Expenses — HTTP Server 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Provides health check, metrics, and module endpoints. 6 + */ 7 + 8 + import { createServer, IncomingMessage, ServerResponse } from 'node:http'; 9 + 10 + import * as balanceCalculation from './balance-calculation.js'; 11 + import * as creatingAnExpense from './creating-an-expense.js'; 12 + import * as expenseHistory from './expense-history.js'; 13 + import * as splitStrategies from './split-strategies.js'; 14 + 15 + // ─── Metrics ───────────────────────────────────────────────────────────────── 16 + 17 + const _svcMetrics = { 18 + requests_total: 0, 19 + requests_by_path: {} as Record<string, number>, 20 + errors_total: 0, 21 + uptime_start: Date.now(), 22 + }; 23 + 24 + // ─── Module Registry ───────────────────────────────────────────────────────── 25 + 26 + const _svcModules = { 27 + 'balance-calculation': balanceCalculation, 28 + 'creating-an-expense': creatingAnExpense, 29 + 'expense-history': expenseHistory, 30 + 'split-strategies': splitStrategies, 31 + }; 32 + 33 + // ─── Router ────────────────────────────────────────────────────────────────── 34 + 35 + type Handler = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>; 36 + 37 + const routes: Record<string, Handler> = { 38 + '/health': (_req, res) => { 39 + res.writeHead(200, { 'Content-Type': 'application/json' }); 40 + res.end(JSON.stringify({ 41 + status: 'ok', 42 + service: 'Expenses', 43 + uptime: Math.floor((Date.now() - _svcMetrics.uptime_start) / 1000), 44 + modules: Object.keys(_svcModules), 45 + })); 46 + }, 47 + 48 + '/metrics': (_req, res) => { 49 + res.writeHead(200, { 'Content-Type': 'application/json' }); 50 + res.end(JSON.stringify({ 51 + ..._svcMetrics, 52 + uptime_seconds: Math.floor((Date.now() - _svcMetrics.uptime_start) / 1000), 53 + }, null, 2)); 54 + }, 55 + 56 + '/modules': (_req, res) => { 57 + const info = Object.entries(_svcModules).map(([name, mod]) => { 58 + const phoenix = (mod as Record<string, unknown>)._phoenix as Record<string, unknown> | undefined; 59 + return { 60 + name, 61 + risk_tier: phoenix?.risk_tier ?? 'unknown', 62 + exports: Object.keys(mod).filter(k => k !== '_phoenix'), 63 + }; 64 + }); 65 + res.writeHead(200, { 'Content-Type': 'application/json' }); 66 + res.end(JSON.stringify(info, null, 2)); 67 + }, 68 + }; 69 + 70 + // ─── Server ────────────────────────────────────────────────────────────────── 71 + 72 + function handleRequest(req: IncomingMessage, res: ServerResponse): void { 73 + const url = req.url ?? '/'; 74 + const path = url.split('?')[0]; 75 + 76 + _svcMetrics.requests_total++; 77 + _svcMetrics.requests_by_path[path] = (_svcMetrics.requests_by_path[path] ?? 0) + 1; 78 + 79 + const handler = routes[path]; 80 + if (handler) { 81 + try { 82 + handler(req, res); 83 + } catch (err) { 84 + _svcMetrics.errors_total++; 85 + res.writeHead(500, { 'Content-Type': 'application/json' }); 86 + res.end(JSON.stringify({ error: String(err) })); 87 + } 88 + } else { 89 + res.writeHead(404, { 'Content-Type': 'application/json' }); 90 + res.end(JSON.stringify({ 91 + error: 'Not Found', 92 + path, 93 + available: Object.keys(routes), 94 + })); 95 + } 96 + } 97 + 98 + export function startServer(port?: number): { server: ReturnType<typeof createServer>; port: number; ready: Promise<void> } { 99 + const requestedPort = port ?? parseInt(process.env.EXPENSES_PORT ?? process.env.PORT ?? '3001', 10); 100 + const server = createServer(handleRequest); 101 + let actualPort = requestedPort; 102 + 103 + const ready = new Promise<void>(resolve => { 104 + server.listen(requestedPort, () => { 105 + const addr = server.address(); 106 + if (addr && typeof addr === 'object') actualPort = addr.port; 107 + result.port = actualPort; 108 + console.log(`Expenses listening on http://localhost:${actualPort}`); 109 + console.log(` /health — health check`); 110 + console.log(` /metrics — request metrics`); 111 + console.log(` /modules — registered modules`); 112 + resolve(); 113 + }); 114 + }); 115 + 116 + const result = { server, port: actualPort, ready }; 117 + return result; 118 + } 119 + 120 + // Start when run directly 121 + const isMain = process.argv[1]?.endsWith('/expenses/server.js') || 122 + process.argv[1]?.endsWith('/expenses/server.ts'); 123 + if (isMain) { 124 + startServer(); 125 + }
+191
examples/settle-up/src/generated/expenses/split-strategies.ts
··· 1 + export interface Participant { 2 + id: string; 3 + name: string; 4 + } 5 + 6 + export interface SplitShare { 7 + participantId: string; 8 + amount: number; 9 + percentage: number; 10 + } 11 + 12 + export interface SplitResult { 13 + shares: SplitShare[]; 14 + totalAmount: number; 15 + remainderAssignedTo?: string; 16 + } 17 + 18 + export interface PercentageSplit { 19 + participantId: string; 20 + percentage: number; 21 + } 22 + 23 + export class SplitValidationError extends Error { 24 + constructor(message: string) { 25 + super(message); 26 + this.name = 'SplitValidationError'; 27 + } 28 + } 29 + 30 + export class SplitStrategies { 31 + /** 32 + * Splits an expense by custom percentages that must sum to 100%. 33 + */ 34 + static splitByPercentages( 35 + totalAmount: number, 36 + percentageSplits: PercentageSplit[], 37 + payerId: string 38 + ): SplitResult { 39 + if (totalAmount <= 0) { 40 + throw new SplitValidationError('Total amount must be positive'); 41 + } 42 + 43 + if (percentageSplits.length === 0) { 44 + throw new SplitValidationError('At least one participant is required'); 45 + } 46 + 47 + // Validate percentages sum to 100 48 + const totalPercentage = percentageSplits.reduce((sum, split) => sum + split.percentage, 0); 49 + if (Math.abs(totalPercentage - 100) > 0.01) { 50 + throw new SplitValidationError(`Percentages must sum to 100%, got ${totalPercentage}%`); 51 + } 52 + 53 + // Validate all percentages are non-negative 54 + for (const split of percentageSplits) { 55 + if (split.percentage < 0) { 56 + throw new SplitValidationError(`Percentage cannot be negative: ${split.percentage}%`); 57 + } 58 + } 59 + 60 + // Validate payer is included in splits 61 + const payerIncluded = percentageSplits.some(split => split.participantId === payerId); 62 + if (!payerIncluded) { 63 + throw new SplitValidationError('Payer must be included in the split'); 64 + } 65 + 66 + // Calculate shares with proper rounding 67 + const shares: SplitShare[] = []; 68 + let allocatedAmount = 0; 69 + 70 + for (const split of percentageSplits) { 71 + const calculatedAmount = Math.round((totalAmount * split.percentage) / 100); 72 + shares.push({ 73 + participantId: split.participantId, 74 + amount: calculatedAmount, 75 + percentage: split.percentage 76 + }); 77 + allocatedAmount += calculatedAmount; 78 + } 79 + 80 + // Handle remainder by assigning to payer 81 + const remainder = totalAmount - allocatedAmount; 82 + let remainderAssignedTo: string | undefined; 83 + 84 + if (remainder !== 0) { 85 + const payerShare = shares.find(share => share.participantId === payerId); 86 + if (payerShare) { 87 + payerShare.amount += remainder; 88 + remainderAssignedTo = payerId; 89 + } 90 + } 91 + 92 + // Verify invariant: sum equals total 93 + const finalSum = shares.reduce((sum, share) => sum + share.amount, 0); 94 + if (finalSum !== totalAmount) { 95 + throw new SplitValidationError(`Split calculation error: sum ${finalSum} does not equal total ${totalAmount}`); 96 + } 97 + 98 + return { 99 + shares, 100 + totalAmount, 101 + remainderAssignedTo 102 + }; 103 + } 104 + 105 + /** 106 + * Splits an expense equally among all participants. 107 + */ 108 + static splitEqually( 109 + totalAmount: number, 110 + participantIds: string[], 111 + payerId: string 112 + ): SplitResult { 113 + if (totalAmount <= 0) { 114 + throw new SplitValidationError('Total amount must be positive'); 115 + } 116 + 117 + if (participantIds.length === 0) { 118 + throw new SplitValidationError('At least one participant is required'); 119 + } 120 + 121 + // Validate payer is included 122 + if (!participantIds.includes(payerId)) { 123 + throw new SplitValidationError('Payer must be included in participants'); 124 + } 125 + 126 + // Create equal percentage splits 127 + const equalPercentage = 100 / participantIds.length; 128 + const percentageSplits: PercentageSplit[] = participantIds.map(id => ({ 129 + participantId: id, 130 + percentage: equalPercentage 131 + })); 132 + 133 + return this.splitByPercentages(totalAmount, percentageSplits, payerId); 134 + } 135 + 136 + /** 137 + * Validates that a split result maintains the required invariants. 138 + */ 139 + static validateSplitResult(result: SplitResult): boolean { 140 + // Check that shares sum to total amount 141 + const sum = result.shares.reduce((total, share) => total + share.amount, 0); 142 + if (sum !== result.totalAmount) { 143 + return false; 144 + } 145 + 146 + // Check that all amounts are non-negative 147 + for (const share of result.shares) { 148 + if (share.amount < 0) { 149 + return false; 150 + } 151 + } 152 + 153 + // Check that percentages are valid 154 + for (const share of result.shares) { 155 + if (share.percentage < 0 || share.percentage > 100) { 156 + return false; 157 + } 158 + } 159 + 160 + return true; 161 + } 162 + 163 + /** 164 + * Recalculates a split when the total amount changes. 165 + */ 166 + static recalculateSplit( 167 + originalResult: SplitResult, 168 + newTotalAmount: number, 169 + payerId: string 170 + ): SplitResult { 171 + if (newTotalAmount <= 0) { 172 + throw new SplitValidationError('New total amount must be positive'); 173 + } 174 + 175 + // Extract percentage splits from original result 176 + const percentageSplits: PercentageSplit[] = originalResult.shares.map(share => ({ 177 + participantId: share.participantId, 178 + percentage: share.percentage 179 + })); 180 + 181 + return this.splitByPercentages(newTotalAmount, percentageSplits, payerId); 182 + } 183 + } 184 + 185 + /** @internal Phoenix VCS traceability — do not remove. */ 186 + export const _phoenix = { 187 + iu_id: '99cbd78cfcb01ca2b8d67575834a6adec408c399d090c91868253f266e726337', 188 + name: 'Split Strategies', 189 + risk_tier: 'high', 190 + canon_ids: [3 as const], 191 + } as const;
+78
examples/settle-up/src/generated/groups/__tests__/groups.test.ts
··· 1 + /** 2 + * Groups — Generated Tests 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Tests module structure, server health, and Phoenix traceability. 6 + */ 7 + 8 + import { describe, it, expect, afterAll } from 'vitest'; 9 + import { startServer } from '../server.js'; 10 + 11 + import * as groupManagement from '../group-management.js'; 12 + import * as groupSummary from '../group-summary.js'; 13 + 14 + describe('Groups modules', () => { 15 + describe('Group Management', () => { 16 + it('exports Phoenix traceability metadata', () => { 17 + expect(groupManagement._phoenix).toBeDefined(); 18 + expect(groupManagement._phoenix.name).toBe('Group Management'); 19 + expect(groupManagement._phoenix.risk_tier).toBeTruthy(); 20 + }); 21 + 22 + it('has exported functions', () => { 23 + const exports = Object.keys(groupManagement).filter(k => k !== '_phoenix'); 24 + expect(exports.length).toBeGreaterThan(0); 25 + }); 26 + }); 27 + 28 + describe('Group Summary', () => { 29 + it('exports Phoenix traceability metadata', () => { 30 + expect(groupSummary._phoenix).toBeDefined(); 31 + expect(groupSummary._phoenix.name).toBe('Group Summary'); 32 + expect(groupSummary._phoenix.risk_tier).toBeTruthy(); 33 + }); 34 + 35 + it('has exported functions', () => { 36 + const exports = Object.keys(groupSummary).filter(k => k !== '_phoenix'); 37 + expect(exports.length).toBeGreaterThan(0); 38 + }); 39 + }); 40 + 41 + }); 42 + 43 + describe('Groups server', () => { 44 + const instance = startServer(0); // random port 45 + 46 + afterAll(() => new Promise<void>(resolve => instance.server.close(() => resolve()))); 47 + 48 + it('GET /health returns 200', async () => { 49 + await instance.ready; 50 + const res = await fetch(`http://localhost:${instance.port}/health`); 51 + expect(res.status).toBe(200); 52 + const body = await res.json() as Record<string, unknown>; 53 + expect(body.status).toBe('ok'); 54 + expect(body.service).toBe('Groups'); 55 + }); 56 + 57 + it('GET /metrics returns request counts', async () => { 58 + await instance.ready; 59 + const res = await fetch(`http://localhost:${instance.port}/metrics`); 60 + expect(res.status).toBe(200); 61 + const body = await res.json() as Record<string, unknown>; 62 + expect(typeof body.requests_total).toBe('number'); 63 + }); 64 + 65 + it('GET /modules lists all registered modules', async () => { 66 + await instance.ready; 67 + const res = await fetch(`http://localhost:${instance.port}/modules`); 68 + expect(res.status).toBe(200); 69 + const body = await res.json() as Array<Record<string, unknown>>; 70 + expect(body.length).toBe(2); 71 + }); 72 + 73 + it('GET /unknown returns 404', async () => { 74 + await instance.ready; 75 + const res = await fetch(`http://localhost:${instance.port}/unknown`); 76 + expect(res.status).toBe(404); 77 + }); 78 + });
+201
examples/settle-up/src/generated/groups/group-management.ts
··· 1 + import { randomUUID } from 'node:crypto'; 2 + 3 + export interface Group { 4 + id: string; 5 + name: string; 6 + currencyCode: string; 7 + createdAt: Date; 8 + memberIds: Set<string>; 9 + } 10 + 11 + export interface Member { 12 + id: string; 13 + displayName: string; 14 + email: string; 15 + } 16 + 17 + export interface MemberBalance { 18 + memberId: string; 19 + balance: number; 20 + } 21 + 22 + export class GroupManagementError extends Error { 23 + constructor(message: string) { 24 + super(message); 25 + this.name = 'GroupManagementError'; 26 + } 27 + } 28 + 29 + export class GroupManager { 30 + private groups = new Map<string, Group>(); 31 + private members = new Map<string, Member>(); 32 + private balances = new Map<string, Map<string, number>>(); 33 + 34 + createGroup(name: string, currencyCode: string): string { 35 + if (!name.trim()) { 36 + throw new GroupManagementError('Group name cannot be empty'); 37 + } 38 + if (!currencyCode.trim()) { 39 + throw new GroupManagementError('Currency code cannot be empty'); 40 + } 41 + 42 + const groupId = randomUUID(); 43 + const group: Group = { 44 + id: groupId, 45 + name: name.trim(), 46 + currencyCode: currencyCode.trim().toUpperCase(), 47 + createdAt: new Date(), 48 + memberIds: new Set(), 49 + }; 50 + 51 + this.groups.set(groupId, group); 52 + this.balances.set(groupId, new Map()); 53 + return groupId; 54 + } 55 + 56 + createMember(displayName: string, email: string): string { 57 + if (!displayName.trim()) { 58 + throw new GroupManagementError('Display name cannot be empty'); 59 + } 60 + if (!email.trim()) { 61 + throw new GroupManagementError('Email cannot be empty'); 62 + } 63 + if (!this.isValidEmail(email)) { 64 + throw new GroupManagementError('Invalid email format'); 65 + } 66 + 67 + const memberId = randomUUID(); 68 + const member: Member = { 69 + id: memberId, 70 + displayName: displayName.trim(), 71 + email: email.trim().toLowerCase(), 72 + }; 73 + 74 + this.members.set(memberId, member); 75 + return memberId; 76 + } 77 + 78 + addMemberToGroup(groupId: string, memberId: string): void { 79 + const group = this.groups.get(groupId); 80 + if (!group) { 81 + throw new GroupManagementError('Group not found'); 82 + } 83 + 84 + const member = this.members.get(memberId); 85 + if (!member) { 86 + throw new GroupManagementError('Member not found'); 87 + } 88 + 89 + if (group.memberIds.has(memberId)) { 90 + throw new GroupManagementError('Member is already in the group'); 91 + } 92 + 93 + group.memberIds.add(memberId); 94 + const groupBalances = this.balances.get(groupId)!; 95 + groupBalances.set(memberId, 0); 96 + } 97 + 98 + removeMemberFromGroup(groupId: string, memberId: string): void { 99 + const group = this.groups.get(groupId); 100 + if (!group) { 101 + throw new GroupManagementError('Group not found'); 102 + } 103 + 104 + if (!group.memberIds.has(memberId)) { 105 + throw new GroupManagementError('Member is not in the group'); 106 + } 107 + 108 + if (group.memberIds.size <= 1) { 109 + throw new GroupManagementError('Cannot remove member: group must contain at least one member'); 110 + } 111 + 112 + const groupBalances = this.balances.get(groupId)!; 113 + const balance = groupBalances.get(memberId) || 0; 114 + if (balance !== 0) { 115 + throw new GroupManagementError('Cannot remove member: member has outstanding balance'); 116 + } 117 + 118 + group.memberIds.delete(memberId); 119 + groupBalances.delete(memberId); 120 + } 121 + 122 + getGroup(groupId: string): Group | undefined { 123 + const group = this.groups.get(groupId); 124 + if (!group) return undefined; 125 + 126 + return { 127 + ...group, 128 + memberIds: new Set(group.memberIds), 129 + }; 130 + } 131 + 132 + getMember(memberId: string): Member | undefined { 133 + const member = this.members.get(memberId); 134 + if (!member) return undefined; 135 + 136 + return { ...member }; 137 + } 138 + 139 + getGroupMembers(groupId: string): Member[] { 140 + const group = this.groups.get(groupId); 141 + if (!group) { 142 + throw new GroupManagementError('Group not found'); 143 + } 144 + 145 + return Array.from(group.memberIds) 146 + .map(memberId => this.members.get(memberId)) 147 + .filter((member): member is Member => member !== undefined); 148 + } 149 + 150 + getMemberBalance(groupId: string, memberId: string): number { 151 + const group = this.groups.get(groupId); 152 + if (!group) { 153 + throw new GroupManagementError('Group not found'); 154 + } 155 + 156 + if (!group.memberIds.has(memberId)) { 157 + throw new GroupManagementError('Member is not in the group'); 158 + } 159 + 160 + const groupBalances = this.balances.get(groupId)!; 161 + return groupBalances.get(memberId) || 0; 162 + } 163 + 164 + updateMemberBalance(groupId: string, memberId: string, balance: number): void { 165 + const group = this.groups.get(groupId); 166 + if (!group) { 167 + throw new GroupManagementError('Group not found'); 168 + } 169 + 170 + if (!group.memberIds.has(memberId)) { 171 + throw new GroupManagementError('Member is not in the group'); 172 + } 173 + 174 + const groupBalances = this.balances.get(groupId)!; 175 + groupBalances.set(memberId, balance); 176 + } 177 + 178 + getAllGroups(): Group[] { 179 + return Array.from(this.groups.values()).map(group => ({ 180 + ...group, 181 + memberIds: new Set(group.memberIds), 182 + })); 183 + } 184 + 185 + getAllMembers(): Member[] { 186 + return Array.from(this.members.values()).map(member => ({ ...member })); 187 + } 188 + 189 + private isValidEmail(email: string): boolean { 190 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 191 + return emailRegex.test(email); 192 + } 193 + } 194 + 195 + /** @internal Phoenix VCS traceability — do not remove. */ 196 + export const _phoenix = { 197 + iu_id: 'c1399ea2d34fc28a41fc692aeea9630fc0666279585518e9af4f489a6cce981a', 198 + name: 'Group Management', 199 + risk_tier: 'high', 200 + canon_ids: [5 as const], 201 + } as const;
+111
examples/settle-up/src/generated/groups/group-summary.ts
··· 1 + export interface Member { 2 + id: string; 3 + name: string; 4 + } 5 + 6 + export interface Expense { 7 + id: string; 8 + amount: number; 9 + description: string; 10 + paidBy: string; 11 + splitAmong: string[]; 12 + date: Date; 13 + } 14 + 15 + export interface MemberBalance { 16 + memberId: string; 17 + memberName: string; 18 + netBalance: number; 19 + } 20 + 21 + export interface GroupSummary { 22 + memberBalances: MemberBalance[]; 23 + totalExpenses: number; 24 + totalAmountSpent: number; 25 + } 26 + 27 + export class GroupSummaryCalculator { 28 + calculateGroupSummary(members: Member[], expenses: Expense[]): GroupSummary { 29 + const memberBalances = this.calculateMemberBalances(members, expenses); 30 + const totalExpenses = expenses.length; 31 + const totalAmountSpent = expenses.reduce((sum, expense) => sum + expense.amount, 0); 32 + 33 + // Validate invariant: net balances sum to zero 34 + const balanceSum = memberBalances.reduce((sum, balance) => sum + balance.netBalance, 0); 35 + if (Math.abs(balanceSum) > 0.01) { // Allow for floating point precision 36 + throw new Error(`Balance invariant violated: net balances sum to ${balanceSum}, expected 0`); 37 + } 38 + 39 + return { 40 + memberBalances, 41 + totalExpenses, 42 + totalAmountSpent 43 + }; 44 + } 45 + 46 + private calculateMemberBalances(members: Member[], expenses: Expense[]): MemberBalance[] { 47 + const balances = new Map<string, number>(); 48 + 49 + // Initialize all member balances to zero 50 + members.forEach(member => { 51 + balances.set(member.id, 0); 52 + }); 53 + 54 + // Calculate balances from expenses 55 + expenses.forEach(expense => { 56 + const paidBy = expense.paidBy; 57 + const splitAmong = expense.splitAmong; 58 + const sharePerMember = expense.amount / splitAmong.length; 59 + 60 + // Add the full amount to the person who paid 61 + const currentPaidBalance = balances.get(paidBy) || 0; 62 + balances.set(paidBy, currentPaidBalance + expense.amount); 63 + 64 + // Subtract each person's share 65 + splitAmong.forEach(memberId => { 66 + const currentBalance = balances.get(memberId) || 0; 67 + balances.set(memberId, currentBalance - sharePerMember); 68 + }); 69 + }); 70 + 71 + // Convert to MemberBalance array 72 + return members.map(member => ({ 73 + memberId: member.id, 74 + memberName: member.name, 75 + netBalance: Math.round((balances.get(member.id) || 0) * 100) / 100 // Round to 2 decimal places 76 + })); 77 + } 78 + } 79 + 80 + export function createGroupSummary(members: Member[], expenses: Expense[]): GroupSummary { 81 + const calculator = new GroupSummaryCalculator(); 82 + return calculator.calculateGroupSummary(members, expenses); 83 + } 84 + 85 + export function formatBalance(balance: number): string { 86 + if (balance > 0) { 87 + return `+$${balance.toFixed(2)}`; 88 + } else if (balance < 0) { 89 + return `-$${Math.abs(balance).toFixed(2)}`; 90 + } else { 91 + return '$0.00'; 92 + } 93 + } 94 + 95 + export function getBalanceDescription(balance: number): string { 96 + if (balance > 0) { 97 + return 'is owed'; 98 + } else if (balance < 0) { 99 + return 'owes'; 100 + } else { 101 + return 'is settled'; 102 + } 103 + } 104 + 105 + /** @internal Phoenix VCS traceability — do not remove. */ 106 + export const _phoenix = { 107 + iu_id: 'a02fd56f33699fed19c79270509ac1218dc35d1bdd5d205a5e7d5fe40a92a0d5', 108 + name: 'Group Summary', 109 + risk_tier: 'high', 110 + canon_ids: [3 as const], 111 + } as const;
+9
examples/settle-up/src/generated/groups/index.ts
··· 1 + /** 2 + * Groups 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Barrel export for all Groups modules. 6 + */ 7 + 8 + export * as groupManagement from './group-management.js'; 9 + export * as groupSummary from './group-summary.js';
+121
examples/settle-up/src/generated/groups/server.ts
··· 1 + /** 2 + * Groups — HTTP Server 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Provides health check, metrics, and module endpoints. 6 + */ 7 + 8 + import { createServer, IncomingMessage, ServerResponse } from 'node:http'; 9 + 10 + import * as groupManagement from './group-management.js'; 11 + import * as groupSummary from './group-summary.js'; 12 + 13 + // ─── Metrics ───────────────────────────────────────────────────────────────── 14 + 15 + const _svcMetrics = { 16 + requests_total: 0, 17 + requests_by_path: {} as Record<string, number>, 18 + errors_total: 0, 19 + uptime_start: Date.now(), 20 + }; 21 + 22 + // ─── Module Registry ───────────────────────────────────────────────────────── 23 + 24 + const _svcModules = { 25 + 'group-management': groupManagement, 26 + 'group-summary': groupSummary, 27 + }; 28 + 29 + // ─── Router ────────────────────────────────────────────────────────────────── 30 + 31 + type Handler = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>; 32 + 33 + const routes: Record<string, Handler> = { 34 + '/health': (_req, res) => { 35 + res.writeHead(200, { 'Content-Type': 'application/json' }); 36 + res.end(JSON.stringify({ 37 + status: 'ok', 38 + service: 'Groups', 39 + uptime: Math.floor((Date.now() - _svcMetrics.uptime_start) / 1000), 40 + modules: Object.keys(_svcModules), 41 + })); 42 + }, 43 + 44 + '/metrics': (_req, res) => { 45 + res.writeHead(200, { 'Content-Type': 'application/json' }); 46 + res.end(JSON.stringify({ 47 + ..._svcMetrics, 48 + uptime_seconds: Math.floor((Date.now() - _svcMetrics.uptime_start) / 1000), 49 + }, null, 2)); 50 + }, 51 + 52 + '/modules': (_req, res) => { 53 + const info = Object.entries(_svcModules).map(([name, mod]) => { 54 + const phoenix = (mod as Record<string, unknown>)._phoenix as Record<string, unknown> | undefined; 55 + return { 56 + name, 57 + risk_tier: phoenix?.risk_tier ?? 'unknown', 58 + exports: Object.keys(mod).filter(k => k !== '_phoenix'), 59 + }; 60 + }); 61 + res.writeHead(200, { 'Content-Type': 'application/json' }); 62 + res.end(JSON.stringify(info, null, 2)); 63 + }, 64 + }; 65 + 66 + // ─── Server ────────────────────────────────────────────────────────────────── 67 + 68 + function handleRequest(req: IncomingMessage, res: ServerResponse): void { 69 + const url = req.url ?? '/'; 70 + const path = url.split('?')[0]; 71 + 72 + _svcMetrics.requests_total++; 73 + _svcMetrics.requests_by_path[path] = (_svcMetrics.requests_by_path[path] ?? 0) + 1; 74 + 75 + const handler = routes[path]; 76 + if (handler) { 77 + try { 78 + handler(req, res); 79 + } catch (err) { 80 + _svcMetrics.errors_total++; 81 + res.writeHead(500, { 'Content-Type': 'application/json' }); 82 + res.end(JSON.stringify({ error: String(err) })); 83 + } 84 + } else { 85 + res.writeHead(404, { 'Content-Type': 'application/json' }); 86 + res.end(JSON.stringify({ 87 + error: 'Not Found', 88 + path, 89 + available: Object.keys(routes), 90 + })); 91 + } 92 + } 93 + 94 + export function startServer(port?: number): { server: ReturnType<typeof createServer>; port: number; ready: Promise<void> } { 95 + const requestedPort = port ?? parseInt(process.env.GROUPS_PORT ?? process.env.PORT ?? '3002', 10); 96 + const server = createServer(handleRequest); 97 + let actualPort = requestedPort; 98 + 99 + const ready = new Promise<void>(resolve => { 100 + server.listen(requestedPort, () => { 101 + const addr = server.address(); 102 + if (addr && typeof addr === 'object') actualPort = addr.port; 103 + result.port = actualPort; 104 + console.log(`Groups listening on http://localhost:${actualPort}`); 105 + console.log(` /health — health check`); 106 + console.log(` /metrics — request metrics`); 107 + console.log(` /modules — registered modules`); 108 + resolve(); 109 + }); 110 + }); 111 + 112 + const result = { server, port: actualPort, ready }; 113 + return result; 114 + } 115 + 116 + // Start when run directly 117 + const isMain = process.argv[1]?.endsWith('/groups/server.js') || 118 + process.argv[1]?.endsWith('/groups/server.ts'); 119 + if (isMain) { 120 + startServer(); 121 + }
+17
examples/settle-up/src/generated/index.ts
··· 1 + /** 2 + * Phoenix VCS — Generated Service Registry 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + */ 6 + 7 + export * as api from './api/index.js'; 8 + export * as expenses from './expenses/index.js'; 9 + export * as groups from './groups/index.js'; 10 + export * as settlements from './settlements/index.js'; 11 + 12 + export const services = [ 13 + { name: 'Api', dir: 'api', port: 3000, modules: 3 }, 14 + { name: 'Expenses', dir: 'expenses', port: 3001, modules: 4 }, 15 + { name: 'Groups', dir: 'groups', port: 3002, modules: 2 }, 16 + { name: 'Settlements', dir: 'settlements', port: 3003, modules: 3 }, 17 + ] as const;
+92
examples/settle-up/src/generated/settlements/__tests__/settlements.test.ts
··· 1 + /** 2 + * Settlements — Generated Tests 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Tests module structure, server health, and Phoenix traceability. 6 + */ 7 + 8 + import { describe, it, expect, afterAll } from 'vitest'; 9 + import { startServer } from '../server.js'; 10 + 11 + import * as debtSimplification from '../debt-simplification.js'; 12 + import * as recordingSettlements from '../recording-settlements.js'; 13 + import * as settlementStatus from '../settlement-status.js'; 14 + 15 + describe('Settlements modules', () => { 16 + describe('Debt Simplification', () => { 17 + it('exports Phoenix traceability metadata', () => { 18 + expect(debtSimplification._phoenix).toBeDefined(); 19 + expect(debtSimplification._phoenix.name).toBe('Debt Simplification'); 20 + expect(debtSimplification._phoenix.risk_tier).toBeTruthy(); 21 + }); 22 + 23 + it('has exported functions', () => { 24 + const exports = Object.keys(debtSimplification).filter(k => k !== '_phoenix'); 25 + expect(exports.length).toBeGreaterThan(0); 26 + }); 27 + }); 28 + 29 + describe('Recording Settlements', () => { 30 + it('exports Phoenix traceability metadata', () => { 31 + expect(recordingSettlements._phoenix).toBeDefined(); 32 + expect(recordingSettlements._phoenix.name).toBe('Recording Settlements'); 33 + expect(recordingSettlements._phoenix.risk_tier).toBeTruthy(); 34 + }); 35 + 36 + it('has exported functions', () => { 37 + const exports = Object.keys(recordingSettlements).filter(k => k !== '_phoenix'); 38 + expect(exports.length).toBeGreaterThan(0); 39 + }); 40 + }); 41 + 42 + describe('Settlement Status', () => { 43 + it('exports Phoenix traceability metadata', () => { 44 + expect(settlementStatus._phoenix).toBeDefined(); 45 + expect(settlementStatus._phoenix.name).toBe('Settlement Status'); 46 + expect(settlementStatus._phoenix.risk_tier).toBeTruthy(); 47 + }); 48 + 49 + it('has exported functions', () => { 50 + const exports = Object.keys(settlementStatus).filter(k => k !== '_phoenix'); 51 + expect(exports.length).toBeGreaterThan(0); 52 + }); 53 + }); 54 + 55 + }); 56 + 57 + describe('Settlements server', () => { 58 + const instance = startServer(0); // random port 59 + 60 + afterAll(() => new Promise<void>(resolve => instance.server.close(() => resolve()))); 61 + 62 + it('GET /health returns 200', async () => { 63 + await instance.ready; 64 + const res = await fetch(`http://localhost:${instance.port}/health`); 65 + expect(res.status).toBe(200); 66 + const body = await res.json() as Record<string, unknown>; 67 + expect(body.status).toBe('ok'); 68 + expect(body.service).toBe('Settlements'); 69 + }); 70 + 71 + it('GET /metrics returns request counts', async () => { 72 + await instance.ready; 73 + const res = await fetch(`http://localhost:${instance.port}/metrics`); 74 + expect(res.status).toBe(200); 75 + const body = await res.json() as Record<string, unknown>; 76 + expect(typeof body.requests_total).toBe('number'); 77 + }); 78 + 79 + it('GET /modules lists all registered modules', async () => { 80 + await instance.ready; 81 + const res = await fetch(`http://localhost:${instance.port}/modules`); 82 + expect(res.status).toBe(200); 83 + const body = await res.json() as Array<Record<string, unknown>>; 84 + expect(body.length).toBe(3); 85 + }); 86 + 87 + it('GET /unknown returns 404', async () => { 88 + await instance.ready; 89 + const res = await fetch(`http://localhost:${instance.port}/unknown`); 90 + expect(res.status).toBe(404); 91 + }); 92 + });
+207
examples/settle-up/src/generated/settlements/debt-simplification.ts
··· 1 + export interface Balance { 2 + memberId: string; 3 + amount: number; 4 + } 5 + 6 + export interface Payment { 7 + payer: string; 8 + payee: string; 9 + amount: number; 10 + } 11 + 12 + export interface SettlementPlan { 13 + payments: Payment[]; 14 + } 15 + 16 + export interface DebtGroup { 17 + balances: Balance[]; 18 + } 19 + 20 + export class DebtSimplifier { 21 + /** 22 + * Computes the minimum number of payments to settle all debts in a group. 23 + * Handles cycles by reducing them to net flows. 24 + * Returns empty settlement plan when all balances are zero. 25 + */ 26 + public simplifyDebts(group: DebtGroup): SettlementPlan { 27 + const balances = this.validateAndCopyBalances(group.balances); 28 + 29 + if (this.allBalancesZero(balances)) { 30 + return { payments: [] }; 31 + } 32 + 33 + const payments: Payment[] = []; 34 + const activeBalances = balances.filter(b => Math.abs(b.amount) > 0.001); 35 + 36 + while (activeBalances.length > 1) { 37 + // Find the largest creditor and debtor 38 + const creditor = this.findLargestCreditor(activeBalances); 39 + const debtor = this.findLargestDebtor(activeBalances); 40 + 41 + if (!creditor || !debtor) { 42 + break; 43 + } 44 + 45 + // Calculate payment amount (minimum of creditor's credit and debtor's debt) 46 + const paymentAmount = Math.min(creditor.amount, Math.abs(debtor.amount)); 47 + 48 + // Create payment 49 + payments.push({ 50 + payer: debtor.memberId, 51 + payee: creditor.memberId, 52 + amount: Math.round(paymentAmount * 100) / 100 // Round to 2 decimal places 53 + }); 54 + 55 + // Update balances 56 + creditor.amount -= paymentAmount; 57 + debtor.amount += paymentAmount; 58 + 59 + // Remove members with zero balance 60 + this.removeZeroBalances(activeBalances); 61 + } 62 + 63 + return { payments }; 64 + } 65 + 66 + /** 67 + * Creates a single payment when exactly two members have non-zero balances. 68 + */ 69 + public createDirectPayment(creditor: Balance, debtor: Balance): Payment { 70 + if (creditor.amount <= 0) { 71 + throw new Error('Creditor must have positive balance'); 72 + } 73 + if (debtor.amount >= 0) { 74 + throw new Error('Debtor must have negative balance'); 75 + } 76 + 77 + const paymentAmount = Math.min(creditor.amount, Math.abs(debtor.amount)); 78 + 79 + return { 80 + payer: debtor.memberId, 81 + payee: creditor.memberId, 82 + amount: Math.round(paymentAmount * 100) / 100 83 + }; 84 + } 85 + 86 + /** 87 + * Validates that the sum of all balances equals zero (conservation of money). 88 + */ 89 + public validateBalances(balances: Balance[]): void { 90 + const total = balances.reduce((sum, balance) => sum + balance.amount, 0); 91 + 92 + if (Math.abs(total) > 0.001) { 93 + throw new Error(`Balances do not sum to zero. Total: ${total}`); 94 + } 95 + 96 + const memberIds = new Set<string>(); 97 + for (const balance of balances) { 98 + if (memberIds.has(balance.memberId)) { 99 + throw new Error(`Duplicate member ID: ${balance.memberId}`); 100 + } 101 + memberIds.add(balance.memberId); 102 + } 103 + } 104 + 105 + /** 106 + * Verifies that the settlement plan produces the same net effect as individual debt payments. 107 + */ 108 + public verifySettlement(originalBalances: Balance[], plan: SettlementPlan): boolean { 109 + const netEffects = new Map<string, number>(); 110 + 111 + // Initialize with original balances 112 + for (const balance of originalBalances) { 113 + netEffects.set(balance.memberId, balance.amount); 114 + } 115 + 116 + // Apply payments 117 + for (const payment of plan.payments) { 118 + const payerBalance = netEffects.get(payment.payer) || 0; 119 + const payeeBalance = netEffects.get(payment.payee) || 0; 120 + 121 + netEffects.set(payment.payer, payerBalance - payment.amount); 122 + netEffects.set(payment.payee, payeeBalance + payment.amount); 123 + } 124 + 125 + // Check if all balances are zero (within tolerance) 126 + for (const [, balance] of netEffects) { 127 + if (Math.abs(balance) > 0.001) { 128 + return false; 129 + } 130 + } 131 + 132 + return true; 133 + } 134 + 135 + private validateAndCopyBalances(balances: Balance[]): Balance[] { 136 + this.validateBalances(balances); 137 + return balances.map(b => ({ ...b })); 138 + } 139 + 140 + private allBalancesZero(balances: Balance[]): boolean { 141 + return balances.every(b => Math.abs(b.amount) < 0.001); 142 + } 143 + 144 + private findLargestCreditor(balances: Balance[]): Balance | null { 145 + let largest: Balance | null = null; 146 + 147 + for (const balance of balances) { 148 + if (balance.amount > 0.001) { 149 + if (!largest || balance.amount > largest.amount) { 150 + largest = balance; 151 + } 152 + } 153 + } 154 + 155 + return largest; 156 + } 157 + 158 + private findLargestDebtor(balances: Balance[]): Balance | null { 159 + let largest: Balance | null = null; 160 + 161 + for (const balance of balances) { 162 + if (balance.amount < -0.001) { 163 + if (!largest || Math.abs(balance.amount) > Math.abs(largest.amount)) { 164 + largest = balance; 165 + } 166 + } 167 + } 168 + 169 + return largest; 170 + } 171 + 172 + private removeZeroBalances(balances: Balance[]): void { 173 + for (let i = balances.length - 1; i >= 0; i--) { 174 + if (Math.abs(balances[i].amount) < 0.001) { 175 + balances.splice(i, 1); 176 + } 177 + } 178 + } 179 + } 180 + 181 + export function simplifyDebts(group: DebtGroup): SettlementPlan { 182 + const simplifier = new DebtSimplifier(); 183 + return simplifier.simplifyDebts(group); 184 + } 185 + 186 + export function createDirectPayment(creditor: Balance, debtor: Balance): Payment { 187 + const simplifier = new DebtSimplifier(); 188 + return simplifier.createDirectPayment(creditor, debtor); 189 + } 190 + 191 + export function validateBalances(balances: Balance[]): void { 192 + const simplifier = new DebtSimplifier(); 193 + simplifier.validateBalances(balances); 194 + } 195 + 196 + export function verifySettlement(originalBalances: Balance[], plan: SettlementPlan): boolean { 197 + const simplifier = new DebtSimplifier(); 198 + return simplifier.verifySettlement(originalBalances, plan); 199 + } 200 + 201 + /** @internal Phoenix VCS traceability — do not remove. */ 202 + export const _phoenix = { 203 + iu_id: 'bd807bbd13956da99b7534dafef3fee0cc0996b3fb388afd9e398ada71532285', 204 + name: 'Debt Simplification', 205 + risk_tier: 'high', 206 + canon_ids: [6 as const], 207 + } as const;
+10
examples/settle-up/src/generated/settlements/index.ts
··· 1 + /** 2 + * Settlements 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Barrel export for all Settlements modules. 6 + */ 7 + 8 + export * as debtSimplification from './debt-simplification.js'; 9 + export * as recordingSettlements from './recording-settlements.js'; 10 + export * as settlementStatus from './settlement-status.js';
+153
examples/settle-up/src/generated/settlements/recording-settlements.ts
··· 1 + export interface Member { 2 + id: string; 3 + name: string; 4 + balance: number; 5 + } 6 + 7 + export interface Settlement { 8 + id: string; 9 + payerId: string; 10 + recipientId: string; 11 + amount: number; 12 + timestamp: Date; 13 + description?: string; 14 + } 15 + 16 + export interface HistoryEntry { 17 + id: string; 18 + type: 'settlement' | 'expense'; 19 + timestamp: Date; 20 + data: Settlement | any; 21 + } 22 + 23 + export class SettlementRecorder { 24 + private members: Map<string, Member> = new Map(); 25 + private settlements: Map<string, Settlement> = new Map(); 26 + private history: HistoryEntry[] = []; 27 + 28 + addMember(member: Member): void { 29 + this.members.set(member.id, { ...member }); 30 + } 31 + 32 + getMember(id: string): Member | undefined { 33 + const member = this.members.get(id); 34 + return member ? { ...member } : undefined; 35 + } 36 + 37 + getDebtAmount(payerId: string, recipientId: string): number { 38 + const payer = this.members.get(payerId); 39 + const recipient = this.members.get(recipientId); 40 + 41 + if (!payer || !recipient) { 42 + return 0; 43 + } 44 + 45 + // If payer has negative balance and recipient has positive balance, 46 + // the debt is the minimum of absolute values 47 + if (payer.balance < 0 && recipient.balance > 0) { 48 + return Math.min(Math.abs(payer.balance), recipient.balance); 49 + } 50 + 51 + return 0; 52 + } 53 + 54 + recordSettlement( 55 + payerId: string, 56 + recipientId: string, 57 + amount: number, 58 + description?: string 59 + ): Settlement { 60 + if (amount <= 0) { 61 + throw new Error('Settlement amount must be positive'); 62 + } 63 + 64 + const payer = this.members.get(payerId); 65 + const recipient = this.members.get(recipientId); 66 + 67 + if (!payer) { 68 + throw new Error(`Payer with id ${payerId} not found`); 69 + } 70 + 71 + if (!recipient) { 72 + throw new Error(`Recipient with id ${recipientId} not found`); 73 + } 74 + 75 + if (payerId === recipientId) { 76 + throw new Error('Payer and recipient cannot be the same person'); 77 + } 78 + 79 + const debtAmount = this.getDebtAmount(payerId, recipientId); 80 + 81 + if (amount > debtAmount) { 82 + throw new Error( 83 + `Settlement amount ${amount} exceeds the amount the payer owes the recipient (${debtAmount})` 84 + ); 85 + } 86 + 87 + const settlement: Settlement = { 88 + id: this.generateId(), 89 + payerId, 90 + recipientId, 91 + amount, 92 + timestamp: new Date(), 93 + description, 94 + }; 95 + 96 + // Update balances 97 + payer.balance += amount; 98 + recipient.balance -= amount; 99 + 100 + // Store settlement 101 + this.settlements.set(settlement.id, settlement); 102 + 103 + // Add to history 104 + const historyEntry: HistoryEntry = { 105 + id: this.generateId(), 106 + type: 'settlement', 107 + timestamp: settlement.timestamp, 108 + data: settlement, 109 + }; 110 + 111 + this.history.push(historyEntry); 112 + 113 + return { ...settlement }; 114 + } 115 + 116 + getSettlement(id: string): Settlement | undefined { 117 + const settlement = this.settlements.get(id); 118 + return settlement ? { ...settlement } : undefined; 119 + } 120 + 121 + getAllSettlements(): Settlement[] { 122 + return Array.from(this.settlements.values()).map(s => ({ ...s })); 123 + } 124 + 125 + getHistory(): HistoryEntry[] { 126 + return this.history.map(entry => ({ 127 + ...entry, 128 + data: { ...entry.data }, 129 + })); 130 + } 131 + 132 + getSettlementHistory(): Settlement[] { 133 + return this.history 134 + .filter(entry => entry.type === 'settlement') 135 + .map(entry => ({ ...entry.data as Settlement })); 136 + } 137 + 138 + private generateId(): string { 139 + return Math.random().toString(36).substring(2) + Date.now().toString(36); 140 + } 141 + } 142 + 143 + export function createSettlementRecorder(): SettlementRecorder { 144 + return new SettlementRecorder(); 145 + } 146 + 147 + /** @internal Phoenix VCS traceability — do not remove. */ 148 + export const _phoenix = { 149 + iu_id: 'b1fdf8ff8565aee6f30792cf338f9a51981dd9d42d62ed9054a341ee6a1bb348', 150 + name: 'Recording Settlements', 151 + risk_tier: 'low', 152 + canon_ids: [3 as const], 153 + } as const;
+123
examples/settle-up/src/generated/settlements/server.ts
··· 1 + /** 2 + * Settlements — HTTP Server 3 + * 4 + * AUTO-GENERATED by Phoenix VCS 5 + * Provides health check, metrics, and module endpoints. 6 + */ 7 + 8 + import { createServer, IncomingMessage, ServerResponse } from 'node:http'; 9 + 10 + import * as debtSimplification from './debt-simplification.js'; 11 + import * as recordingSettlements from './recording-settlements.js'; 12 + import * as settlementStatus from './settlement-status.js'; 13 + 14 + // ─── Metrics ───────────────────────────────────────────────────────────────── 15 + 16 + const _svcMetrics = { 17 + requests_total: 0, 18 + requests_by_path: {} as Record<string, number>, 19 + errors_total: 0, 20 + uptime_start: Date.now(), 21 + }; 22 + 23 + // ─── Module Registry ───────────────────────────────────────────────────────── 24 + 25 + const _svcModules = { 26 + 'debt-simplification': debtSimplification, 27 + 'recording-settlements': recordingSettlements, 28 + 'settlement-status': settlementStatus, 29 + }; 30 + 31 + // ─── Router ────────────────────────────────────────────────────────────────── 32 + 33 + type Handler = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>; 34 + 35 + const routes: Record<string, Handler> = { 36 + '/health': (_req, res) => { 37 + res.writeHead(200, { 'Content-Type': 'application/json' }); 38 + res.end(JSON.stringify({ 39 + status: 'ok', 40 + service: 'Settlements', 41 + uptime: Math.floor((Date.now() - _svcMetrics.uptime_start) / 1000), 42 + modules: Object.keys(_svcModules), 43 + })); 44 + }, 45 + 46 + '/metrics': (_req, res) => { 47 + res.writeHead(200, { 'Content-Type': 'application/json' }); 48 + res.end(JSON.stringify({ 49 + ..._svcMetrics, 50 + uptime_seconds: Math.floor((Date.now() - _svcMetrics.uptime_start) / 1000), 51 + }, null, 2)); 52 + }, 53 + 54 + '/modules': (_req, res) => { 55 + const info = Object.entries(_svcModules).map(([name, mod]) => { 56 + const phoenix = (mod as Record<string, unknown>)._phoenix as Record<string, unknown> | undefined; 57 + return { 58 + name, 59 + risk_tier: phoenix?.risk_tier ?? 'unknown', 60 + exports: Object.keys(mod).filter(k => k !== '_phoenix'), 61 + }; 62 + }); 63 + res.writeHead(200, { 'Content-Type': 'application/json' }); 64 + res.end(JSON.stringify(info, null, 2)); 65 + }, 66 + }; 67 + 68 + // ─── Server ────────────────────────────────────────────────────────────────── 69 + 70 + function handleRequest(req: IncomingMessage, res: ServerResponse): void { 71 + const url = req.url ?? '/'; 72 + const path = url.split('?')[0]; 73 + 74 + _svcMetrics.requests_total++; 75 + _svcMetrics.requests_by_path[path] = (_svcMetrics.requests_by_path[path] ?? 0) + 1; 76 + 77 + const handler = routes[path]; 78 + if (handler) { 79 + try { 80 + handler(req, res); 81 + } catch (err) { 82 + _svcMetrics.errors_total++; 83 + res.writeHead(500, { 'Content-Type': 'application/json' }); 84 + res.end(JSON.stringify({ error: String(err) })); 85 + } 86 + } else { 87 + res.writeHead(404, { 'Content-Type': 'application/json' }); 88 + res.end(JSON.stringify({ 89 + error: 'Not Found', 90 + path, 91 + available: Object.keys(routes), 92 + })); 93 + } 94 + } 95 + 96 + export function startServer(port?: number): { server: ReturnType<typeof createServer>; port: number; ready: Promise<void> } { 97 + const requestedPort = port ?? parseInt(process.env.SETTLEMENTS_PORT ?? process.env.PORT ?? '3003', 10); 98 + const server = createServer(handleRequest); 99 + let actualPort = requestedPort; 100 + 101 + const ready = new Promise<void>(resolve => { 102 + server.listen(requestedPort, () => { 103 + const addr = server.address(); 104 + if (addr && typeof addr === 'object') actualPort = addr.port; 105 + result.port = actualPort; 106 + console.log(`Settlements listening on http://localhost:${actualPort}`); 107 + console.log(` /health — health check`); 108 + console.log(` /metrics — request metrics`); 109 + console.log(` /modules — registered modules`); 110 + resolve(); 111 + }); 112 + }); 113 + 114 + const result = { server, port: actualPort, ready }; 115 + return result; 116 + } 117 + 118 + // Start when run directly 119 + const isMain = process.argv[1]?.endsWith('/settlements/server.js') || 120 + process.argv[1]?.endsWith('/settlements/server.ts'); 121 + if (isMain) { 122 + startServer(); 123 + }
+188
examples/settle-up/src/generated/settlements/settlement-status.ts
··· 1 + export interface DebtInfo { 2 + creditorId: string; 3 + debtorId: string; 4 + amount: number; 5 + } 6 + 7 + export interface MemberStatus { 8 + memberId: string; 9 + owesTo: Array<{ memberId: string; amount: number }>; 10 + owedBy: Array<{ memberId: string; amount: number }>; 11 + netBalance: number; 12 + } 13 + 14 + export interface GroupSettlementStatus { 15 + isSettled: boolean; 16 + totalOutstandingDebt: number; 17 + memberStatuses: MemberStatus[]; 18 + } 19 + 20 + export class SettlementStatusTracker { 21 + private debts: Map<string, DebtInfo[]> = new Map(); 22 + 23 + addDebt(creditorId: string, debtorId: string, amount: number): void { 24 + if (amount <= 0) { 25 + throw new Error('Debt amount must be positive'); 26 + } 27 + if (creditorId === debtorId) { 28 + throw new Error('Creditor and debtor cannot be the same person'); 29 + } 30 + 31 + const key = `${creditorId}-${debtorId}`; 32 + const existingDebts = this.debts.get(key) || []; 33 + existingDebts.push({ creditorId, debtorId, amount }); 34 + this.debts.set(key, existingDebts); 35 + } 36 + 37 + removeDebt(creditorId: string, debtorId: string, amount: number): void { 38 + const key = `${creditorId}-${debtorId}`; 39 + const existingDebts = this.debts.get(key) || []; 40 + 41 + let remainingAmount = amount; 42 + const updatedDebts = existingDebts.filter(debt => { 43 + if (remainingAmount <= 0) return true; 44 + if (debt.amount <= remainingAmount) { 45 + remainingAmount -= debt.amount; 46 + return false; 47 + } else { 48 + debt.amount -= remainingAmount; 49 + remainingAmount = 0; 50 + return true; 51 + } 52 + }); 53 + 54 + if (updatedDebts.length === 0) { 55 + this.debts.delete(key); 56 + } else { 57 + this.debts.set(key, updatedDebts); 58 + } 59 + } 60 + 61 + getNetBalances(memberIds: string[]): Map<string, number> { 62 + const balances = new Map<string, number>(); 63 + 64 + // Initialize all members with zero balance 65 + memberIds.forEach(id => balances.set(id, 0)); 66 + 67 + // Calculate net balances from all debts 68 + for (const debts of this.debts.values()) { 69 + for (const debt of debts) { 70 + const creditorBalance = balances.get(debt.creditorId) || 0; 71 + const debtorBalance = balances.get(debt.debtorId) || 0; 72 + 73 + balances.set(debt.creditorId, creditorBalance + debt.amount); 74 + balances.set(debt.debtorId, debtorBalance - debt.amount); 75 + } 76 + } 77 + 78 + return balances; 79 + } 80 + 81 + getMemberStatus(memberId: string, allMemberIds: string[]): MemberStatus { 82 + const netBalances = this.getNetBalances(allMemberIds); 83 + const owesTo: Array<{ memberId: string; amount: number }> = []; 84 + const owedBy: Array<{ memberId: string; amount: number }> = []; 85 + 86 + // Calculate what this member owes to others 87 + for (const [key, debts] of this.debts.entries()) { 88 + for (const debt of debts) { 89 + if (debt.debtorId === memberId) { 90 + const existing = owesTo.find(item => item.memberId === debt.creditorId); 91 + if (existing) { 92 + existing.amount += debt.amount; 93 + } else { 94 + owesTo.push({ memberId: debt.creditorId, amount: debt.amount }); 95 + } 96 + } 97 + if (debt.creditorId === memberId) { 98 + const existing = owedBy.find(item => item.memberId === debt.debtorId); 99 + if (existing) { 100 + existing.amount += debt.amount; 101 + } else { 102 + owedBy.push({ memberId: debt.debtorId, amount: debt.amount }); 103 + } 104 + } 105 + } 106 + } 107 + 108 + return { 109 + memberId, 110 + owesTo: owesTo.sort((a, b) => b.amount - a.amount), 111 + owedBy: owedBy.sort((a, b) => b.amount - a.amount), 112 + netBalance: netBalances.get(memberId) || 0 113 + }; 114 + } 115 + 116 + getGroupStatus(memberIds: string[]): GroupSettlementStatus { 117 + const netBalances = this.getNetBalances(memberIds); 118 + const memberStatuses = memberIds.map(id => this.getMemberStatus(id, memberIds)); 119 + 120 + // Calculate total outstanding debt (sum of all positive balances) 121 + let totalOutstandingDebt = 0; 122 + for (const balance of netBalances.values()) { 123 + if (balance > 0) { 124 + totalOutstandingDebt += balance; 125 + } 126 + } 127 + 128 + // Group is settled if all balances are zero (within floating point precision) 129 + const isSettled = Array.from(netBalances.values()).every(balance => 130 + Math.abs(balance) < 0.01 131 + ); 132 + 133 + return { 134 + isSettled, 135 + totalOutstandingDebt: Math.round(totalOutstandingDebt * 100) / 100, 136 + memberStatuses 137 + }; 138 + } 139 + 140 + clearAllDebts(): void { 141 + this.debts.clear(); 142 + } 143 + 144 + getAllDebts(): DebtInfo[] { 145 + const allDebts: DebtInfo[] = []; 146 + for (const debts of this.debts.values()) { 147 + allDebts.push(...debts); 148 + } 149 + return allDebts; 150 + } 151 + } 152 + 153 + export function createSettlementTracker(): SettlementStatusTracker { 154 + return new SettlementStatusTracker(); 155 + } 156 + 157 + export function formatDebtSummary(memberStatus: MemberStatus): string { 158 + const lines: string[] = []; 159 + 160 + if (memberStatus.owesTo.length > 0) { 161 + lines.push('You owe:'); 162 + memberStatus.owesTo.forEach(debt => { 163 + lines.push(` ${debt.memberId}: $${debt.amount.toFixed(2)}`); 164 + }); 165 + } 166 + 167 + if (memberStatus.owedBy.length > 0) { 168 + if (lines.length > 0) lines.push(''); 169 + lines.push('You are owed by:'); 170 + memberStatus.owedBy.forEach(debt => { 171 + lines.push(` ${debt.memberId}: $${debt.amount.toFixed(2)}`); 172 + }); 173 + } 174 + 175 + if (lines.length === 0) { 176 + lines.push('No outstanding debts'); 177 + } 178 + 179 + return lines.join('\n'); 180 + } 181 + 182 + /** @internal Phoenix VCS traceability — do not remove. */ 183 + export const _phoenix = { 184 + iu_id: '7b08497eefb99ff90fbe8cdf1377b42c64fbe480776ace6cd60b6a03296c3bee', 185 + name: 'Settlement Status', 186 + risk_tier: 'low', 187 + canon_ids: [3 as const], 188 + } as const;
+23
examples/settle-up/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "ESNext", 5 + "moduleResolution": "bundler", 6 + "declaration": true, 7 + "outDir": "dist", 8 + "rootDir": "src", 9 + "strict": true, 10 + "esModuleInterop": true, 11 + "skipLibCheck": true, 12 + "forceConsistentCasingInFileNames": true, 13 + "resolveJsonModule": true, 14 + "sourceMap": true 15 + }, 16 + "include": [ 17 + "src/**/*" 18 + ], 19 + "exclude": [ 20 + "node_modules", 21 + "dist" 22 + ] 23 + }
+7
examples/settle-up/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config'; 2 + 3 + export default defineConfig({ 4 + test: { 5 + include: ['src/**/__tests__/**/*.test.ts'], 6 + }, 7 + });
examples/todo-app/data/app.db

This is a binary file and will not be displayed.

examples/todo-app/data/app.db-shm

This is a binary file and will not be displayed.

examples/todo-app/data/app.db-wal

This is a binary file and will not be displayed.

-9
examples/todo-app/package.json
··· 1 1 { 2 2 "name": "todo-app", 3 3 "version": "0.1.0", 4 - "description": "Generated by Phoenix VCS — 1 services", 5 4 "type": "module", 6 - "scripts": { 7 - "build": "tsc", 8 - "typecheck": "tsc --noEmit", 9 - "test": "vitest run", 10 - "test:watch": "vitest", 11 - "dev": "tsx watch src/server.ts", 12 - "start": "tsx src/server.ts" 13 - }, 14 5 "dependencies": { 15 6 "hono": "^4.6.0", 16 7 "@hono/node-server": "^1.13.0",
+13
examples/todo-app/spec/todos.md
··· 27 27 28 28 - GET /todos/stats must return a JSON object with: total (total todo count), completed (completed count), incomplete (incomplete count), by_category (array of {category_name, count} ordered by count descending) 29 29 30 + ## Web Interface 31 + 32 + - GET / must serve a single-page HTML application with inline CSS and JavaScript 33 + - The page must display a header with the title "Todos" and a stats summary showing total, completed, and incomplete counts 34 + - The page must display a form to create new todos with a text input for title and a dropdown to select a category (populated from GET /categories) 35 + - The page must display all todos as a list, each showing the title, category name as a colored badge, and a checkbox for completed status 36 + - Clicking the checkbox must toggle the todo's completed status via PATCH /todos/:id 37 + - Each todo must have a delete button that removes it via DELETE /todos/:id 38 + - The page must display a category management section where users can add new categories with a name and color picker, and delete empty categories 39 + - The page must include filter buttons: All, Active (incomplete), Completed 40 + - The page must refresh the todo list and stats after every create, update, or delete action 41 + - The design must be clean and modern with a centered layout, max-width 640px, system-ui font, subtle shadows, and a light color scheme 42 + 30 43 ## Error Handling 31 44 32 45 - All error responses must be JSON with an "error" field
-11
examples/todo-app/src/generated/index.ts
··· 1 - /** 2 - * Phoenix VCS — Generated Service Registry 3 - * 4 - * AUTO-GENERATED by Phoenix VCS 5 - */ 6 - 7 - export * as todos from './todos/index.js'; 8 - 9 - export const services = [ 10 - { name: 'Todos', dir: 'todos', port: 3000, modules: 1 }, 11 - ] as const;
-16
examples/todo-app/src/generated/todos/__tests__/todos.test.ts
··· 1 - /** 2 - * Todos — Generated Tests 3 - * AUTO-GENERATED by Phoenix VCS 4 - */ 5 - 6 - import { describe, it, expect } from 'vitest'; 7 - import todos_resource from '../todos-resource.js'; 8 - 9 - describe('Todos modules', () => { 10 - describe('Todos Resource', () => { 11 - it('exports a Hono router as default', () => { 12 - expect(todos_resource).toBeDefined(); 13 - expect(typeof todos_resource.fetch).toBe('function'); 14 - }); 15 - }); 16 - });
+77
examples/todo-app/src/generated/todos/categories.ts
··· 1 + import { Hono } from 'hono'; 2 + import { db, registerMigration } from '../../db.js'; 3 + import { z } from 'zod'; 4 + 5 + registerMigration('categories', ` 6 + CREATE TABLE IF NOT EXISTS categories ( 7 + id INTEGER PRIMARY KEY AUTOINCREMENT, 8 + name TEXT NOT NULL UNIQUE, 9 + color TEXT NOT NULL DEFAULT '#888888', 10 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 11 + ) 12 + `); 13 + 14 + const CreateCategorySchema = z.object({ 15 + name: z.string().min(1).max(50), 16 + color: z.string().optional().default('#888888'), 17 + }); 18 + 19 + const router = new Hono(); 20 + 21 + router.get('/', (c) => { 22 + const categories = db.prepare('SELECT * FROM categories ORDER BY name').all(); 23 + return c.json(categories); 24 + }); 25 + 26 + router.post('/', async (c) => { 27 + let body; 28 + try { 29 + body = await c.req.json(); 30 + } catch { 31 + return c.json({ error: 'Invalid JSON' }, 400); 32 + } 33 + 34 + const result = CreateCategorySchema.safeParse(body); 35 + if (!result.success) { 36 + return c.json({ error: result.error.issues[0].message }, 400); 37 + } 38 + 39 + const { name, color } = result.data; 40 + 41 + try { 42 + const info = db.prepare('INSERT INTO categories (name, color) VALUES (?, ?)').run(name, color); 43 + const category = db.prepare('SELECT * FROM categories WHERE id = ?').get(info.lastInsertRowid); 44 + return c.json(category, 201); 45 + } catch (error: unknown) { 46 + if (error && typeof error === 'object' && 'code' in error && error.code === 'SQLITE_CONSTRAINT_UNIQUE') { 47 + return c.json({ error: 'Category name already exists' }, 400); 48 + } 49 + throw error; 50 + } 51 + }); 52 + 53 + router.delete('/:id', (c) => { 54 + const id = c.req.param('id'); 55 + const existing = db.prepare('SELECT id FROM categories WHERE id = ?').get(id); 56 + if (!existing) { 57 + return c.json({ error: 'Not found' }, 404); 58 + } 59 + 60 + const todosCount = db.prepare('SELECT COUNT(*) as count FROM todos WHERE category_id = ?').get(id) as { count: number }; 61 + if (todosCount.count > 0) { 62 + return c.json({ error: 'Cannot delete category with todos' }, 400); 63 + } 64 + 65 + db.prepare('DELETE FROM categories WHERE id = ?').run(id); 66 + return c.body(null, 204); 67 + }); 68 + 69 + export default router; 70 + 71 + /** @internal Phoenix VCS traceability — do not remove. */ 72 + export const _phoenix = { 73 + iu_id: '643061e5748a224153e0f670e25f0f3b8edb566356e175dbe8555bab2d2adf49', 74 + name: 'Categories', 75 + risk_tier: 'high', 76 + canon_ids: [6 as const], 77 + } as const;
+94
examples/todo-app/src/generated/todos/error-handling.ts
··· 1 + import { Hono } from 'hono'; 2 + import { db, registerMigration } from '../../db.js'; 3 + import { z } from 'zod'; 4 + 5 + const router = new Hono(); 6 + 7 + // Global error handler middleware 8 + router.onError((err, c) => { 9 + console.error('Unhandled error:', err); 10 + return c.json({ error: 'Internal server error' }, 500); 11 + }); 12 + 13 + // Middleware to handle invalid JSON 14 + router.use('*', async (c, next) => { 15 + if (c.req.method === 'POST' || c.req.method === 'PATCH' || c.req.method === 'PUT') { 16 + const contentType = c.req.header('content-type'); 17 + if (contentType && contentType.includes('application/json')) { 18 + try { 19 + // Pre-parse JSON to catch syntax errors 20 + const body = await c.req.text(); 21 + if (body.trim()) { 22 + JSON.parse(body); 23 + } 24 + } catch (error) { 25 + return c.json({ error: 'Invalid JSON body' }, 400); 26 + } 27 + } 28 + } 29 + await next(); 30 + }); 31 + 32 + // Test endpoint to demonstrate error handling 33 + router.get('/test-errors', (c) => { 34 + const type = c.req.query('type'); 35 + 36 + switch (type) { 37 + case 'validation': 38 + return c.json({ error: 'Validation failed: title is required' }, 400); 39 + case 'not-found': 40 + return c.json({ error: 'Resource not found' }, 404); 41 + case 'invalid-json': 42 + return c.json({ error: 'Invalid JSON body' }, 400); 43 + case 'server-error': 44 + throw new Error('Simulated server error'); 45 + default: 46 + return c.json({ 47 + message: 'Error handling test endpoint', 48 + available_types: ['validation', 'not-found', 'invalid-json', 'server-error'] 49 + }); 50 + } 51 + }); 52 + 53 + // Test endpoint for JSON validation 54 + router.post('/test-json', async (c) => { 55 + try { 56 + const body = await c.req.json(); 57 + return c.json({ message: 'Valid JSON received', data: body }); 58 + } catch (error) { 59 + return c.json({ error: 'Invalid JSON body' }, 400); 60 + } 61 + }); 62 + 63 + // Test endpoint for Zod validation 64 + const TestSchema = z.object({ 65 + name: z.string().min(1, 'Name is required'), 66 + email: z.string().email('Invalid email format'), 67 + age: z.number().int().min(0, 'Age must be non-negative'), 68 + }); 69 + 70 + router.post('/test-validation', async (c) => { 71 + try { 72 + const body = await c.req.json(); 73 + const result = TestSchema.safeParse(body); 74 + 75 + if (!result.success) { 76 + const firstError = result.error.issues[0]; 77 + return c.json({ error: firstError.message }, 400); 78 + } 79 + 80 + return c.json({ message: 'Validation passed', data: result.data }); 81 + } catch (error) { 82 + return c.json({ error: 'Invalid JSON body' }, 400); 83 + } 84 + }); 85 + 86 + export default router; 87 + 88 + /** @internal Phoenix VCS traceability — do not remove. */ 89 + export const _phoenix = { 90 + iu_id: '1ada78d27b09eccf1ebece746bdd645cf9dccc6e35efdbdeb0d23fa194400152', 91 + name: 'Error Handling', 92 + risk_tier: 'low', 93 + canon_ids: [3 as const], 94 + } as const;
-8
examples/todo-app/src/generated/todos/index.ts
··· 1 - /** 2 - * Todos 3 - * 4 - * AUTO-GENERATED by Phoenix VCS 5 - * Barrel export for all Todos modules. 6 - */ 7 - 8 - export * as todosResource from './todos-resource.js';
-106
examples/todo-app/src/generated/todos/todos-resource.ts
··· 1 - import { Hono } from 'hono'; 2 - import { db, registerMigration } from '../../db.js'; 3 - import { z } from 'zod'; 4 - 5 - registerMigration('todos', ` 6 - CREATE TABLE IF NOT EXISTS todos ( 7 - id INTEGER PRIMARY KEY AUTOINCREMENT, 8 - title TEXT NOT NULL, 9 - completed INTEGER NOT NULL DEFAULT 0, 10 - created_at TEXT NOT NULL DEFAULT (datetime('now')) 11 - ) 12 - `); 13 - 14 - const CreateTodoSchema = z.object({ 15 - title: z.string().min(1, 'Title must not be empty').max(200, 'Title must not exceed 200 characters'), 16 - }); 17 - 18 - const UpdateTodoSchema = z.object({ 19 - title: z.string().min(1, 'Title must not be empty').max(200, 'Title must not exceed 200 characters').optional(), 20 - completed: z.number().int().min(0).max(1).optional(), 21 - }); 22 - 23 - const router = new Hono(); 24 - 25 - // List all todos 26 - router.get('/', (c) => { 27 - const todos = db.prepare('SELECT * FROM todos ORDER BY created_at DESC').all(); 28 - return c.json(todos); 29 - }); 30 - 31 - // Get single todo 32 - router.get('/:id', (c) => { 33 - const todo = db.prepare('SELECT * FROM todos WHERE id = ?').get(c.req.param('id')); 34 - if (!todo) return c.json({ error: 'Not found' }, 404); 35 - return c.json(todo); 36 - }); 37 - 38 - // Create todo 39 - router.post('/', async (c) => { 40 - let body; 41 - try { 42 - body = await c.req.json(); 43 - } catch { 44 - return c.json({ error: 'Invalid JSON request body' }, 400); 45 - } 46 - 47 - const result = CreateTodoSchema.safeParse(body); 48 - if (!result.success) { 49 - return c.json({ error: result.error.issues[0].message }, 400); 50 - } 51 - 52 - const { title } = result.data; 53 - const info = db.prepare('INSERT INTO todos (title) VALUES (?)').run(title); 54 - const todo = db.prepare('SELECT * FROM todos WHERE id = ?').get(info.lastInsertRowid); 55 - return c.json(todo, 201); 56 - }); 57 - 58 - // Update todo 59 - router.patch('/:id', async (c) => { 60 - const id = c.req.param('id'); 61 - const existing = db.prepare('SELECT * FROM todos WHERE id = ?').get(id); 62 - if (!existing) return c.json({ error: 'Not found' }, 404); 63 - 64 - let body; 65 - try { 66 - body = await c.req.json(); 67 - } catch { 68 - return c.json({ error: 'Invalid JSON request body' }, 400); 69 - } 70 - 71 - const result = UpdateTodoSchema.safeParse(body); 72 - if (!result.success) { 73 - return c.json({ error: result.error.issues[0].message }, 400); 74 - } 75 - 76 - const updates = result.data; 77 - if (updates.title !== undefined) { 78 - db.prepare('UPDATE todos SET title = ? WHERE id = ?').run(updates.title, id); 79 - } 80 - if (updates.completed !== undefined) { 81 - db.prepare('UPDATE todos SET completed = ? WHERE id = ?').run(updates.completed, id); 82 - } 83 - 84 - const updated = db.prepare('SELECT * FROM todos WHERE id = ?').get(id); 85 - return c.json(updated); 86 - }); 87 - 88 - // Delete todo 89 - router.delete('/:id', (c) => { 90 - const id = c.req.param('id'); 91 - const existing = db.prepare('SELECT * FROM todos WHERE id = ?').get(id); 92 - if (!existing) return c.json({ error: 'Not found' }, 404); 93 - 94 - db.prepare('DELETE FROM todos WHERE id = ?').run(id); 95 - return c.body(null, 204); 96 - }); 97 - 98 - export default router; 99 - 100 - /** @internal Phoenix VCS traceability — do not remove. */ 101 - export const _phoenix = { 102 - iu_id: '9034ad0a11e5572f648cbbbc49401554d7901e78d9b24de3209750fb3a04b1ef', 103 - name: 'Todos Resource', 104 - risk_tier: 'high', 105 - canon_ids: [12 as const], 106 - } as const;
+181
examples/todo-app/src/generated/todos/todos.ts
··· 1 + import { Hono } from 'hono'; 2 + import { db, registerMigration } from '../../db.js'; 3 + import { z } from 'zod'; 4 + 5 + // Register table migrations 6 + registerMigration('categories', ` 7 + CREATE TABLE IF NOT EXISTS categories ( 8 + id INTEGER PRIMARY KEY AUTOINCREMENT, 9 + name TEXT NOT NULL UNIQUE, 10 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 11 + ) 12 + `); 13 + 14 + registerMigration('todos', ` 15 + CREATE TABLE IF NOT EXISTS todos ( 16 + id INTEGER PRIMARY KEY AUTOINCREMENT, 17 + title TEXT NOT NULL, 18 + completed INTEGER NOT NULL DEFAULT 0, 19 + category_id INTEGER REFERENCES categories(id), 20 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 21 + ) 22 + `); 23 + 24 + const CreateTodoSchema = z.object({ 25 + title: z.string().min(1).max(200), 26 + category_id: z.number().int().optional(), 27 + }); 28 + 29 + const UpdateTodoSchema = z.object({ 30 + title: z.string().min(1).max(200).optional(), 31 + completed: z.number().int().min(0).max(1).optional(), 32 + category_id: z.number().int().nullable().optional(), 33 + }); 34 + 35 + const router = new Hono(); 36 + 37 + // List todos with filtering and category names 38 + router.get('/', (c) => { 39 + let sql = 'SELECT todos.*, categories.name as category_name FROM todos LEFT JOIN categories ON todos.category_id = categories.id'; 40 + const conditions: string[] = []; 41 + const params: (string | number)[] = []; 42 + 43 + const completed = c.req.query('completed'); 44 + if (completed !== undefined) { 45 + conditions.push('todos.completed = ?'); 46 + params.push(Number(completed)); 47 + } 48 + 49 + const categoryId = c.req.query('category_id'); 50 + if (categoryId !== undefined) { 51 + conditions.push('todos.category_id = ?'); 52 + params.push(Number(categoryId)); 53 + } 54 + 55 + if (conditions.length > 0) { 56 + sql += ' WHERE ' + conditions.join(' AND '); 57 + } 58 + sql += ' ORDER BY todos.created_at DESC'; 59 + 60 + return c.json(db.prepare(sql).all(...params)); 61 + }); 62 + 63 + // Get stats 64 + router.get('/stats', (c) => { 65 + const total = db.prepare('SELECT COUNT(*) as count FROM todos').get() as { count: number }; 66 + const completed = db.prepare('SELECT COUNT(*) as count FROM todos WHERE completed = 1').get() as { count: number }; 67 + const incomplete = db.prepare('SELECT COUNT(*) as count FROM todos WHERE completed = 0').get() as { count: number }; 68 + 69 + const byCategory = db.prepare(` 70 + SELECT categories.name as category_name, COUNT(todos.id) as count 71 + FROM categories 72 + LEFT JOIN todos ON categories.id = todos.category_id 73 + GROUP BY categories.id, categories.name 74 + ORDER BY count DESC 75 + `).all() as { category_name: string; count: number }[]; 76 + 77 + return c.json({ 78 + total: total.count, 79 + completed: completed.count, 80 + incomplete: incomplete.count, 81 + by_category: byCategory 82 + }); 83 + }); 84 + 85 + // Get single todo 86 + router.get('/:id', (c) => { 87 + const todo = db.prepare('SELECT todos.*, categories.name as category_name FROM todos LEFT JOIN categories ON todos.category_id = categories.id WHERE todos.id = ?').get(c.req.param('id')); 88 + if (!todo) return c.json({ error: 'Not found' }, 404); 89 + return c.json(todo); 90 + }); 91 + 92 + // Create todo 93 + router.post('/', async (c) => { 94 + let body; 95 + try { 96 + body = await c.req.json(); 97 + } catch { 98 + return c.json({ error: 'Invalid JSON' }, 400); 99 + } 100 + 101 + const result = CreateTodoSchema.safeParse(body); 102 + if (!result.success) { 103 + return c.json({ error: result.error.issues[0].message }, 400); 104 + } 105 + 106 + const { title, category_id } = result.data; 107 + 108 + // Validate category exists if provided 109 + if (category_id !== undefined) { 110 + const category = db.prepare('SELECT id FROM categories WHERE id = ?').get(category_id); 111 + if (!category) { 112 + return c.json({ error: 'Category not found' }, 400); 113 + } 114 + } 115 + 116 + const info = db.prepare('INSERT INTO todos (title, category_id) VALUES (?, ?)').run(title, category_id ?? null); 117 + const todo = db.prepare('SELECT todos.*, categories.name as category_name FROM todos LEFT JOIN categories ON todos.category_id = categories.id WHERE todos.id = ?').get(info.lastInsertRowid); 118 + return c.json(todo, 201); 119 + }); 120 + 121 + // Update todo 122 + router.patch('/:id', async (c) => { 123 + const id = c.req.param('id'); 124 + const existing = db.prepare('SELECT id FROM todos WHERE id = ?').get(id); 125 + if (!existing) return c.json({ error: 'Not found' }, 404); 126 + 127 + let body; 128 + try { 129 + body = await c.req.json(); 130 + } catch { 131 + return c.json({ error: 'Invalid JSON' }, 400); 132 + } 133 + 134 + const result = UpdateTodoSchema.safeParse(body); 135 + if (!result.success) { 136 + return c.json({ error: result.error.issues[0].message }, 400); 137 + } 138 + 139 + const updates = result.data; 140 + 141 + // Validate category exists if provided 142 + if (updates.category_id !== undefined && updates.category_id !== null) { 143 + const category = db.prepare('SELECT id FROM categories WHERE id = ?').get(updates.category_id); 144 + if (!category) { 145 + return c.json({ error: 'Category not found' }, 400); 146 + } 147 + } 148 + 149 + if (updates.title !== undefined) { 150 + db.prepare('UPDATE todos SET title = ? WHERE id = ?').run(updates.title, id); 151 + } 152 + if (updates.completed !== undefined) { 153 + db.prepare('UPDATE todos SET completed = ? WHERE id = ?').run(updates.completed, id); 154 + } 155 + if (updates.category_id !== undefined) { 156 + db.prepare('UPDATE todos SET category_id = ? WHERE id = ?').run(updates.category_id, id); 157 + } 158 + 159 + const updated = db.prepare('SELECT todos.*, categories.name as category_name FROM todos LEFT JOIN categories ON todos.category_id = categories.id WHERE todos.id = ?').get(id); 160 + return c.json(updated); 161 + }); 162 + 163 + // Delete todo 164 + router.delete('/:id', (c) => { 165 + const id = c.req.param('id'); 166 + const existing = db.prepare('SELECT id FROM todos WHERE id = ?').get(id); 167 + if (!existing) return c.json({ error: 'Not found' }, 404); 168 + 169 + db.prepare('DELETE FROM todos WHERE id = ?').run(id); 170 + return c.body(null, 204); 171 + }); 172 + 173 + export default router; 174 + 175 + /** @internal Phoenix VCS traceability — do not remove. */ 176 + export const _phoenix = { 177 + iu_id: '614d1c26e17fec59d237b38cb78d14045816af536a424d086aee82f154b0e287', 178 + name: 'Todos', 179 + risk_tier: 'high', 180 + canon_ids: [13 as const], 181 + } as const;
+710
examples/todo-app/src/generated/todos/web-interface.ts
··· 1 + import { Hono } from 'hono'; 2 + import { db, registerMigration } from '../../db.js'; 3 + import { z } from 'zod'; 4 + 5 + // Register table migrations 6 + registerMigration('categories', ` 7 + CREATE TABLE IF NOT EXISTS categories ( 8 + id INTEGER PRIMARY KEY AUTOINCREMENT, 9 + name TEXT NOT NULL UNIQUE, 10 + color TEXT NOT NULL DEFAULT '#3b82f6', 11 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 12 + ) 13 + `); 14 + 15 + registerMigration('todos', ` 16 + CREATE TABLE IF NOT EXISTS todos ( 17 + id INTEGER PRIMARY KEY AUTOINCREMENT, 18 + title TEXT NOT NULL, 19 + completed INTEGER NOT NULL DEFAULT 0, 20 + category_id INTEGER REFERENCES categories(id), 21 + created_at TEXT NOT NULL DEFAULT (datetime('now')) 22 + ) 23 + `); 24 + 25 + const router = new Hono(); 26 + 27 + // Validation schemas 28 + const createTodoSchema = z.object({ 29 + title: z.string().min(1), 30 + category_id: z.number().int().positive().optional() 31 + }); 32 + 33 + const updateTodoSchema = z.object({ 34 + title: z.string().min(1).optional(), 35 + completed: z.number().int().min(0).max(1).optional(), 36 + category_id: z.number().int().positive().optional().nullable() 37 + }); 38 + 39 + const createCategorySchema = z.object({ 40 + name: z.string().min(1), 41 + color: z.string().regex(/^#[0-9a-fA-F]{6}$/) 42 + }); 43 + 44 + // Categories routes 45 + router.get('/categories', (c) => { 46 + const stmt = db.prepare('SELECT * FROM categories ORDER BY name'); 47 + const categories = stmt.all(); 48 + return c.json(categories); 49 + }); 50 + 51 + router.get('/categories/:id', (c) => { 52 + const id = parseInt(c.req.param('id')); 53 + if (isNaN(id)) { 54 + return c.json({ error: 'Invalid ID' }, 400); 55 + } 56 + 57 + const stmt = db.prepare('SELECT * FROM categories WHERE id = ?'); 58 + const category = stmt.get(id); 59 + 60 + if (!category) { 61 + return c.json({ error: 'Not found' }, 404); 62 + } 63 + 64 + return c.json(category); 65 + }); 66 + 67 + router.post('/categories', async (c) => { 68 + const body = await c.req.json(); 69 + const result = createCategorySchema.safeParse(body); 70 + 71 + if (!result.success) { 72 + return c.json({ error: 'Invalid input' }, 400); 73 + } 74 + 75 + try { 76 + const stmt = db.prepare('INSERT INTO categories (name, color) VALUES (?, ?)'); 77 + const info = stmt.run(result.data.name, result.data.color); 78 + 79 + const getStmt = db.prepare('SELECT * FROM categories WHERE id = ?'); 80 + const category = getStmt.get(info.lastInsertRowid); 81 + 82 + return c.json(category, 201); 83 + } catch (error) { 84 + return c.json({ error: 'Category name already exists' }, 400); 85 + } 86 + }); 87 + 88 + router.delete('/categories/:id', (c) => { 89 + const id = parseInt(c.req.param('id')); 90 + if (isNaN(id)) { 91 + return c.json({ error: 'Invalid ID' }, 400); 92 + } 93 + 94 + const stmt = db.prepare('DELETE FROM categories WHERE id = ?'); 95 + const info = stmt.run(id); 96 + 97 + if (info.changes === 0) { 98 + return c.json({ error: 'Not found' }, 404); 99 + } 100 + 101 + return c.body(null, 204); 102 + }); 103 + 104 + // Todos routes 105 + router.get('/todos', (c) => { 106 + const stmt = db.prepare(` 107 + SELECT t.*, c.name as category_name, c.color as category_color 108 + FROM todos t 109 + LEFT JOIN categories c ON t.category_id = c.id 110 + ORDER BY t.created_at DESC 111 + `); 112 + const todos = stmt.all(); 113 + return c.json(todos); 114 + }); 115 + 116 + router.get('/todos/:id', (c) => { 117 + const id = parseInt(c.req.param('id')); 118 + if (isNaN(id)) { 119 + return c.json({ error: 'Invalid ID' }, 400); 120 + } 121 + 122 + const stmt = db.prepare(` 123 + SELECT t.*, c.name as category_name, c.color as category_color 124 + FROM todos t 125 + LEFT JOIN categories c ON t.category_id = c.id 126 + WHERE t.id = ? 127 + `); 128 + const todo = stmt.get(id); 129 + 130 + if (!todo) { 131 + return c.json({ error: 'Not found' }, 404); 132 + } 133 + 134 + return c.json(todo); 135 + }); 136 + 137 + router.post('/todos', async (c) => { 138 + const body = await c.req.json(); 139 + const result = createTodoSchema.safeParse(body); 140 + 141 + if (!result.success) { 142 + return c.json({ error: 'Invalid input' }, 400); 143 + } 144 + 145 + const stmt = db.prepare('INSERT INTO todos (title, category_id) VALUES (?, ?)'); 146 + const info = stmt.run(result.data.title, result.data.category_id || null); 147 + 148 + const getStmt = db.prepare(` 149 + SELECT t.*, c.name as category_name, c.color as category_color 150 + FROM todos t 151 + LEFT JOIN categories c ON t.category_id = c.id 152 + WHERE t.id = ? 153 + `); 154 + const todo = getStmt.get(info.lastInsertRowid); 155 + 156 + return c.json(todo, 201); 157 + }); 158 + 159 + router.patch('/todos/:id', async (c) => { 160 + const id = parseInt(c.req.param('id')); 161 + if (isNaN(id)) { 162 + return c.json({ error: 'Invalid ID' }, 400); 163 + } 164 + 165 + const body = await c.req.json(); 166 + const result = updateTodoSchema.safeParse(body); 167 + 168 + if (!result.success) { 169 + return c.json({ error: 'Invalid input' }, 400); 170 + } 171 + 172 + const updates: string[] = []; 173 + const values: any[] = []; 174 + 175 + if (result.data.title !== undefined) { 176 + updates.push('title = ?'); 177 + values.push(result.data.title); 178 + } 179 + 180 + if (result.data.completed !== undefined) { 181 + updates.push('completed = ?'); 182 + values.push(result.data.completed); 183 + } 184 + 185 + if (result.data.category_id !== undefined) { 186 + updates.push('category_id = ?'); 187 + values.push(result.data.category_id); 188 + } 189 + 190 + if (updates.length === 0) { 191 + return c.json({ error: 'No fields to update' }, 400); 192 + } 193 + 194 + values.push(id); 195 + const stmt = db.prepare(`UPDATE todos SET ${updates.join(', ')} WHERE id = ?`); 196 + const info = stmt.run(...values); 197 + 198 + if (info.changes === 0) { 199 + return c.json({ error: 'Not found' }, 404); 200 + } 201 + 202 + const getStmt = db.prepare(` 203 + SELECT t.*, c.name as category_name, c.color as category_color 204 + FROM todos t 205 + LEFT JOIN categories c ON t.category_id = c.id 206 + WHERE t.id = ? 207 + `); 208 + const todo = getStmt.get(id); 209 + 210 + return c.json(todo); 211 + }); 212 + 213 + router.delete('/todos/:id', (c) => { 214 + const id = parseInt(c.req.param('id')); 215 + if (isNaN(id)) { 216 + return c.json({ error: 'Invalid ID' }, 400); 217 + } 218 + 219 + const stmt = db.prepare('DELETE FROM todos WHERE id = ?'); 220 + const info = stmt.run(id); 221 + 222 + if (info.changes === 0) { 223 + return c.json({ error: 'Not found' }, 404); 224 + } 225 + 226 + return c.body(null, 204); 227 + }); 228 + 229 + // Stats route 230 + router.get('/stats', (c) => { 231 + const stmt = db.prepare(` 232 + SELECT 233 + COUNT(*) as total_todos, 234 + SUM(completed) as completed_todos, 235 + COUNT(*) - SUM(completed) as incomplete_todos 236 + FROM todos 237 + `); 238 + const stats = stmt.get(); 239 + return c.json(stats); 240 + }); 241 + 242 + // Serve the web interface 243 + router.get('/', (c) => { 244 + const html = `<!DOCTYPE html> 245 + <html lang="en"> 246 + <head> 247 + <meta charset="UTF-8"> 248 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 249 + <title>Todos</title> 250 + <style> 251 + * { 252 + margin: 0; 253 + padding: 0; 254 + box-sizing: border-box; 255 + } 256 + 257 + body { 258 + font-family: system-ui, -apple-system, sans-serif; 259 + background-color: #f8fafc; 260 + color: #334155; 261 + line-height: 1.6; 262 + } 263 + 264 + .container { 265 + max-width: 640px; 266 + margin: 0 auto; 267 + padding: 2rem 1rem; 268 + } 269 + 270 + h1 { 271 + text-align: center; 272 + font-size: 2.5rem; 273 + font-weight: 700; 274 + color: #1e293b; 275 + margin-bottom: 2rem; 276 + } 277 + 278 + .stats { 279 + background: white; 280 + border-radius: 0.5rem; 281 + padding: 1.5rem; 282 + margin-bottom: 2rem; 283 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 284 + display: flex; 285 + justify-content: space-around; 286 + text-align: center; 287 + } 288 + 289 + .stat { 290 + display: flex; 291 + flex-direction: column; 292 + } 293 + 294 + .stat-number { 295 + font-size: 2rem; 296 + font-weight: 700; 297 + color: #3b82f6; 298 + } 299 + 300 + .stat-label { 301 + font-size: 0.875rem; 302 + color: #64748b; 303 + text-transform: uppercase; 304 + letter-spacing: 0.05em; 305 + } 306 + 307 + .section { 308 + background: white; 309 + border-radius: 0.5rem; 310 + padding: 1.5rem; 311 + margin-bottom: 2rem; 312 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 313 + } 314 + 315 + .section h2 { 316 + font-size: 1.25rem; 317 + font-weight: 600; 318 + margin-bottom: 1rem; 319 + color: #1e293b; 320 + } 321 + 322 + .form { 323 + display: flex; 324 + gap: 0.75rem; 325 + margin-bottom: 1rem; 326 + flex-wrap: wrap; 327 + } 328 + 329 + input, select, button { 330 + padding: 0.5rem 0.75rem; 331 + border: 1px solid #d1d5db; 332 + border-radius: 0.375rem; 333 + font-size: 0.875rem; 334 + } 335 + 336 + input[type="text"] { 337 + flex: 1; 338 + min-width: 200px; 339 + } 340 + 341 + input[type="color"] { 342 + width: 3rem; 343 + height: 2.5rem; 344 + padding: 0.25rem; 345 + cursor: pointer; 346 + } 347 + 348 + select { 349 + min-width: 120px; 350 + } 351 + 352 + button { 353 + background: #3b82f6; 354 + color: white; 355 + border: none; 356 + cursor: pointer; 357 + font-weight: 500; 358 + transition: background-color 0.2s; 359 + } 360 + 361 + button:hover { 362 + background: #2563eb; 363 + } 364 + 365 + button.danger { 366 + background: #ef4444; 367 + } 368 + 369 + button.danger:hover { 370 + background: #dc2626; 371 + } 372 + 373 + button.small { 374 + padding: 0.25rem 0.5rem; 375 + font-size: 0.75rem; 376 + } 377 + 378 + .filters { 379 + display: flex; 380 + gap: 0.5rem; 381 + margin-bottom: 1rem; 382 + } 383 + 384 + .filter-btn { 385 + background: #f1f5f9; 386 + color: #475569; 387 + border: 1px solid #e2e8f0; 388 + padding: 0.5rem 1rem; 389 + border-radius: 0.375rem; 390 + cursor: pointer; 391 + font-size: 0.875rem; 392 + transition: all 0.2s; 393 + } 394 + 395 + .filter-btn.active { 396 + background: #3b82f6; 397 + color: white; 398 + border-color: #3b82f6; 399 + } 400 + 401 + .todo-list { 402 + list-style: none; 403 + } 404 + 405 + .todo-item { 406 + display: flex; 407 + align-items: center; 408 + gap: 0.75rem; 409 + padding: 0.75rem; 410 + border: 1px solid #e2e8f0; 411 + border-radius: 0.375rem; 412 + margin-bottom: 0.5rem; 413 + background: #fefefe; 414 + } 415 + 416 + .todo-checkbox { 417 + width: 1.25rem; 418 + height: 1.25rem; 419 + cursor: pointer; 420 + } 421 + 422 + .todo-title { 423 + flex: 1; 424 + font-weight: 500; 425 + } 426 + 427 + .todo-title.completed { 428 + text-decoration: line-through; 429 + color: #64748b; 430 + } 431 + 432 + .category-badge { 433 + padding: 0.25rem 0.5rem; 434 + border-radius: 0.25rem; 435 + font-size: 0.75rem; 436 + font-weight: 500; 437 + color: white; 438 + } 439 + 440 + .category-list { 441 + list-style: none; 442 + } 443 + 444 + .category-item { 445 + display: flex; 446 + align-items: center; 447 + gap: 0.75rem; 448 + padding: 0.5rem; 449 + border: 1px solid #e2e8f0; 450 + border-radius: 0.375rem; 451 + margin-bottom: 0.5rem; 452 + background: #fefefe; 453 + } 454 + 455 + .category-color { 456 + width: 1rem; 457 + height: 1rem; 458 + border-radius: 50%; 459 + } 460 + 461 + .category-name { 462 + flex: 1; 463 + font-weight: 500; 464 + } 465 + 466 + .empty-state { 467 + text-align: center; 468 + color: #64748b; 469 + font-style: italic; 470 + padding: 2rem; 471 + } 472 + </style> 473 + </head> 474 + <body> 475 + <div class="container"> 476 + <h1>todos</h1> 477 + 478 + <div class="stats"> 479 + <div class="stat"> 480 + <div class="stat-number" id="total-count">0</div> 481 + <div class="stat-label">Total</div> 482 + </div> 483 + <div class="stat"> 484 + <div class="stat-number" id="completed-count">0</div> 485 + <div class="stat-label">Completed</div> 486 + </div> 487 + <div class="stat"> 488 + <div class="stat-number" id="incomplete-count">0</div> 489 + <div class="stat-label">Incomplete</div> 490 + </div> 491 + </div> 492 + 493 + <div class="section"> 494 + <h2>Add Todo</h2> 495 + <div class="form"> 496 + <input type="text" id="todo-title" placeholder="Enter todo title..." /> 497 + <select id="todo-category"> 498 + <option value="">No category</option> 499 + </select> 500 + <button onclick="createTodo()">Add Todo</button> 501 + </div> 502 + </div> 503 + 504 + <div class="section"> 505 + <h2>Todos</h2> 506 + <div class="filters"> 507 + <button class="filter-btn active" onclick="setFilter('all')">All</button> 508 + <button class="filter-btn" onclick="setFilter('active')">Active</button> 509 + <button class="filter-btn" onclick="setFilter('completed')">Completed</button> 510 + </div> 511 + <ul class="todo-list" id="todo-list"> 512 + <li class="empty-state">Loading todos...</li> 513 + </ul> 514 + </div> 515 + 516 + <div class="section"> 517 + <h2>Categories</h2> 518 + <div class="form"> 519 + <input type="text" id="category-name" placeholder="Category name..." /> 520 + <input type="color" id="category-color" value="#3b82f6" /> 521 + <button onclick="createCategory()">Add Category</button> 522 + </div> 523 + <ul class="category-list" id="category-list"> 524 + <li class="empty-state">Loading categories...</li> 525 + </ul> 526 + </div> 527 + </div> 528 + 529 + <script> 530 + let currentFilter = 'all'; 531 + let todos = []; 532 + let categories = []; 533 + 534 + async function loadData() { 535 + await Promise.all([loadTodos(), loadCategories(), loadStats()]); 536 + renderTodos(); 537 + renderCategories(); 538 + } 539 + 540 + async function loadTodos() { 541 + const response = await fetch('/todos'); 542 + todos = await response.json(); 543 + } 544 + 545 + async function loadCategories() { 546 + const response = await fetch('/categories'); 547 + categories = await response.json(); 548 + renderCategorySelect(); 549 + } 550 + 551 + async function loadStats() { 552 + const response = await fetch('/stats'); 553 + const stats = await response.json(); 554 + document.getElementById('total-count').textContent = stats.total_todos; 555 + document.getElementById('completed-count').textContent = stats.completed_todos; 556 + document.getElementById('incomplete-count').textContent = stats.incomplete_todos; 557 + } 558 + 559 + function renderCategorySelect() { 560 + const select = document.getElementById('todo-category'); 561 + select.innerHTML = '<option value="">No category</option>'; 562 + categories.forEach(cat => { 563 + const option = document.createElement('option'); 564 + option.value = cat.id; 565 + option.textContent = cat.name; 566 + select.appendChild(option); 567 + }); 568 + } 569 + 570 + function renderTodos() { 571 + const list = document.getElementById('todo-list'); 572 + const filteredTodos = todos.filter(todo => { 573 + if (currentFilter === 'active') return !todo.completed; 574 + if (currentFilter === 'completed') return todo.completed; 575 + return true; 576 + }); 577 + 578 + if (filteredTodos.length === 0) { 579 + list.innerHTML = '<li class="empty-state">No todos found</li>'; 580 + return; 581 + } 582 + 583 + list.innerHTML = filteredTodos.map(todo => { 584 + const category = categories.find(c => c.id === todo.category_id); 585 + return \` 586 + <li class="todo-item"> 587 + <input type="checkbox" class="todo-checkbox" 588 + \${todo.completed ? 'checked' : ''} 589 + onchange="toggleTodo(\${todo.id})" /> 590 + <span class="todo-title \${todo.completed ? 'completed' : ''}">\${todo.title}</span> 591 + \${category ? \`<span class="category-badge" style="background-color: \${category.color}">\${category.name}</span>\` : ''} 592 + <button class="danger small" onclick="deleteTodo(\${todo.id})">Delete</button> 593 + </li> 594 + \`; 595 + }).join(''); 596 + } 597 + 598 + function renderCategories() { 599 + const list = document.getElementById('category-list'); 600 + 601 + if (categories.length === 0) { 602 + list.innerHTML = '<li class="empty-state">No categories found</li>'; 603 + return; 604 + } 605 + 606 + list.innerHTML = categories.map(category => { 607 + const todoCount = todos.filter(t => t.category_id === category.id).length; 608 + return \` 609 + <li class="category-item"> 610 + <div class="category-color" style="background-color: \${category.color}"></div> 611 + <span class="category-name">\${category.name}</span> 612 + \${todoCount === 0 ? \`<button class="danger small" onclick="deleteCategory(\${category.id})">Delete</button>\` : \`<span style="font-size: 0.75rem; color: #64748b;">(\${todoCount} todos)</span>\`} 613 + </li> 614 + \`; 615 + }).join(''); 616 + } 617 + 618 + function setFilter(filter) { 619 + currentFilter = filter; 620 + document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active')); 621 + event.target.classList.add('active'); 622 + renderTodos(); 623 + } 624 + 625 + async function createTodo() { 626 + const title = document.getElementById('todo-title').value.trim(); 627 + const categoryId = document.getElementById('todo-category').value; 628 + 629 + if (!title) return; 630 + 631 + const payload = { title }; 632 + if (categoryId) payload.category_id = parseInt(categoryId); 633 + 634 + const response = await fetch('/todos', { 635 + method: 'POST', 636 + headers: { 'Content-Type': 'application/json' }, 637 + body: JSON.stringify(payload) 638 + }); 639 + 640 + if (response.ok) { 641 + document.getElementById('todo-title').value = ''; 642 + document.getElementById('todo-category').value = ''; 643 + await loadData(); 644 + } 645 + } 646 + 647 + async function toggleTodo(id) { 648 + const todo = todos.find(t => t.id === id); 649 + const response = await fetch(\`/todos/\${id}\`, { 650 + method: 'PATCH', 651 + headers: { 'Content-Type': 'application/json' }, 652 + body: JSON.stringify({ completed: todo.completed ? 0 : 1 }) 653 + }); 654 + 655 + if (response.ok) { 656 + await loadData(); 657 + } 658 + } 659 + 660 + async function deleteTodo(id) { 661 + const response = await fetch(\`/todos/\${id}\`, { method: 'DELETE' }); 662 + if (response.ok) { 663 + await loadData(); 664 + } 665 + } 666 + 667 + async function createCategory() { 668 + const name = document.getElementById('category-name').value.trim(); 669 + const color = document.getElementById('category-color').value; 670 + 671 + if (!name) return; 672 + 673 + const response = await fetch('/categories', { 674 + method: 'POST', 675 + headers: { 'Content-Type': 'application/json' }, 676 + body: JSON.stringify({ name, color }) 677 + }); 678 + 679 + if (response.ok) { 680 + document.getElementById('category-name').value = ''; 681 + document.getElementById('category-color').value = '#3b82f6'; 682 + await loadData(); 683 + } 684 + } 685 + 686 + async function deleteCategory(id) { 687 + const response = await fetch(\`/categories/\${id}\`, { method: 'DELETE' }); 688 + if (response.ok) { 689 + await loadData(); 690 + } 691 + } 692 + 693 + // Initialize the app 694 + loadData(); 695 + </script> 696 + </body> 697 + </html>`; 698 + 699 + return c.html(html); 700 + }); 701 + 702 + export default router; 703 + 704 + /** @internal Phoenix VCS traceability — do not remove. */ 705 + export const _phoenix = { 706 + iu_id: '36212ecf5a73b3cdaf7f64d0fdfe77ac955188826af9854d63ab2168e88ab795', 707 + name: 'Web Interface', 708 + risk_tier: 'medium', 709 + canon_ids: [10 as const], 710 + } as const;
-14
examples/todo-app/src/server.ts
··· 1 - import { serve } from '@hono/node-server'; 2 - import { app, mount } from './app.js'; 3 - import { runMigrations } from './db.js'; 4 - 5 - // Generated route modules 6 - import todos_resource from './generated/todos/todos-resource.js'; 7 - 8 - // Mount routes 9 - mount('/todos', todos_resource); 10 - 11 - const port = parseInt(process.env.PORT ?? '3000', 10); 12 - runMigrations(); 13 - console.log(`Server running at http://localhost:${port}`); 14 - serve({ fetch: app.fetch, port });
+8
src/architectures/sqlite-web-api.ts
··· 130 130 - If the spec describes a stats or aggregate endpoint, implement it as a route on the same router. 131 131 - Use SQL aggregate functions (COUNT, SUM, AVG) with GROUP BY. 132 132 - Return the JSON structure EXACTLY as the spec describes, using snake_case keys. 133 + 134 + ### Web interface / HTML pages 135 + - If the spec describes a web interface or HTML page, generate a Hono route that returns \`c.html()\` with a complete HTML string. 136 + - Include ALL CSS and JavaScript inline in the HTML — no external files or build steps. 137 + - The JavaScript must use fetch() to call the API endpoints (same origin, e.g., fetch('/todos')). 138 + - After any create/update/delete action, refresh the displayed data by re-fetching. 139 + - Use modern vanilla JavaScript (no frameworks). Use template literals for HTML generation. 140 + - The HTML must be a complete document with <!DOCTYPE html>, <head>, and <body>. 133 141 `; 134 142 135 143 const CODE_EXAMPLES = `
+25 -2
src/regen.ts
··· 64 64 const msg = err instanceof Error ? err.message : String(err); 65 65 ctx.onProgress?.(iu, 'error', msg); 66 66 // Fall back to stub on LLM failure 67 - content = generateModule(iu); 67 + content = ctx.architecture ? generateArchStub(iu) : generateModule(iu); 68 68 } 69 69 } else { 70 - content = generateModule(iu); 70 + content = ctx?.architecture ? generateArchStub(iu) : generateModule(iu); 71 71 } 72 72 73 73 files.set(outputPath, content); ··· 264 264 } 265 265 266 266 // ─── Module Generation ─────────────────────────────────────────────────────── 267 + 268 + /** 269 + * Generate a minimal Hono router stub for architecture mode. 270 + * Ensures fallback code still produces a valid default-export router. 271 + */ 272 + function generateArchStub(iu: ImplementationUnit): string { 273 + return `import { Hono } from 'hono'; 274 + 275 + const router = new Hono(); 276 + 277 + router.get('/', (c) => c.json({ stub: true, module: '${iu.name}', message: 'Not yet implemented' })); 278 + 279 + export default router; 280 + 281 + /** @internal Phoenix VCS traceability — do not remove. */ 282 + export const _phoenix = { 283 + iu_id: '${iu.iu_id}', 284 + name: '${iu.name}', 285 + risk_tier: '${iu.risk_tier}', 286 + canon_ids: [${iu.source_canon_ids.length} as const], 287 + } as const; 288 + `; 289 + } 267 290 268 291 /** 269 292 * Generate a natural TypeScript module from an IU contract.
+4 -1
src/scaffold.ts
··· 93 93 routeImports.push(`import ${modName} from '${importPath}';`); 94 94 // Derive mount path from IU name: "Todos" → "/todos", "Categories" → "/categories" 95 95 const iuName = iu?.name ?? mod.replace('.ts', ''); 96 - const prefix = '/' + iuName.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); 96 + const lowerName = iuName.toLowerCase(); 97 + // Web interface / UI modules mount at root 98 + const isWebUI = /\b(web|ui|frontend|interface|page|dashboard)\b/.test(lowerName); 99 + const prefix = isWebUI ? '' : '/' + lowerName.replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); 97 100 routeMounts.push(`mount('${prefix}', ${modName});`); 98 101 } 99 102 }