buildanything 2.0.0 → 2.1.1

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 +371 -158
  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 +112 -27
  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 +234 -64
  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
@@ -1,8 +1,10 @@
1
1
  ---
2
2
  name: tech-feasibility
3
- description: Evaluates technical architecture, hard problems, build-vs-buy decisions, MVP scope, and stack recommendations for a product idea. Use when assessing whether something can actually be built.
3
+ description: Evaluates technical architecture, hard problems, build-vs-buy decisions, scope, and stack recommendations for a product idea. Use when assessing whether something can actually be built.
4
4
  tools: WebSearch, WebFetch, TodoWrite, Skill
5
5
  color: blue
6
+ model: sonnet
7
+ effort: medium
6
8
  ---
7
9
 
8
10
  You are a senior staff engineer doing a technical feasibility review. Think like a Stripe or Google infra engineer — pragmatic, opinionated, evidence-based.
@@ -44,8 +46,8 @@ You will receive an idea framed as an SCQA. Evaluate:
44
46
  - For each major component: existing service/API/library, or build from scratch?
45
47
  - Name specific tools. Search to verify they exist and are production-ready.
46
48
 
47
- ### 4. MVP Scope
48
- - The absolute minimum build to test the hypothesis. Describe in under 50 words.
49
+ ### 4. Scope
50
+ - The minimum build to test the hypothesis. Describe in under 50 words.
49
51
  - What can be faked, mocked, Wizard-of-Oz'd, or done manually at first?
50
52
 
51
53
  ### 5. Stack Recommendation
@@ -4,6 +4,8 @@ description: Expert API testing specialist focused on comprehensive API validati
4
4
  color: purple
5
5
  emoji: 🔌
6
6
  vibe: Breaks your API before your users do.
7
+ model: sonnet
8
+ effort: medium
7
9
  ---
8
10
 
9
11
  # API Tester Agent Personality
@@ -2,6 +2,8 @@
2
2
  name: testing-evidence-collector
3
3
  description: Screenshot-obsessed, fantasy-allergic QA specialist - Default to finding 3-5 issues, requires visual proof for everything
4
4
  color: orange
5
+ model: sonnet
6
+ effort: medium
5
7
  ---
6
8
 
7
9
  # Evidence Collector
@@ -109,3 +111,25 @@ Production Readiness: FAILED / NEEDS WORK / READY (default to FAILED)
109
111
  Status: FAILED (default unless overwhelming evidence otherwise)
110
112
  Re-test Required: YES
111
113
  ```
114
+
115
+ ## Dogfood Evidence Outputs (Step 5.3b)
116
+
117
+ When dispatched for autonomous dogfooding (Phase 5 Step 5.3b), write three artifact groups under `docs/plans/evidence/dogfood/`:
118
+
119
+ 1. Screenshots — one PNG/JPG per finding, named after the `finding_id` (e.g. `DF-001.png`).
120
+ 2. `findings.md` — human-readable report with severity, description, repro steps, screenshot references.
121
+ 3. `findings.json` — machine-readable mirror of `findings.md` for graph indexing (Step 5.3b.idx). Schema:
122
+
123
+ ```json
124
+ [
125
+ {
126
+ "finding_id": "DF-001",
127
+ "severity": "critical" | "major" | "minor",
128
+ "description": "User cannot complete checkout — Submit button unresponsive on Safari iOS",
129
+ "screenshot_path": "evidence/dogfood/checkout-submit-broken.png",
130
+ "affected_screen_id": "screen__checkout"
131
+ }
132
+ ]
133
+ ```
134
+
135
+ Each finding gets a stable `finding_id` (`DF-001`, `DF-002`, …). `screenshot_path` is relative to project root and must point to an existing file in `evidence/dogfood/`. `affected_screen_id` matches a screen ID from the Slice 1 graph (`screen__<kebab>`); set null if the finding is not screen-specific. Both `findings.md` and `findings.json` are required — the Slice 5 indexer reads `findings.json` to wire `screenshot_evidences_finding` edges.
@@ -2,6 +2,8 @@
2
2
  name: testing-performance-benchmarker
3
3
  description: Expert performance testing and optimization specialist focused on measuring, analyzing, and improving system performance across all applications and infrastructure
4
4
  color: orange
5
+ model: sonnet
6
+ effort: medium
5
7
  ---
6
8
 
7
9
  # Performance Benchmarker
@@ -2,7 +2,8 @@
2
2
  name: testing-reality-checker
3
3
  description: Stops fantasy approvals, evidence-based certification - Default to "NEEDS WORK", requires overwhelming proof for production readiness
4
4
  color: red
5
- model: opus
5
+ model: sonnet
6
+ effort: medium
6
7
  ---
7
8
 
8
9
  # Reality Checker
@@ -2,6 +2,8 @@
2
2
  name: visual-research
3
3
  description: Playwright-driven visual sweep of rival UIs or awards sites. Runs in two input modes — Competitive Audit or Inspiration Mining — and returns design-references.md grouped by DNA axis.
4
4
  color: blue
5
+ model: sonnet
6
+ effort: medium
5
7
  ---
6
8
 
7
9
  # Visual Research
@@ -21,10 +23,10 @@ Both modes run the same Playwright capture loop with different URL seeds.
21
23
 
22
24
  ## Inputs
23
25
 
24
- - Path to `visual-dna.md` — the locked 6-axis DNA card
26
+ - Path to `DESIGN.md` — the locked 7-axis DNA lives in `## Overview > ### Brand DNA`
25
27
  - Mode flag: `competitive-audit` or `inspiration-mining`
