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.
- 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 +371 -158
- 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 +112 -27
- 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 +234 -64
- 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
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tech-feasibility
|
|
3
|
-
description: Evaluates technical architecture, hard problems, build-vs-buy decisions,
|
|
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.
|
|
48
|
-
- The
|
|
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
|
|
@@ -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:
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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
|
|
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]
|
|
207
|
-
// [Task 2.3.4]
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
+
}
|