buildanything 2.0.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +9 -1
  3. package/README.md +57 -61
  4. package/agents/a11y-architect.md +2 -0
  5. package/agents/briefing-officer.md +172 -0
  6. package/agents/business-model.md +14 -12
  7. package/agents/code-architect.md +6 -1
  8. package/agents/code-reviewer.md +3 -2
  9. package/agents/code-simplifier.md +12 -4
  10. package/agents/design-brand-guardian.md +19 -0
  11. package/agents/design-critic.md +16 -11
  12. package/agents/design-inclusive-visuals-specialist.md +2 -0
  13. package/agents/design-ui-designer.md +17 -0
  14. package/agents/design-ux-architect.md +15 -0
  15. package/agents/design-ux-researcher.md +102 -7
  16. package/agents/engineering-ai-engineer.md +2 -0
  17. package/agents/engineering-backend-architect.md +2 -0
  18. package/agents/engineering-data-engineer.md +2 -0
  19. package/agents/engineering-devops-automator.md +2 -0
  20. package/agents/engineering-frontend-developer.md +13 -0
  21. package/agents/engineering-mobile-app-builder.md +2 -0
  22. package/agents/engineering-rapid-prototyper.md +15 -2
  23. package/agents/engineering-security-engineer.md +2 -0
  24. package/agents/engineering-senior-developer.md +13 -0
  25. package/agents/engineering-sre.md +2 -0
  26. package/agents/engineering-technical-writer.md +2 -0
  27. package/agents/feature-intel.md +8 -7
  28. package/agents/ios-app-review-guardian.md +2 -0
  29. package/agents/ios-foundation-models-specialist.md +2 -0
  30. package/agents/ios-product-reality-auditor.md +292 -0
  31. package/agents/ios-storekit-specialist.md +2 -0
  32. package/agents/ios-swift-architect.md +1 -0
  33. package/agents/ios-swift-search.md +1 -0
  34. package/agents/ios-swift-ui-design.md +7 -4
  35. package/agents/marketing-app-store-optimizer.md +2 -0
  36. package/agents/planner.md +6 -1
  37. package/agents/pr-test-analyzer.md +3 -2
  38. package/agents/product-feedback-synthesizer.md +62 -0
  39. package/agents/product-owner.md +163 -0
  40. package/agents/product-reality-auditor.md +216 -0
  41. package/agents/product-spec-writer.md +176 -0
  42. package/agents/refactor-cleaner.md +9 -1
  43. package/agents/security-reviewer.md +2 -1
  44. package/agents/silent-failure-hunter.md +2 -1
  45. package/agents/swift-build-resolver.md +2 -0
  46. package/agents/swift-reviewer.md +2 -1
  47. package/agents/tech-feasibility.md +5 -3
  48. package/agents/testing-api-tester.md +2 -0
  49. package/agents/testing-evidence-collector.md +24 -0
  50. package/agents/testing-performance-benchmarker.md +2 -0
  51. package/agents/testing-reality-checker.md +2 -1
  52. package/agents/visual-research.md +7 -5
  53. package/bin/adapters/scribe-tool.ts +4 -2
  54. package/bin/adapters/write-lease-tool.ts +1 -1
  55. package/bin/buildanything-runtime.ts +20 -107
  56. package/bin/graph-index.js +24 -0
  57. package/bin/graph-index.ts +340 -0
  58. package/bin/mcp-servers/graph-mcp.js +26 -0
  59. package/bin/mcp-servers/graph-mcp.ts +481 -0
  60. package/bin/mcp-servers/orchestrator-mcp.js +26 -0
  61. package/bin/mcp-servers/orchestrator-mcp.ts +361 -0
  62. package/bin/setup.js +272 -111
  63. package/commands/build.md +424 -177
  64. package/commands/idea-sweep.md +2 -2
  65. package/commands/setup.md +15 -4
  66. package/commands/ux-review.md +3 -3
  67. package/commands/verify.md +3 -0
  68. package/docs/migration/phase-graph.yaml +573 -157
  69. package/hooks/design-md-lint +4 -0
  70. package/hooks/design-md-lint.ts +295 -0
  71. package/hooks/pre-tool-use.ts +37 -6
  72. package/hooks/record-mode-transitions.ts +63 -6
  73. package/hooks/subagent-start.ts +3 -2
  74. package/package.json +3 -1
  75. package/protocols/agent-prompt-authoring.md +165 -0
  76. package/protocols/architecture-schema.md +10 -3
  77. package/protocols/cleanup.md +4 -0
  78. package/protocols/decision-log.md +8 -4
  79. package/protocols/design-md-authoring.md +520 -0
  80. package/protocols/design-md-spec.md +362 -0
  81. package/protocols/fake-data-detector.md +1 -1
  82. package/protocols/ios-fake-data-detector.md +65 -0
  83. package/protocols/ios-phase-branches.md +128 -43
  84. package/protocols/launch-readiness.md +9 -5
  85. package/protocols/metric-loop.md +1 -1
  86. package/protocols/page-spec-schema.md +234 -0
  87. package/protocols/product-spec-schema.md +354 -0
  88. package/protocols/sprint-tasks-schema.md +53 -0
  89. package/protocols/state-schema.json +38 -3
  90. package/protocols/state-schema.md +32 -2
  91. package/protocols/verify.md +29 -1
  92. package/protocols/web-phase-branches.md +246 -76
  93. package/skills/ios/ios-bootstrap/SKILL.md +1 -1
  94. package/src/graph/ids.ts +86 -0
  95. package/src/graph/index.ts +32 -0
  96. package/src/graph/parser/architecture.ts +603 -0
  97. package/src/graph/parser/component-manifest.ts +268 -0
  98. package/src/graph/parser/decisions-jsonl.ts +407 -0
  99. package/src/graph/parser/design-md-pass2.ts +253 -0
  100. package/src/graph/parser/design-md.ts +477 -0
  101. package/src/graph/parser/page-spec.ts +496 -0
  102. package/src/graph/parser/product-spec.ts +930 -0
  103. package/src/graph/parser/screenshot.ts +342 -0
  104. package/src/graph/parser/sprint-tasks.ts +317 -0
  105. package/src/graph/storage/index.ts +1154 -0
  106. package/src/graph/types.ts +432 -0
  107. package/src/graph/util/dhash.ts +84 -0
  108. package/src/lrr/aggregator.ts +105 -10
  109. package/src/orchestrator/hooks/context-header.ts +34 -10
  110. package/src/orchestrator/hooks/token-accounting.ts +25 -14
  111. package/src/orchestrator/mcp/cycle-counter.ts +2 -1
  112. package/src/orchestrator/mcp/scribe.ts +27 -16
  113. package/src/orchestrator/mcp/write-lease.ts +30 -13
  114. package/src/orchestrator/phase4-shared-context.ts +20 -4
  115. package/protocols/visual-dna.md +0 -185