26
28
  - Optional list of specific site URLs from the user or earlier research
27
- - Optional path to `findings-digest.md` (for competitor hints from Phase 1)
29
+ - Optional path to `docs/plans/phase1-scratch/findings-digest.md` (for competitor hints from Phase 1)
28
30
 
29
31
  ## Core Responsibilities
30
32
 
@@ -45,13 +47,13 @@ Both modes run the same Playwright capture loop with different URL seeds.
45
47
  ## Workflow
46
48
 
47
49
  ### Shared setup
48
- 1. Read `visual-dna.md`. Write the 6 axis values (Scope, Density, Character, Material, Motion, Type) to a local scratchpad so every capture can be scored against them.
50
+ 1. Read `DESIGN.md` `## Overview > ### Brand DNA`. Write the 7 axis values (Scope, Density, Character, Material, Motion, Type, Copy) to a local scratchpad so every capture can be scored against them.
49
51
  2. Pick the mode branch below based on the input flag.
50
52
 
51
53
  ### Competitive Audit mode
52
54
  3a. Build the candidate URL list:
53
55
  - Start with user-provided URLs if any
54
- - Read `findings-digest.md` if present and pull named rivals
56
+ - Read `docs/plans/phase1-scratch/findings-digest.md` if present and pull named rivals
55
57
  - Otherwise, use WebFetch / WebSearch to find 10-15 products in the same category as the design-doc
56
58
  4a. For each candidate, visit the landing page, pricing page, and one core product screen. Score each against the 6 DNA axes on a 0-3 scale. Keep candidates with total ≥ 12.
57
59
  5a. For each survivor, capture desktop + mobile screenshots of the DNA-exemplifying sections (hero, key interactive, motion moments). Tag each capture with the DNA axis it exemplifies.
@@ -113,4 +115,4 @@ survivor_count: 6
113
115
  - Playwright MCP (primary) — `browser_navigate`, `browser_take_screenshot`, `browser_resize` for breakpoint parity
114
116
  - WebFetch / WebSearch for URL discovery
115
117
  - Write for the final `design-references.md`
