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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +9 -1
- package/README.md +57 -61
- package/agents/a11y-architect.md +2 -0
- package/agents/briefing-officer.md +172 -0
- package/agents/business-model.md +14 -12
- package/agents/code-architect.md +6 -1
- package/agents/code-reviewer.md +3 -2
- package/agents/code-simplifier.md +12 -4
- package/agents/design-brand-guardian.md +19 -0
- package/agents/design-critic.md +16 -11
- package/agents/design-inclusive-visuals-specialist.md +2 -0
- package/agents/design-ui-designer.md +17 -0
- package/agents/design-ux-architect.md +15 -0
- package/agents/design-ux-researcher.md +102 -7
- package/agents/engineering-ai-engineer.md +2 -0
- package/agents/engineering-backend-architect.md +2 -0
- package/agents/engineering-data-engineer.md +2 -0
- package/agents/engineering-devops-automator.md +2 -0
- package/agents/engineering-frontend-developer.md +13 -0
- package/agents/engineering-mobile-app-builder.md +2 -0
- package/agents/engineering-rapid-prototyper.md +15 -2
- package/agents/engineering-security-engineer.md +2 -0
- package/agents/engineering-senior-developer.md +13 -0
- package/agents/engineering-sre.md +2 -0
- package/agents/engineering-technical-writer.md +2 -0
- package/agents/feature-intel.md +8 -7
- package/agents/ios-app-review-guardian.md +2 -0
- package/agents/ios-foundation-models-specialist.md +2 -0
- package/agents/ios-product-reality-auditor.md +292 -0
- package/agents/ios-storekit-specialist.md +2 -0
- package/agents/ios-swift-architect.md +1 -0
- package/agents/ios-swift-search.md +1 -0
- package/agents/ios-swift-ui-design.md +7 -4
- package/agents/marketing-app-store-optimizer.md +2 -0
- package/agents/planner.md +6 -1
- package/agents/pr-test-analyzer.md +3 -2
- package/agents/product-feedback-synthesizer.md +62 -0
- package/agents/product-owner.md +163 -0
- package/agents/product-reality-auditor.md +216 -0
- package/agents/product-spec-writer.md +176 -0
- package/agents/refactor-cleaner.md +9 -1
- package/agents/security-reviewer.md +2 -1
- package/agents/silent-failure-hunter.md +2 -1
- package/agents/swift-build-resolver.md +2 -0
- package/agents/swift-reviewer.md +2 -1
- package/agents/tech-feasibility.md +5 -3
- package/agents/testing-api-tester.md +2 -0
- package/agents/testing-evidence-collector.md +24 -0
- package/agents/testing-performance-benchmarker.md +2 -0
- package/agents/testing-reality-checker.md +2 -1
- package/agents/visual-research.md +7 -5
- package/bin/adapters/scribe-tool.ts +4 -2
- package/bin/adapters/write-lease-tool.ts +1 -1
- package/bin/buildanything-runtime.ts +20 -107
- package/bin/graph-index.js +24 -0
- package/bin/graph-index.ts +340 -0
- package/bin/mcp-servers/graph-mcp.js +26 -0
- package/bin/mcp-servers/graph-mcp.ts +481 -0
- package/bin/mcp-servers/orchestrator-mcp.js +26 -0
- package/bin/mcp-servers/orchestrator-mcp.ts +361 -0
- package/bin/setup.js +272 -111
- package/commands/build.md +424 -177
- package/commands/idea-sweep.md +2 -2
- package/commands/setup.md +15 -4
- package/commands/ux-review.md +3 -3
- package/commands/verify.md +3 -0
- package/docs/migration/phase-graph.yaml +573 -157
- package/hooks/design-md-lint +4 -0
- package/hooks/design-md-lint.ts +295 -0
- package/hooks/pre-tool-use.ts +37 -6
- package/hooks/record-mode-transitions.ts +63 -6
- package/hooks/subagent-start.ts +3 -2
- package/package.json +3 -1
- package/protocols/agent-prompt-authoring.md +165 -0
- package/protocols/architecture-schema.md +10 -3
- package/protocols/cleanup.md +4 -0
- package/protocols/decision-log.md +8 -4
- package/protocols/design-md-authoring.md +520 -0
- package/protocols/design-md-spec.md +362 -0
- package/protocols/fake-data-detector.md +1 -1
- package/protocols/ios-fake-data-detector.md +65 -0
- package/protocols/ios-phase-branches.md +128 -43
- package/protocols/launch-readiness.md +9 -5
- package/protocols/metric-loop.md +1 -1
- package/protocols/page-spec-schema.md +234 -0
- package/protocols/product-spec-schema.md +354 -0
- package/protocols/sprint-tasks-schema.md +53 -0
- package/protocols/state-schema.json +38 -3
- package/protocols/state-schema.md +32 -2
- package/protocols/verify.md +29 -1
- package/protocols/web-phase-branches.md +246 -76
- package/skills/ios/ios-bootstrap/SKILL.md +1 -1
- package/src/graph/ids.ts +86 -0
- package/src/graph/index.ts +32 -0
- package/src/graph/parser/architecture.ts +603 -0
- package/src/graph/parser/component-manifest.ts +268 -0
- package/src/graph/parser/decisions-jsonl.ts +407 -0
- package/src/graph/parser/design-md-pass2.ts +253 -0
- package/src/graph/parser/design-md.ts +477 -0
- package/src/graph/parser/page-spec.ts +496 -0
- package/src/graph/parser/product-spec.ts +930 -0
- package/src/graph/parser/screenshot.ts +342 -0
- package/src/graph/parser/sprint-tasks.ts +317 -0
- package/src/graph/storage/index.ts +1154 -0
- package/src/graph/types.ts +432 -0
- package/src/graph/util/dhash.ts +84 -0
- package/src/lrr/aggregator.ts +105 -10
- package/src/orchestrator/hooks/context-header.ts +34 -10
- package/src/orchestrator/hooks/token-accounting.ts +25 -14
- package/src/orchestrator/mcp/cycle-counter.ts +2 -1
- package/src/orchestrator/mcp/scribe.ts +27 -16
- package/src/orchestrator/mcp/write-lease.ts +30 -13
- package/src/orchestrator/phase4-shared-context.ts +20 -4
- 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
|
+
}
|