@@ -0,0 +1,603 @@
1
+ import { ids, kebab, sha256Hex } from "../ids.js";
2
+ import type {
3
+ ApiContractNode,
4
+ ArchitectureModuleNode,
5
+ Confidence,
6
+ DataModelNode,
7
+ ExtractError,
8
+ ExtractResult,
9
+ GraphEdge,
10
+ GraphFragment,
11
+ GraphNode,
12
+ Relation,
13
+ } from "../types.js";
14
+
15
+ const PRODUCED_BY = "code-architect";
16
+ const PRODUCED_AT_STEP = "2.3.1";
17
+
18
+ const SKIP_HEADINGS = new Set(["overview", "scope", "out of scope"]);
19
+ const REQUIRED_MODULE_NAMES = ["frontend", "backend", "auth", "data model", "security", "infrastructure"];
20
+
21
+ const ENDPOINT_RE = /^\*\*(GET|POST|PUT|PATCH|DELETE)\s+(\/[^\s*]+)\*\*(.*)$/;
22
+
23
+ // Words that should never be treated as a feature hint when scanning path
24
+ // segments. Common HTTP/REST nouns and path-parameter placeholders.
25
+ const PATH_STOP_WORDS = new Set([
26
+ "api",
27
+ "v1",
28
+ "v2",
29
+ "v3",
30
+ "id",
31
+ "ids",
32
+ "uuid",
33
+ "list",
34
+ "new",
35
+ "edit",
36
+ "create",
37
+ "update",
38
+ "delete",
39
+ "search",
40
+ "me",
41
+ "self",
42
+ ]);
43
+
44
+ interface Line {
45
+ n: number;
46
+ text: string;
47
+ }
48
+
49
+ interface Section {
50
+ heading: string;
51
+ level: number;
52
+ startLine: number;
53
+ bodyLines: Line[];
54
+ }
55
+
56
+ interface Ctx {
57
+ mdPath: string;
58
+ errors: ExtractError[];
59
+ nodes: GraphNode[];
60
+ edges: GraphEdge[];
61
+ }
62
+
63
+ function loc(line: number): string {
64
+ return `L${line}`;
65
+ }
66
+
67
+ function pushError(ctx: Ctx, line: number, message: string): void {
68
+ ctx.errors.push({ line, message });
69
+ }
70
+
71
+ function splitLines(content: string): Line[] {
72
+ return content.split(/\r?\n/).map((text, i) => ({ n: i + 1, text }));
73
+ }
74
+
75
+ function isHeadingAtLevel(text: string, level: number): boolean {
76
+ const prefix = "#".repeat(level) + " ";
77
+ if (!text.startsWith(prefix)) return false;
78
+ return text[level] !== "#";
79
+ }
80
+
81
+ function isHeadingAtOrAbove(text: string, level: number): boolean {
82
+ for (let l = 1; l <= level; l++) {
83
+ if (isHeadingAtLevel(text, l)) return true;
84
+ }
85
+ return false;
86
+ }
87
+
88
+ function partitionSections(lines: Line[], level: number, start: number, end: number): Section[] {
89
+ const sections: Section[] = [];
90
+ const headingPrefix = "#".repeat(level) + " ";
91
+ let i = start;
92
+ while (i < end) {
93
+ const line = lines[i];
94
+ if (isHeadingAtLevel(line.text, level)) {
95
+ const heading = line.text.slice(headingPrefix.length).trim();
96
+ const bodyStart = i + 1;
97
+ let j = bodyStart;
98
+ while (j < end && !isHeadingAtOrAbove(lines[j].text, level)) j++;
99
+ sections.push({ heading, level, startLine: line.n, bodyLines: lines.slice(bodyStart, j) });
100
+ i = j;
101
+ } else {
102
+ i++;
103
+ }
104
+ }
105
+ return sections;
106
+ }
107
+
108
+ function makeEdge(
109
+ ctx: Ctx,
110
+ source: string,
111
+ target: string,
112
+ relation: Relation,
113
+ line: number,
114
+ confidence: Confidence = "EXTRACTED",
115
+ ): GraphEdge {
116
+ return {
117
+ source,
118
+ target,
119
+ relation,
120
+ confidence,
121
+ source_file: ctx.mdPath,
122
+ source_location: loc(line),
123
+ produced_by_agent: PRODUCED_BY,
124
+ produced_at_step: PRODUCED_AT_STEP,
125
+ };
126
+ }
127
+
128
+ function sortNodes(nodes: GraphNode[]): GraphNode[] {
129
+ return [...nodes].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
130
+ }
131
+
132
+ function sortEdges(edges: GraphEdge[]): GraphEdge[] {
133
+ return [...edges].sort((a, b) => {
134
+ const k = (e: GraphEdge): string =>
135
+ `${e.relation} ${e.source} ${e.target} ${e.source_location ?? ""}`;
136
+ return k(a) < k(b) ? -1 : k(a) > k(b) ? 1 : 0;
137
+ });
138
+ }
139
+
140
+ function isTitleHeading(text: string): boolean {
141
+ return /^architecture\s*:/i.test(text);
142
+ }
143
+
144
+ function extractDescription(bodyLines: Line[]): string {
145
+ const parts: string[] = [];
146
+ for (const line of bodyLines) {
147
+ if (isHeadingAtOrAbove(line.text, 2)) break;
148
+ const t = line.text.trim();
149
+ if (t === "") {
150
+ if (parts.length > 0) break;
151
+ continue;
152
+ }
153
+ parts.push(t);
154
+ }
155
+ return parts.join(" ");
156
+ }
157
+
158
+ function extractBulletsUnderH3(bodyLines: Line[], h3Name: string): string[] {
159
+ let inSection = false;
160
+ const bullets: string[] = [];
161
+ for (const line of bodyLines) {
162
+ if (isHeadingAtLevel(line.text, 3)) {
163
+ inSection = line.text.slice(4).trim().toLowerCase() === h3Name.toLowerCase();
164
+ continue;
165
+ }
166
+ if (inSection) {
167
+ if (isHeadingAtOrAbove(line.text, 3)) break;
168
+ const m = line.text.match(/^\s*-\s+(.+)$/);
169
+ if (m) bullets.push(m[1].trim());
170
+ }
171
+ }
172
+ return bullets;
173
+ }
174
+
175
+ function splitParenAware(value: string): string[] {
176
+ const parts: string[] = [];
177
+ let depth = 0;
178
+ let current = "";
179
+ for (const ch of value) {
180
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
181
+ else if (ch === ")" || ch === "]" || ch === "}") depth--;
182
+ if (ch === "," && depth === 0) {
183
+ parts.push(current);
184
+ current = "";
185
+ } else {
186
+ current += ch;
187
+ }
188
+ }
189
+ parts.push(current);
190
+ return parts.map((s) => s.trim()).filter((s) => s.length > 0);
191
+ }
192
+
193
+ // Feature-attribution heuristics for api_contract nodes.
194
+ //
195
+ // The parser only sees architecture.md, not product-spec.md, so it cannot
196
+ // validate that the inferred feature_id corresponds to a real Slice 1 feature
197
+ // node. Edges are emitted optimistically against `feature__{kebab(name)}` IDs;
198
+ // the merged graph in loadAllGraphs resolves them when the Slice 1 fragment
199
+ // is also present. Unmatched edges are tolerable — queryDependencies and
200
+ // queryCrossContracts simply return empty arrays for unknown features.
201
+ //
202
+ // Edge emission precedence (highest to lowest confidence):
203
+ // 1. Explicit annotation: `(provides: x)` / `(consumes: y)` → EXTRACTED
204
+ // 2. Path-segment inference (provides only): first non-stopword segment → INFERRED
205
+ // 3. Module-name match (provides only): module kebab matches a feature → INFERRED
206
+ //
207
+ // Prose heuristics ("provided by X", "consumed by X") removed 2026-04-30
208
+ // after ultrareview flagged them as a major false-positive source.
209
+
210
+ interface EndpointAnnotation {
211
+ provides: string[];
212
+ consumes: string[];
213
+ }
214
+
215
+ function parseEndpointAnnotation(trailing: string): EndpointAnnotation {
216
+ const provides: string[] = [];
217
+ const consumes: string[] = [];
218
+ const annotationRe = /\(\s*(provides|consumes)\s*:\s*([^)]+)\)/gi;
219
+ let m: RegExpExecArray | null;
220
+ while ((m = annotationRe.exec(trailing)) !== null) {
221
+ const kind = m[1].toLowerCase();
222
+ const targets = m[2]
223
+ .split(",")
224
+ .map((s) => kebab(s.trim()))
225
+ .filter((s) => s.length > 0);
226
+ if (kind === "provides") provides.push(...targets);
227
+ else consumes.push(...targets);
228
+ }
229
+ return { provides, consumes };
230
+ }
231
+
232
+ function inferFeatureFromPath(path: string): string | null {
233
+ const segments = path
234
+ .replace(/^\/+/, "")
235
+ .split("/")
236
+ .map((s) => s.replace(/^[:{].*[}]?$/, "").trim())
237
+ .filter((s) => s.length > 0)
238
+ .map((s) => kebab(s));
239
+ for (const seg of segments) {
240
+ if (seg && !PATH_STOP_WORDS.has(seg)) return seg;
241
+ }
242
+ return null;
243
+ }
244
+
245
+ function inferFeatureFromModuleName(moduleName: string): string | null {
246
+ const kebabbed = kebab(moduleName);
247
+ const GENERIC = new Set([
248
+ "frontend",
249
+ "backend",
250
+ "auth",
251
+ "data-model",
252
+ "security",
253
+ "infrastructure",
254
+ "api",
255
+ ]);
256
+ if (GENERIC.has(kebabbed)) return null;
257
+ return kebabbed;
258
+ }
259
+
260
+ /**
261
+ * Edge emission precedence (highest to lowest confidence):
262
+ * 1. Explicit annotation: `**POST /api/orders** (provides: checkout)` → confidence: EXTRACTED
263
+ * 2. Path inference: `**POST /api/checkout**` in any module → confidence: INFERRED
264
+ * 3. Module-name match: any endpoint in a module named after a feature → confidence: INFERRED
265
+ *
266
+ * Prose heuristics ("provided by X", "consumed by X") removed as of 2026-04-30
267
+ * after ultrareview flagged them as a major false-positive source.
268
+ */
269
+ function emitFeatureEdges(
270
+ ctx: Ctx,
271
+ contractId: string,
272
+ line: number,
273
+ explicit: EndpointAnnotation,
274
+ inferred: EndpointAnnotation,
275
+ ): void {
276
+ const seenProvides = new Set<string>();
277
+ for (const f of explicit.provides) {
278
+ if (!f || seenProvides.has(f)) continue;
279
+ seenProvides.add(f);
280
+ ctx.edges.push(
281
+ makeEdge(ctx, `feature__${f}`, contractId, "feature_provides_endpoint", line, "EXTRACTED"),
282
+ );
283
+ }
284
+ const seenConsumes = new Set<string>();
285
+ for (const f of explicit.consumes) {
286
+ if (!f || seenConsumes.has(f)) continue;
287
+ seenConsumes.add(f);
288
+ ctx.edges.push(
289
+ makeEdge(ctx, `feature__${f}`, contractId, "feature_consumes_endpoint", line, "EXTRACTED"),
290
+ );
291
+ }
292
+ for (const f of inferred.provides) {
293
+ if (!f || seenProvides.has(f)) continue;
294
+ seenProvides.add(f);
295
+ ctx.edges.push(
296
+ makeEdge(ctx, `feature__${f}`, contractId, "feature_provides_endpoint", line, "INFERRED"),
297
+ );
298
+ }
299
+ for (const f of inferred.consumes) {
300
+ if (!f || seenConsumes.has(f)) continue;
301
+ seenConsumes.add(f);
302
+ ctx.edges.push(
303
+ makeEdge(ctx, `feature__${f}`, contractId, "feature_consumes_endpoint", line, "INFERRED"),
304
+ );
305
+ }
306
+ }
307
+
308
+ function isApiContractHeading(heading: string): boolean {
309
+ const lower = heading.toLowerCase();
310
+ return (
311
+ lower.includes("api contract") ||
312
+ lower.includes("api contracts") ||
313
+ lower.includes("api endpoints") ||
314
+ lower === "api"
315
+ );
316
+ }
317
+
318
+ function parseApiContracts(
319
+ ctx: Ctx,
320
+ bodyLines: Line[],
321
+ moduleId: string,
322
+ moduleLine: number,
323
+ moduleName: string,
324
+ ): void {
325
+ const h2Sections = partitionBodyH2(bodyLines);
326
+ for (const sec of h2Sections) {
327
+ if (!isApiContractHeading(sec.heading)) continue;
328
+ parseEndpointsInSection(ctx, sec.bodyLines, moduleId, moduleName);
329
+ }
330
+ }
331
+
332
+ function partitionBodyH2(bodyLines: Line[]): Section[] {
333
+ const sections: Section[] = [];
334
+ let i = 0;
335
+ while (i < bodyLines.length) {
336
+ const line = bodyLines[i];
337
+ if (isHeadingAtLevel(line.text, 2)) {
338
+ const heading = line.text.slice(3).trim();
339
+ const start = i + 1;
340
+ let j = start;
341
+ while (j < bodyLines.length && !isHeadingAtOrAbove(bodyLines[j].text, 2)) j++;
342
+ sections.push({ heading, level: 2, startLine: line.n, bodyLines: bodyLines.slice(start, j) });
343
+ i = j;
344
+ } else {
345
+ i++;
346
+ }
347
+ }
348
+ return sections;
349
+ }
350
+
351
+ function parseEndpointsInSection(
352
+ ctx: Ctx,
353
+ lines: Line[],
354
+ moduleId: string,
355
+ moduleName: string,
356
+ ): void {
357
+ const endpointStarts: number[] = [];
358
+ for (let i = 0; i < lines.length; i++) {
359
+ if (ENDPOINT_RE.test(lines[i].text.trim())) endpointStarts.push(i);
360
+ }
361
+
362
+ for (let ei = 0; ei < endpointStarts.length; ei++) {
363
+ const startIdx = endpointStarts[ei];
364
+ const endIdx = ei + 1 < endpointStarts.length ? endpointStarts[ei + 1] : lines.length;
365
+ const headLine = lines[startIdx];
366
+ const m = headLine.text.trim().match(ENDPOINT_RE)!;
367
+ const method = m[1];
368
+ const path = m[2];
369
+ const trailing = m[3] ?? "";
370
+ const endpoint = `${method} ${path}`;
371
+ const block = lines.slice(startIdx + 1, endIdx);
372
+
373
+ let authRequired = false;
374
+ let errorCodes: string[] = [];
375
+ let requestSchema = "";
376
+ let responseSchema = "";
377
+
378
+ for (const line of block) {
379
+ const t = line.text.trim();
380
+ const authMatch = t.match(/^-\s+Auth\s+required\s*:\s*(.+)$/i);
381
+ if (authMatch) {
382
+ authRequired = /yes/i.test(authMatch[1]);
383
+ continue;
384
+ }
385
+ const errorMatch = t.match(/^-\s+Error\s+codes\s*:\s*(.+)$/i);
386
+ if (errorMatch) {
387
+ errorCodes = splitParenAware(errorMatch[1]).map((s) => s.trim()).filter((s) => s.length > 0);
388
+ continue;
389
+ }
390
+ const reqMatch = t.match(/^-\s+Request\s*:\s*(.+)$/i);
391
+ if (reqMatch) {
392
+ requestSchema = reqMatch[1].trim().replace(/^`+|`+$/g, "").trim();
393
+ continue;
394
+ }
395
+ const resMatch = t.match(/^-\s+Response\s*:\s*(.+)$/i);
396
+ if (resMatch) {
397
+ responseSchema = resMatch[1].trim().replace(/^`+|`+$/g, "").trim();
398
+ continue;
399
+ }
400
+ }
401
+
402
+ const node: ApiContractNode = {
403
+ id: ids.apiContract(endpoint),
404
+ label: endpoint,
405
+ entity_type: "api_contract",
406
+ source_file: ctx.mdPath,
407
+ source_location: loc(headLine.n),
408
+ confidence: "EXTRACTED",
409
+ endpoint,
410
+ module_id: moduleId,
411
+ request_schema: requestSchema,
412
+ response_schema: responseSchema,
413
+ auth_required: authRequired,
414
+ error_codes: errorCodes,
415
+ };
416
+ ctx.nodes.push(node);
417
+ ctx.edges.push(makeEdge(ctx, moduleId, node.id, "module_has_contract", headLine.n));
418
+
419
+ const explicit = parseEndpointAnnotation(trailing);
420
+ const inferred: EndpointAnnotation = { provides: [], consumes: [] };
421
+ if (explicit.provides.length === 0 && explicit.consumes.length === 0) {
422
+ const pathHint = inferFeatureFromPath(path);
423
+ if (pathHint) inferred.provides.push(pathHint);
424
+ const moduleHint = inferFeatureFromModuleName(moduleName);
425
+ if (moduleHint) inferred.provides.push(moduleHint);
426
+ }
427
+ emitFeatureEdges(ctx, node.id, headLine.n, explicit, inferred);
428
+ }
429
+ }
430
+
431
+ function parseDataModels(ctx: Ctx, bodyLines: Line[], moduleId: string): void {
432
+ const entityRe = /^\*\*([A-Za-z][A-Za-z0-9_]*)\*\*\s*$/;
433
+ const entityStarts: number[] = [];
434
+ for (let i = 0; i < bodyLines.length; i++) {
435
+ if (entityRe.test(bodyLines[i].text.trim())) entityStarts.push(i);
436
+ }
437
+
438
+ for (let ei = 0; ei < entityStarts.length; ei++) {
439
+ const startIdx = entityStarts[ei];
440
+ const endIdx = ei + 1 < entityStarts.length ? entityStarts[ei + 1] : bodyLines.length;
441
+ const headLine = bodyLines[startIdx];
442
+ const entityName = headLine.text.trim().match(entityRe)![1];
443
+
444
+ // Scan until next entity, h2, or h1
445
+ let blockEnd = endIdx;
446
+ for (let j = startIdx + 1; j < endIdx; j++) {
447
+ if (isHeadingAtOrAbove(bodyLines[j].text, 2)) {
448
+ blockEnd = j;
449
+ break;
450
+ }
451
+ }
452
+ const block = bodyLines.slice(startIdx + 1, blockEnd);
453
+
454
+ let fields: string[] = [];
455
+ let indexes: string[] = [];
456
+
457
+ for (const line of block) {
458
+ const t = line.text.trim();
459
+ const fieldsMatch = t.match(/^-\s+Fields\s*:\s*(.+)$/i);
460
+ if (fieldsMatch) {
461
+ const raw = fieldsMatch[1];
462
+ const parts = splitParenAware(raw);
463
+ fields = parts
464
+ .map((part) => {
465
+ const colonIdx = part.indexOf(":");
466
+ if (colonIdx < 0) return "";
467
+ const name = part.slice(0, colonIdx).trim();
468
+ let type = part.slice(colonIdx + 1).trim();
469
+ const parenIdx = type.indexOf("(");
470
+ if (parenIdx >= 0) type = type.slice(0, parenIdx).trim();
471
+ if (!name || !type) return "";
472
+ return `${name}:${type}`;
473
+ })
474
+ .filter((s) => s.length > 0);
475
+ continue;
476
+ }
477
+ const indexMatch = t.match(/^-\s+Indexes\s*:\s*(.+)$/i);
478
+ if (indexMatch) {
479
+ const raw = indexMatch[1];
480
+ const parts = splitParenAware(raw);
481
+ indexes = parts
482
+ .map((part) => {
483
+ const parenIdx = part.indexOf("(");
484
+ return (parenIdx >= 0 ? part.slice(0, parenIdx) : part).trim();
485
+ })
486
+ .filter((s) => s.length > 0);
487
+ continue;
488
+ }
489
+ }
490
+
491
+ const node: DataModelNode = {
492
+ id: ids.dataModel(entityName),
493
+ label: entityName,
494
+ entity_type: "data_model",
495
+ source_file: ctx.mdPath,
496
+ source_location: loc(headLine.n),
497
+ confidence: "EXTRACTED",
498
+ entity_name: entityName,
499
+ module_id: moduleId,
500
+ fields,
501
+ indexes,
502
+ };
503
+ ctx.nodes.push(node);
504
+ ctx.edges.push(makeEdge(ctx, moduleId, node.id, "module_has_data_model", headLine.n));
505
+ }
506
+ }
507
+
508
+ export function extractArchitecture(input: { mdPath: string; mdContent: string }): ExtractResult {
509
+ const { mdPath, mdContent } = input;
510
+ const lines = splitLines(mdContent);
511
+ const ctx: Ctx = { mdPath, errors: [], nodes: [], edges: [] };
512
+
513
+ // Collect h1 sections
514
+ const h1Sections = partitionSections(lines, 1, 0, lines.length);
515
+
516
+ // Also collect ## Module: Foo as module candidates
517
+ interface ModuleCandidate {
518
+ name: string;
519
+ startLine: number;
520
+ bodyLines: Line[];
521
+ }
522
+ const candidates: ModuleCandidate[] = [];
523
+
524
+ for (const sec of h1Sections) {
525
+ if (isTitleHeading(sec.heading)) continue;
526
+ if (SKIP_HEADINGS.has(sec.heading.toLowerCase())) continue;
527
+ candidates.push({ name: sec.heading, startLine: sec.startLine, bodyLines: sec.bodyLines });
528
+ }
529
+
530
+ // Also scan for ## Module: Foo
531
+ for (const sec of h1Sections) {
532
+ const h2s = partitionBodyH2(sec.bodyLines);
533
+ for (const h2 of h2s) {
534
+ const moduleMatch = h2.heading.match(/^Module\s*:\s*(.+)$/i);
535
+ if (moduleMatch) {
536
+ candidates.push({
537
+ name: moduleMatch[1].trim(),
538
+ startLine: h2.startLine,
539
+ bodyLines: h2.bodyLines,
540
+ });
541
+ }
542
+ }
543
+ }
544
+
545
+ // Check if any required module name appears
546
+ const candidateNamesLower = candidates.map((c) => c.name.toLowerCase());
547
+ const hasRequired = REQUIRED_MODULE_NAMES.some((req) =>
548
+ candidateNamesLower.some((cn) => cn.includes(req)),
549
+ );
550
+
551
+ if (!hasRequired) {
552
+ return {
553
+ ok: false,
554
+ errors: [
555
+ {
556
+ line: 1,
557
+ message:
558
+ "architecture.md has no recognizable module sections (none of: Frontend, Backend, Auth, Data Model, Security, Infrastructure)",
559
+ },
560
+ ],
561
+ };
562
+ }
563
+
564
+ for (const cand of candidates) {
565
+ const moduleId = ids.architectureModule(cand.name);
566
+ const description = extractDescription(cand.bodyLines);
567
+ const responsibilities = extractBulletsUnderH3(cand.bodyLines, "Responsibilities");
568
+ const techStack = extractBulletsUnderH3(cand.bodyLines, "Tech Stack");
569
+
570
+ const moduleNode: ArchitectureModuleNode = {
571
+ id: moduleId,
572
+ label: cand.name,
573
+ entity_type: "architecture_module",
574
+ source_file: mdPath,
575
+ source_location: loc(cand.startLine),
576
+ confidence: "EXTRACTED",
577
+ name: cand.name,
578
+ description,
579
+ responsibilities,
580
+ tech_stack: techStack,
581
+ };
582
+ ctx.nodes.push(moduleNode);
583
+
584
+ // Parse API contracts within this module
585
+ parseApiContracts(ctx, cand.bodyLines, moduleId, cand.startLine, cand.name);
586
+
587
+ // Parse data models if this is the Data Model module
588
+ if (cand.name.toLowerCase().includes("data model")) {
589
+ parseDataModels(ctx, cand.bodyLines, moduleId);
590
+ }
591
+ }
592
+
593
+ const fragment: GraphFragment = {
594
+ version: 1,
595
+ schema: "buildanything-slice-4",
596
+ source_file: mdPath,
597
+ source_sha: sha256Hex(mdContent),
598
+ produced_at: new Date().toISOString(),
599
+ nodes: sortNodes(ctx.nodes),
600
+ edges: sortEdges(ctx.edges),
601
+ };
602
+ return { ok: true, fragment, errors: [] };
603
+ }