116
- - Read for `visual-dna.md` and `findings-digest.md`
118
+ - Read for `DESIGN.md` and `docs/plans/phase1-scratch/findings-digest.md`
@@ -26,13 +26,15 @@ const rejectedAlternativeSchema = z.object({
26
26
 
27
27
  const scribeInputShape = {
28
28
  phase: z.string().min(1),
29
- category: z.string().min(1),
30
29
  summary: z.string().min(1),
31
30
  decided_by: z.string().min(1),
32
31
  impact_level: z.enum(["low", "medium", "high", "critical"]),
33
32
  chosen_approach: z.string().min(1),
34
33
  rejected_alternatives: z.array(rejectedAlternativeSchema).max(3).optional(),
35
- ref: z.string().optional(),
34
+ ref: z
35
+ .string()
36
+ .min(1)
37
+ .regex(/^[a-zA-Z0-9_\-./]+\.(md|json|jsonl|yaml|yml)(#[a-zA-Z0-9_\-/.]+)?$/),
36
38
  };
37
39
 
38
40
  export function buildScribeTool(tool: ToolConstructor, cwd: string) {
@@ -37,7 +37,7 @@ export function buildAcquireWriteLeaseTool(tool: ToolConstructor) {
37
37
  acquireInputShape,
38
38
  async (args) => {
39
39
  try {
40
- const result = acquireWriteLease(args.task_id, args.file_paths);
40
+ const result = await acquireWriteLease(args.task_id, args.file_paths);
41
41
  return {
42
42
  content: [
43
43
  {
@@ -2,6 +2,17 @@
2
2
  /*
3
3
  * buildanything runtime — entrypoint loaded by Claude Code plugin
4
4
  * when SDK mode is enabled.
5
+ *
6
+ * Responsibilities:
7
+ * - schema_version forward-compat check on docs/plans/.build-state.json
8
+ * - --resume stale-edge recovery (Task 4.3.4)
9
+ * - SDK/host compat probe (sdkActive = state file + CLAUDE_CODE_VERSION ok)
10
+ * - logging
11
+ *
12
+ * Orchestrator MCP tools (state_save, write_lease, cycle_counter, scribe) are
13
+ * served over stdio by bin/mcp-servers/orchestrator-mcp.ts — registered via
14
+ * .claude-plugin/plugin.json's mcpServers block and auto-started by Claude
15
+ * Code. This runtime no longer registers them in-process.
5
16
  */
6
17
 
7
18
  import process from "node:process";
@@ -203,113 +214,15 @@ async function main(): Promise<void> {
203
214
  const sdkActive = stateFileActive && hostCompat;
204
215
  console.log(`[buildanything-runtime] sdkActive=${sdkActive} (stateFile=${stateFileActive}, hostCompat=${hostCompat})`);
205
216
 
206
- // [Task 1.2.4] scribe MCP registration
207
- // [Task 2.3.4] write-lease MCP registration
208
- // [Task 3.2.4] state-save MCP registration
209
- // [Task 4.2.4] cycle-counter MCP registration
210
- const mcpServers: Record<string, unknown> = {};
211
- if (!sdkActive) {
212
- console.log("[buildanything-runtime] sdk inactive scribe + write-lease + state-save + cycle-counter MCP registration skipped (markdown mode)");
213
- } else {
214
- try {
215
- const sdk = await import("@anthropic-ai/claude-agent-sdk");
216
- const { buildScribeTool } = await import("./adapters/scribe-tool.js");
217
- const scribeTool = buildScribeTool(sdk.tool, process.cwd());
218
- mcpServers.scribe = sdk.createSdkMcpServer({
219
- name: "scribe",
220
- tools: [scribeTool],
221
- });
222
- console.log("[buildanything-runtime] scribe MCP server registered (tool: scribe_decision)");
223
- } catch (err) {
224
- console.warn(
225
- `[buildanything-runtime] warning: scribe MCP registration failed (${(err as Error).message}); continuing in markdown mode`,
226
- );
227
- }
228
-
229
- // Hydrate in-memory lease store from disk so it matches persisted state
230
- // across runtime restarts. init() no-ops on missing file and swallows
231
- // parse errors internally; we still guard so a surprise throw doesn't
232
- // block MCP registration.
233
- try {
234
- const writeLeaseModule = await import("../src/orchestrator/mcp/write-lease.js");
235
- writeLeaseModule.init(buildStatePath);
236
- } catch (err) {
237
- console.warn(
238
- `[buildanything-runtime] warning: write-lease init failed (${(err as Error).message}); continuing with empty in-memory leases`,
239
- );
240
- }
241
-
242
- try {
243
- const sdk = await import("@anthropic-ai/claude-agent-sdk");
244
- const {
245
- buildAcquireWriteLeaseTool,
246
- buildReleaseWriteLeaseTool,
247
- buildListWriteLeasesTool,
248
- } = await import("./adapters/write-lease-tool.js");
249
- const acquireTool = buildAcquireWriteLeaseTool(sdk.tool);
250
- const releaseTool = buildReleaseWriteLeaseTool(sdk.tool);
251
- const listTool = buildListWriteLeasesTool(sdk.tool);
252
- mcpServers.write_lease = sdk.createSdkMcpServer({
253
- name: "write_lease",
254
- tools: [acquireTool, releaseTool, listTool],
255
- });
256
- console.log(
257
- "[buildanything-runtime] write-lease MCP server registered (tools: acquire_write_lease, release_write_lease, list_write_leases)",
258
- );
259
- } catch (err) {
260
- console.warn(
261
- `[buildanything-runtime] warning: write-lease MCP registration failed (${(err as Error).message}); continuing without lease enforcement`,
262
- );
263
- }
264
-
265
- try {
266
- const sdk = await import("@anthropic-ai/claude-agent-sdk");
267
- const {
268
- buildStateSaveTool,
269
- buildStateReadTool,
270
- buildVerifyIntegrityTool,
271
- } = await import("./adapters/state-save-tool.js");
272
- const saveTool = buildStateSaveTool(sdk.tool);
273
- const readTool = buildStateReadTool(sdk.tool);
274
- const verifyTool = buildVerifyIntegrityTool(sdk.tool);
275
- mcpServers.state_save = sdk.createSdkMcpServer({
276
- name: "state_save",
277
- tools: [saveTool, readTool, verifyTool],
278
- });
279
- console.log(
280
- "[buildanything-runtime] state-save MCP server registered (tools: state_save, state_read, verify_integrity)",
281
- );
282
- } catch (err) {
283
- console.warn(
284
- `[buildanything-runtime] warning: state-save MCP registration failed (${(err as Error).message}); continuing in markdown mode`,
285
- );
286
- }
287
-
288
- try {
289
- const sdk = await import("@anthropic-ai/claude-agent-sdk");
290
- const {
291
- buildCycleCounterCheckTool,
292
- buildClearInFlightEdgeTool,
293
- buildHandleStaleEdgeTool,
294
- } = await import("./adapters/cycle-counter-tool.js");
295
- const checkTool = buildCycleCounterCheckTool(sdk.tool);
296
- const clearTool = buildClearInFlightEdgeTool(sdk.tool);
297
- const staleTool = buildHandleStaleEdgeTool(sdk.tool);
298
- mcpServers.cycle_counter = sdk.createSdkMcpServer({
299
- name: "cycle_counter",
300
- tools: [checkTool, clearTool, staleTool],
301
- });
302
- console.log(
303
- "[buildanything-runtime] cycle-counter MCP server registered (tools: cycle_counter_check, clear_in_flight_edge, handle_stale_edge)",
304
- );
305
- } catch (err) {
306
- console.warn(
307
- `[buildanything-runtime] warning: cycle-counter MCP registration failed (${(err as Error).message}); continuing without cycle enforcement`,
308
- );
309
- }
310
- }
311
-
312
- void mcpServers;
217
+ // Orchestrator MCP tools (scribe_decision [Task 1.2.4], acquire/release/list
218
+ // write-lease [Task 2.3.4], state_save/state_read/verify_integrity [Task
219
+ // 3.2.4], cycle_counter_check/clear_in_flight_edge/handle_stale_edge [Task
220
+ // 4.2.4]) are served over stdio by bin/mcp-servers/orchestrator-mcp.ts,
221
+ // registered through .claude-plugin/plugin.json's mcpServers block. The
222
+ // Agent SDK path no longer owns these tools — stdio is the single source.
223
+ // The adapters in bin/adapters/*.ts remain for any future in-process SDK
224
+ // path that wants to share the zod shapes.
225
+ void sdkActive;
313
226
  }
314
227
 
315
228
  function isCliEntry(): boolean {
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Thin shim: delegates to the graph indexer TypeScript entry via tsx.
5
+ const { spawn } = require('child_process');
6
+ const path = require('path');
7
+
8
+ const tsEntry = path.join(__dirname, 'graph-index.ts');
9
+ const child = spawn('npx', ['--no-install', 'tsx', tsEntry, ...process.argv.slice(2)], {
10
+ stdio: 'inherit',
11
+ });
12
+
13
+ child.on('exit', (code, signal) => {
14
+ if (signal) {
15
+ process.kill(process.pid, signal);
16
+ return;
17
+ }
18
+ process.exit(code ?? 0);
19
+ });
20
+
21
+ child.on('error', (err) => {
22
+ process.stderr.write(`[graph-index shim] failed to spawn tsx: ${err.message}\n`);
23
+ process.exit(1);
24
+ });
@@ -0,0 +1,340 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ import { readFileSync, statSync, readdirSync, existsSync, unlinkSync } from "node:fs";
4
+ import { basename, resolve, join, relative, extname } from "node:path";
5
+ import { createHash } from "node:crypto";
6
+ import process from "node:process";
7
+ import { extractProductSpec, extractDesignMd, extractDesignMdTokens, extractComponentManifest, extractPageSpec, extractArchitecture, extractSprintTasks, extractDecisionsJsonl, extractScreenshot, saveGraph } from "../src/graph/index.js";
8
+ import type { GraphFragment } from "../src/graph/types.js";
9
+
10
+ type ImageClass = "reference" | "brand_drift" | "dogfood";
11
+
12
+ const VALID_IMAGE_CLASSES: ReadonlySet<string> = new Set(["reference", "brand_drift", "dogfood"]);
13
+ const IMAGE_EXTENSIONS: ReadonlySet<string> = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif"]);
14
+
15
+ function parseArgs(argv: string[]): { positional: string[]; imageClass: string | undefined } {
16
+ const positional: string[] = [];
17
+ let imageClass: string | undefined;
18
+ for (const arg of argv) {
19
+ if (arg.startsWith("--image-class=")) {
20
+ imageClass = arg.slice("--image-class=".length);
21
+ } else if (arg === "--image-class") {
22
+ // peek-ahead style not supported; require =VALUE form
23
+ process.stderr.write("[graph-index] error: --image-class requires =VALUE form (e.g. --image-class=reference)\n");
24
+ process.exit(64);
25
+ } else {
26
+ positional.push(arg);
27
+ }
28
+ }
29
+ return { positional, imageClass };
30
+ }
31
+
32
+ function inferImageClassFromPath(absPath: string): ImageClass | null {
33
+ // Normalize trailing slash + lower-case scan against known suffixes.
34
+ const normalized = absPath.replace(/\/+$/, "");
35
+ if (normalized.endsWith("/design-references") || normalized.includes("/design-references/")) {
36
+ return "reference";
37
+ }
38
+ if (normalized.endsWith("/evidence/brand-drift") || normalized.includes("/evidence/brand-drift/")) {
39
+ return "brand_drift";
40
+ }
41
+ if (normalized.endsWith("/evidence/dogfood") || normalized.includes("/evidence/dogfood/")) {
42
+ return "dogfood";
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function targetFileForClass(c: ImageClass): string {
48
+ switch (c) {
49
+ case "reference": return "slice-5-references.json";
50
+ case "brand_drift": return "slice-5-brand-drift.json";
51
+ case "dogfood": return "slice-5-dogfood.json";
52
+ }
53
+ }
54
+
55
+ function collectImageFiles(dir: string): string[] {
56
+ const files: string[] = [];
57
+ // Recurse one level — competitors/, inspiration/, screenshots/ subdirs are common.
58
+ const entries = readdirSync(dir, { withFileTypes: true });
59
+ for (const entry of entries) {
60
+ const full = join(dir, entry.name);
61
+ if (entry.isDirectory()) {
62
+ try {
63
+ for (const sub of readdirSync(full, { withFileTypes: true })) {
64
+ if (sub.isFile() && IMAGE_EXTENSIONS.has(extname(sub.name).toLowerCase())) {
65
+ files.push(join(full, sub.name));
66
+ }
67
+ }
68
+ } catch { /* unreadable subdir */ }
69
+ } else if (entry.isFile() && IMAGE_EXTENSIONS.has(extname(entry.name).toLowerCase())) {
70
+ files.push(full);
71
+ }
72
+ }
73
+ return files.sort();
74
+ }
75
+
76
+ function indexImageDirectory(absPath: string, imageClass: ImageClass): void {
77
+ // TODO Slice 5 production: when imageClass === 'dogfood', read evidence/dogfood/findings.json side-channel to populate linked_finding_id per screenshot.
78
+ const imageFiles = collectImageFiles(absPath);
79
+
80
+ if (imageFiles.length === 0) {
81
+ // Per Slice 5 schema §11 scenario 1: empty directory writes empty fragment, does NOT fail.
82
+ process.stdout.write(`[graph-index] info — no images in ${absPath}, writing empty fragment\n`);
83
+ }
84
+
85
+ const allNodes: GraphFragment["nodes"] = [];
86
+ const allEdges: GraphFragment["edges"] = [];
87
+ const hashChunks: Buffer[] = [];
88
+ const warnings: string[] = [];
89
+ let successCount = 0;
90
+
91
+ for (const filePath of imageFiles) {
92
+ let bytes: Buffer;
93
+ try {
94
+ bytes = readFileSync(filePath);
95
+ } catch (err) {
96
+ const msg = `failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`;
97
+ process.stderr.write(`[graph-index] warning: ${msg}\n`);
98
+ warnings.push(msg);
99
+ continue;
100
+ }
101
+ hashChunks.push(bytes);
102
+
103
+ try {
104
+ const result = extractScreenshot({
105
+ imagePath: relative(process.cwd(), filePath),
106
+ imageClass,
107
+ imageBytes: new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength),
108
+ });
109
+
110
+ if (!result.ok) {
111
+ const msg = `${filePath}: ${result.errors.map((e) => e.message).join("; ")}`;
112
+ warnings.push(msg);
113
+ continue;
114
+ }
115
+ allNodes.push(...result.nodes);
116
+ allEdges.push(...result.edges);
117
+ successCount++;
118
+ } catch (err) {
119
+ const msg = `${filePath}: ${err instanceof Error ? err.message : String(err)}`;
120
+ warnings.push(msg);
121
+ continue;
122
+ }
123
+ }
124
+
125
+ if (imageFiles.length > 0 && successCount === 0) {
126
+ for (const w of warnings) {
127
+ process.stderr.write(`[graph-index] warning: ${w}\n`);
128
+ }
129
+ process.exit(1);
130
+ }
131
+
132
+ const combined = Buffer.concat(hashChunks);
133
+ const combinedHash = combined.length > 0
134
+ ? createHash("sha256").update(combined).digest("hex")
135
+ : "0".repeat(64);
136
+
137
+ const relDir = relative(process.cwd(), absPath) + "/";
138
+
139
+ const fragment: GraphFragment = {
140
+ source_file: relDir,
141
+ source_sha: combinedHash,
142
+ produced_at: new Date().toISOString(),
143
+ version: 1,
144
+ schema: "buildanything-slice-5",
145
+ nodes: allNodes,
146
+ edges: allEdges,
147
+ };
148
+
149
+ const targetFile = targetFileForClass(imageClass);
150
+ saveGraph(process.cwd(), fragment, targetFile);
151
+ process.stdout.write(
152
+ `[graph-index] ok — ${fragment.nodes.length} nodes, ${fragment.edges.length} edges → .buildanything/graph/${targetFile}\n`,
153
+ );
154
+
155
+ if (imageFiles.length > 0) {
156
+ for (const w of warnings) {
157
+ process.stdout.write(`[graph-index] warning: ${w}\n`);
158
+ }
159
+ process.stdout.write(`[graph-index] indexed ${successCount}/${imageFiles.length} images; ${warnings.length} warnings\n`);
160
+ }
161
+ }
162
+
163
+ const { positional, imageClass: explicitImageClass } = parseArgs(process.argv.slice(2));
164
+ const target = positional[0];
165
+
166
+ if (!target) {
167
+ process.stderr.write(
168
+ "Usage: graph-index <path> [--image-class=reference|brand_drift|dogfood]\n" +
169
+ " Recognized basenames: product-spec.md, DESIGN.md, component-manifest.md, architecture.md, sprint-tasks.md, decisions.jsonl\n" +
170
+ " Directory mode: page-specs/ → indexes all *.md files inside\n" +
171
+ " Image directory mode: design-references/ | evidence/brand-drift/ | evidence/dogfood/ → indexes all images\n" +
172
+ " DESIGN.md produces both slice-2-dna.json (Pass 1) and slice-3-tokens.json (Pass 2, if tokens found)\n",
173
+ );
174
+ process.exit(64);
175
+ }
176
+
177
+ if (explicitImageClass !== undefined && !VALID_IMAGE_CLASSES.has(explicitImageClass)) {
178
+ process.stderr.write(`[graph-index] error: --image-class must be one of: reference, brand_drift, dogfood (got: ${explicitImageClass})\n`);
179
+ process.exit(64);
180
+ }
181
+
182
+ try {
183
+ const absPath = resolve(target);
184
+
185
+ // ── Directory mode ──────────────────────────────────────────────────
186
+ let isDir = false;
187
+ try { isDir = statSync(absPath).isDirectory(); } catch { /* not a dir */ }
188
+
189
+ if (isDir) {
190
+ // page-specs/ — Slice 3 markdown directory mode
191
+ if (basename(absPath) === "page-specs") {
192
+ const mdFiles = readdirSync(absPath)
193
+ .filter((f) => f.endsWith(".md"))
194
+ .sort();
195
+
196
+ if (mdFiles.length === 0) {
197
+ process.stderr.write(`[graph-index] error: no .md files found in directory: ${absPath}\n`);
198
+ process.exit(1);
199
+ }
200
+
201
+ const allNodes: GraphFragment["nodes"] = [];
202
+ const allEdges: GraphFragment["edges"] = [];
203
+ const contents: string[] = [];
204
+
205
+ for (const file of mdFiles) {
206
+ const filePath = join(absPath, file);
207
+ const content = readFileSync(filePath, "utf-8");
208
+ contents.push(content);
209
+ const result = extractPageSpec({ mdPath: filePath, mdContent: content });
210
+ if (!result.ok) {
211
+ for (const err of result.errors) {
212
+ process.stderr.write(`[graph-index] ${file} L${err.line}: ${err.message}\n`);
213
+ }
214
+ process.stderr.write(`[graph-index] fatal: page-spec parse failed for ${file}\n`);
215
+ process.exit(1);
216
+ }
217
+ allNodes.push(...result.fragment!.nodes);
218
+ allEdges.push(...result.fragment!.edges);
219
+ }
220
+
221
+ const combinedHash = createHash("sha256").update(contents.join("")).digest("hex");
222
+ const relDir = relative(process.cwd(), absPath) + "/";
223
+
224
+ const fragment: GraphFragment = {
225
+ source_file: relDir,
226
+ source_sha: combinedHash,
227
+ produced_at: new Date().toISOString(),
228
+ version: 1,
229
+ schema: "buildanything-slice-3",
230
+ nodes: allNodes,
231
+ edges: allEdges,
232
+ };
233
+
234
+ saveGraph(process.cwd(), fragment, "slice-3-pages.json");
235
+ process.stdout.write(
236
+ `[graph-index] ok — ${fragment.nodes.length} nodes, ${fragment.edges.length} edges → .buildanything/graph/slice-3-pages.json\n`,
237
+ );
238
+ process.exit(0);
239
+ }
240
+
241
+ // Slice 5 image-directory mode — explicit override OR path inference
242
+ const inferred = inferImageClassFromPath(absPath);
243
+ const resolvedClass = (explicitImageClass ?? inferred) as ImageClass | null;
244
+
245
+ if (resolvedClass !== null) {
246
+ indexImageDirectory(absPath, resolvedClass);
247
+ process.exit(0);
248
+ }
249
+
250
+ process.stderr.write(
251
+ `[graph-index] error: directory ${absPath} is not a recognized indexer target.\n` +
252
+ ` Markdown directory: page-specs/\n` +
253
+ ` Image directories (auto-detected): design-references/, evidence/brand-drift/, evidence/dogfood/\n` +
254
+ ` Or pass --image-class=reference|brand_drift|dogfood to force.\n`,
255
+ );
256
+ process.exit(64);
257
+ }
258
+
259
+ // ── File mode ───────────────────────────────────────────────────────
260
+ if (!(() => { try { readFileSync(absPath); return true; } catch { return false; } })()) {
261
+ process.stderr.write(
262
+ "Usage: graph-index <path>\n" +
263
+ " Recognized basenames: product-spec.md, DESIGN.md, component-manifest.md, architecture.md, sprint-tasks.md, decisions.jsonl\n" +
264
+ " Directory mode: pass a page-specs/ directory to index all *.md files inside\n",
265
+ );
266
+ process.exit(64);
267
+ }
268
+
269
+ const mdContent = readFileSync(absPath, "utf-8");
270
+ const base = basename(absPath);
271
+
272
+ let result;
273
+ let targetFile: string;
274
+ if (base === "product-spec.md") {
275
+ result = extractProductSpec({ mdPath: absPath, mdContent });
276
+ targetFile = "slice-1.json";
277
+ } else if (base === "DESIGN.md") {
278
+ result = extractDesignMd({ mdPath: absPath, mdContent });
279
+ targetFile = "slice-2-dna.json";
280
+ } else if (base === "component-manifest.md") {
281
+ result = extractComponentManifest({ mdPath: absPath, mdContent });
282
+ targetFile = "slice-2-manifest.json";
283
+ } else if (base === "architecture.md") {
284
+ result = extractArchitecture({ mdPath: absPath, mdContent });
285
+ targetFile = "slice-4-architecture.json";
286
+ } else if (base === "sprint-tasks.md") {
287
+ result = extractSprintTasks({ mdPath: absPath, mdContent });
288
+ targetFile = "slice-4-tasks.json";
289
+ } else if (base === "decisions.jsonl") {
290
+ result = extractDecisionsJsonl({ mdPath: absPath, mdContent });
291
+ targetFile = "slice-4-decisions.json";
292
+ } else {
293
+ process.stderr.write(
294
+ `Usage: graph-index <path>\n Recognized basenames: product-spec.md, DESIGN.md, component-manifest.md, architecture.md, sprint-tasks.md, decisions.jsonl\n Directory mode: pass a page-specs/ directory to index all *.md files inside\n Got: ${base}\n`,
295
+ );
296
+ process.exit(64);
297
+ }
298
+
299
+ if (result.ok) {
300
+ const fragment = result.fragment!;
301
+ saveGraph(process.cwd(), fragment, targetFile);
302
+ process.stdout.write(
303
+ `[graph-index] ok — ${fragment.nodes.length} nodes, ${fragment.edges.length} edges → .buildanything/graph/${targetFile}\n`,
304
+ );
305
+ } else {
306
+ for (const err of result.errors) {
307
+ process.stderr.write(`[graph-index] L${err.line}: ${err.message}\n`);
308
+ }
309
+ process.exit(1);
310
+ }
311
+
312
+ // ── DESIGN.md Pass 2: tokens ────────────────────────────────────────
313
+ if (base === "DESIGN.md") {
314
+ const pass2 = extractDesignMdTokens({ mdPath: absPath, mdContent });
315
+ if (pass2.ok && pass2.fragment!.nodes.length > 0) {
316
+ saveGraph(process.cwd(), pass2.fragment!, "slice-3-tokens.json");
317
+ process.stdout.write(
318
+ `[graph-index] ok — ${pass2.fragment!.nodes.length} nodes, ${pass2.fragment!.edges.length} edges → .buildanything/graph/slice-3-tokens.json\n`,
319
+ );
320
+ } else if (!pass2.ok) {
321
+ for (const err of pass2.errors) {
322
+ process.stderr.write(`[graph-index] warning (Pass 2): L${err.line}: ${err.message}\n`);
323
+ }
324
+ }
325
+
326
+ if ((pass2.ok && pass2.fragment!.nodes.length === 0) || !pass2.ok) {
327
+ const stalePath = join(process.cwd(), ".buildanything", "graph", "slice-3-tokens.json");
328
+ try {
329
+ if (existsSync(stalePath)) {
330
+ unlinkSync(stalePath);
331
+ process.stdout.write("[graph-index] Pass 2 empty — removed stale slice-3-tokens.json\n");
332
+ }
333
+ } catch { /* best-effort */ }
334
+ }
335
+ }
336
+ } catch (e: unknown) {
337
+ const msg = e instanceof Error ? e.message : String(e);
338
+ process.stderr.write(`[graph-index] fatal: ${msg}\n`);
339
+ process.exit(2);
340
+ }