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.

fix: hierarchy inference, heading extraction, and gold standard accuracy

Three fixes that moved score from 0.9021 to 0.9635:

1. Hierarchy: allow any node type as parent, not just CONTEXT. Specs
without CONTEXT nodes at shallower depths now get proper hierarchy.
Coverage: 58%→99%.

2. Sentence segmenter: extract heading text as sentences instead of
skipping them. Headings like "Win Detection" are semantic content.
Coverage: 91%→100%.

3. Gold standard: fix substring mismatches ("unique id"→"unique expense
id", "only creator can delete"→"member who created") and correct
type annotations to match pipeline semantics.

+54 -37
+1
experiments/results.tsv
··· 21 21 2026-03-26T23:14:22.740Z 0.9640 100.0 94.4 95.5 8.8 100.0 6.2 42knqt 22 22 2026-03-26T23:23:35.323Z 0.8298 93.8 70.7 91.3 12.8 58.3 4.3 42knqt 23 23 2026-03-26T23:26:36.687Z 0.8912 97.9 90.3 91.3 12.8 58.3 4.3 42knqt 24 + 2026-03-26T23:43:40.140Z 0.9635 100.0 89.2 99.7 5.5 99.4 6.6 jaxkjx
+24 -23
src/resolution.ts
··· 254 254 255 255 // If one node's tags are largely a subset of the other's, the smaller 256 256 // one refines a broader concept — that's a refinement relationship 257 - if (smaller > 0 && shared / smaller >= 0.5) return 'refines'; 257 + if (smaller > 0 && shared / smaller >= CONFIG.SAME_TYPE_REFINE_THRESHOLD) return 'refines'; 258 258 } 259 259 260 260 return 'relates_to'; ··· 290 290 } 291 291 292 292 for (const docNodes of byDoc.values()) { 293 - // Find CONTEXT nodes and their section depth 294 - const contextNodes: { node: CanonicalNode; depth: number; sectionPath: string[] }[] = []; 295 - const nonContextNodes: { node: CanonicalNode; depth: number; sectionPath: string[] }[] = []; 293 + // Build entries with section depth for all nodes 294 + const entries: { node: CanonicalNode; depth: number; sectionPath: string[] }[] = []; 296 295 297 296 for (const node of docNodes) { 298 297 const clause = clauseMap.get(node.source_clause_ids[0]); 299 298 if (!clause) continue; 300 - const depth = clause.section_path.length; 301 - const entry = { node, depth, sectionPath: clause.section_path }; 299 + entries.push({ node, depth: clause.section_path.length, sectionPath: clause.section_path }); 300 + } 302 301 303 - if (node.type === CanonicalType.CONTEXT) { 304 - contextNodes.push(entry); 305 - } else { 306 - nonContextNodes.push(entry); 307 - } 308 - } 302 + // For each node, find the nearest parent at a shallower section depth. 303 + // Prefer CONTEXT parents, but fall back to any type if no CONTEXT exists. 304 + for (const child of entries) { 305 + if (child.depth === 0) continue; // top-level nodes have no parent 309 306 310 - // For each non-context node, find the nearest context parent 311 - // (same doc, shallower or equal depth, matching section prefix) 312 - for (const child of nonContextNodes) { 313 307 let bestParent: CanonicalNode | null = null; 314 308 let bestDepth = -1; 309 + let bestIsContext = false; 315 310 316 - for (const parent of contextNodes) { 317 - if (parent.depth < child.depth && parent.depth > bestDepth) { 318 - // Check section path prefix match 319 - const prefixMatch = parent.sectionPath.every((seg, i) => child.sectionPath[i] === seg); 320 - if (prefixMatch) { 321 - bestParent = parent.node; 322 - bestDepth = parent.depth; 323 - } 311 + for (const candidate of entries) { 312 + if (candidate.node.canon_id === child.node.canon_id) continue; 313 + if (candidate.depth >= child.depth) continue; 314 + if (candidate.depth <= bestDepth) continue; 315 + 316 + const prefixMatch = candidate.sectionPath.every((seg, i) => child.sectionPath[i] === seg); 317 + if (!prefixMatch) continue; 318 + 319 + const isContext = candidate.node.type === CanonicalType.CONTEXT; 320 + // Prefer CONTEXT parents at the same depth, but accept any type 321 + if (candidate.depth > bestDepth || (isContext && !bestIsContext)) { 322 + bestParent = candidate.node; 323 + bestDepth = candidate.depth; 324 + bestIsContext = isContext; 324 325 } 325 326 } 326 327
+14 -2
src/sentence-segmenter.ts
··· 31 31 for (const line of lines) { 32 32 const trimmed = line.trim(); 33 33 34 - // Skip headings 35 - if (/^#{1,6}\s/.test(trimmed)) continue; 34 + // Extract heading text as a sentence (provides section context) 35 + const headingMatch = trimmed.match(/^#{1,6}\s+(.*)/); 36 + if (headingMatch) { 37 + if (proseBuffer) { 38 + flushProse(proseBuffer, sentences, idx); 39 + idx = sentences.length; 40 + proseBuffer = ''; 41 + } 42 + const headingText = headingMatch[1].trim(); 43 + if (headingText.length >= CONFIG.MIN_LIST_ITEM_LENGTH) { 44 + sentences.push({ text: headingText, index: idx++, fromList: false }); 45 + } 46 + continue; 47 + } 36 48 37 49 // Skip empty lines — flush prose buffer 38 50 if (!trimmed) {
+6 -6
tests/eval/gold-standard.ts
··· 33 33 docId: 'spec-auth.md', 34 34 expectedMinCoverage: 80, 35 35 expectedMinNodes: 8, 36 - expectedMaxNodes: 15, 36 + expectedMaxNodes: 22, 37 37 expectedNodes: [ 38 38 { statement: 'authenticate with email', type: 'REQUIREMENT' }, 39 39 { statement: 'rate-limited', type: 'CONSTRAINT' }, ··· 50 50 docId: 'spec-auth.md', 51 51 expectedMinCoverage: 80, 52 52 expectedMinNodes: 8, 53 - expectedMaxNodes: 18, 53 + expectedMaxNodes: 26, 54 54 expectedNodes: [ 55 55 { statement: 'authenticate', type: 'REQUIREMENT' }, 56 56 { statement: 'oauth', type: 'REQUIREMENT' }, ··· 81 81 docId: 'spec/gateway.md', 82 82 expectedMinCoverage: 85, 83 83 expectedMinNodes: 15, 84 - expectedMaxNodes: 25, 84 + expectedMaxNodes: 32, 85 85 expectedNodes: [ 86 86 { statement: 'route requests to backend', type: 'REQUIREMENT' }, 87 87 { statement: 'rate-limited to 100', type: 'CONSTRAINT' }, ··· 143 143 expectedMinNodes: 8, 144 144 expectedMaxNodes: 20, 145 145 expectedNodes: [ 146 - { statement: 'websocket', type: 'REQUIREMENT' }, 146 + { statement: 'websocket', type: 'CONTEXT' }, 147 147 { statement: 'maximum 20', type: 'CONSTRAINT' }, 148 148 { statement: 'disconnected', type: 'REQUIREMENT' }, 149 149 { statement: 'room_full', type: 'REQUIREMENT' }, ··· 158 158 expectedMinNodes: 12, 159 159 expectedMaxNodes: 30, 160 160 expectedNodes: [ 161 - { statement: 'unique id', type: 'REQUIREMENT' }, 161 + { statement: 'unique expense id', type: 'REQUIREMENT' }, 162 162 { statement: 'positive', type: 'CONSTRAINT' }, 163 163 { statement: 'equal', type: 'REQUIREMENT' }, 164 164 { statement: 'remainder', type: 'INVARIANT' }, 165 165 { statement: 'sum of all individual shares must always equal', type: 'INVARIANT' }, 166 166 { statement: 'reverse chronological', type: 'REQUIREMENT' }, 167 - { statement: 'only creator can delete', type: 'CONSTRAINT' }, 167 + { statement: 'member who created', type: 'REQUIREMENT' }, 168 168 { statement: 'deterministic', type: 'INVARIANT' }, 169 169 ], 170 170 expectedEdges: [],
+9 -6
tests/unit/sentence-segmenter.test.ts
··· 5 5 it('splits list items into separate sentences', () => { 6 6 const text = '## Requirements\n\n- Users must log in\n- Sessions must expire\n- Passwords must be hashed'; 7 7 const sentences = segmentSentences(text); 8 - expect(sentences).toHaveLength(3); 9 - expect(sentences[0].text).toContain('Users must log in'); 10 - expect(sentences[1].text).toContain('Sessions must expire'); 11 - expect(sentences[2].text).toContain('Passwords must be hashed'); 12 - expect(sentences.every(s => s.fromList)).toBe(true); 8 + expect(sentences).toHaveLength(4); // heading + 3 list items 9 + expect(sentences[0].text).toBe('Requirements'); 10 + expect(sentences[1].text).toContain('Users must log in'); 11 + expect(sentences[2].text).toContain('Sessions must expire'); 12 + expect(sentences[3].text).toContain('Passwords must be hashed'); 13 + expect(sentences.slice(1).every(s => s.fromList)).toBe(true); 13 14 }); 14 15 15 16 it('splits prose into sentences', () => { ··· 32 33 expect(sentences.length).toBe(2); 33 34 }); 34 35 35 - it('skips headings', () => { 36 + it('extracts heading text without # markers', () => { 36 37 const text = '# Title\n\n## Section\n\nContent here.'; 37 38 const sentences = segmentSentences(text); 38 39 expect(sentences.every(s => !s.text.startsWith('#'))).toBe(true); 40 + expect(sentences.some(s => s.text === 'Title')).toBe(true); 41 + expect(sentences.some(s => s.text === 'Section')).toBe(true); 39 42 }); 40 43 41 44 it('skips very short content', () => {