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,432 @@
|
|
|
1
|
+
// Slice 1 graph types. Source of truth: docs/graph/04-slice1-schema.md.
|
|
2
|
+
// Confidence semantics inherit Graphify's vocabulary even though Slice 1 is
|
|
3
|
+
// pure-deterministic (every edge is EXTRACTED). The field is preserved so
|
|
4
|
+
// later slices that introduce LLM extraction can extend without migration.
|
|
5
|
+
|
|
6
|
+
export type Confidence = "EXTRACTED" | "INFERRED" | "AMBIGUOUS";
|
|
7
|
+
|
|
8
|
+
export type EntityType =
|
|
9
|
+
| "persona"
|
|
10
|
+
| "feature"
|
|
11
|
+
| "screen"
|
|
12
|
+
| "state"
|
|
13
|
+
| "transition"
|
|
14
|
+
| "business_rule"
|
|
15
|
+
| "failure_mode"
|
|
16
|
+
| "acceptance_criterion"
|
|
17
|
+
| "persona_constraint"
|
|
18
|
+
// Slice 2 additions. Source of truth: docs/graph/05-slice2-schema.md.
|
|
19
|
+
| "dna_axis"
|
|
20
|
+
| "brand_dna_guideline"
|
|
21
|
+
| "brand_reference"
|
|
22
|
+
| "component_manifest_entry"
|
|
23
|
+
| "component_slot"
|
|
24
|
+
| "design_doc_root"
|
|
25
|
+
// Slice 3 additions. Source of truth: docs/graph/07-slice3-schema.md.
|
|
26
|
+
| "token"
|
|
27
|
+
| "page_spec"
|
|
28
|
+
| "wireframe_section"
|
|
29
|
+
| "screen_state_slot"
|
|
30
|
+
| "screen_component_use"
|
|
31
|
+
| "key_copy"
|
|
32
|
+
// Slice 4 additions. Source of truth: docs/graph/09-slice4-schema.md.
|
|
33
|
+
| "architecture_module"
|
|
34
|
+
| "api_contract"
|
|
35
|
+
| "data_model"
|
|
36
|
+
| "task"
|
|
37
|
+
| "decision"
|
|
38
|
+
// Slice 5 additions. Source of truth: docs/graph/11-slice5-schema.md.
|
|
39
|
+
| "screenshot"
|
|
40
|
+
| "image_component_detection"
|
|
41
|
+
| "dogfood_finding"
|
|
42
|
+
| "brand_drift_observation";
|
|
43
|
+
|
|
44
|
+
export type Relation =
|
|
45
|
+
| "has_screen"
|
|
46
|
+
| "has_state"
|
|
47
|
+
| "has_initial_state"
|
|
48
|
+
| "transitions_to"
|
|
49
|
+
| "triggered_by_transition"
|
|
50
|
+
| "has_rule"
|
|
51
|
+
| "has_failure_mode"
|
|
52
|
+
| "has_acceptance"
|
|
53
|
+
| "constrains"
|
|
54
|
+
| "applies_to_persona"
|
|
55
|
+
| "depends_on"
|
|
56
|
+
// Slice 2 additions. Source of truth: docs/graph/05-slice2-schema.md.
|
|
57
|
+
| "has_axis"
|
|
58
|
+
| "dna_governs"
|
|
59
|
+
| "forbids"
|
|
60
|
+
| "applies_to"
|
|
61
|
+
| "slot_filled_by"
|
|
62
|
+
| "manifest_uses_library"
|
|
63
|
+
| "references_axis"
|
|
64
|
+
// Slice 3 additions. Source of truth: docs/graph/07-slice3-schema.md.
|
|
65
|
+
| "has_page_spec"
|
|
66
|
+
| "has_section"
|
|
67
|
+
| "has_screen_state"
|
|
68
|
+
| "slot_used_on_screen"
|
|
69
|
+
| "screen_uses_token"
|
|
70
|
+
| "token_derived_from"
|
|
71
|
+
| "key_copy_on_screen"
|
|
72
|
+
// Slice 4 additions. Source of truth: docs/graph/09-slice4-schema.md.
|
|
73
|
+
| "module_has_contract"
|
|
74
|
+
| "module_has_data_model"
|
|
75
|
+
| "task_implements_feature"
|
|
76
|
+
| "task_touches_screen"
|
|
77
|
+
| "task_depends_on"
|
|
78
|
+
| "feature_provides_endpoint"
|
|
79
|
+
| "feature_consumes_endpoint"
|
|
80
|
+
| "decision_supersedes"
|
|
81
|
+
| "decision_relates_to"
|
|
82
|
+
| "decision_drove"
|
|
83
|
+
// Slice 5 additions. Source of truth: docs/graph/11-slice5-schema.md.
|
|
84
|
+
| "references_axis_image"
|
|
85
|
+
| "screenshot_depicts_screen"
|
|
86
|
+
| "screenshot_evidences_finding"
|
|
87
|
+
| "image_has_component_detection"
|
|
88
|
+
| "prod_drifts_from_reference_prod"
|
|
89
|
+
| "prod_drifts_from_reference_ref"
|
|
90
|
+
| "similar_to_image";
|
|
91
|
+
|
|
92
|
+
export interface NodeBase {
|
|
93
|
+
id: string;
|
|
94
|
+
label: string;
|
|
95
|
+
entity_type: EntityType;
|
|
96
|
+
source_file: string;
|
|
97
|
+
source_location?: string; // "L42"
|
|
98
|
+
confidence: Confidence;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface PersonaNode extends NodeBase {
|
|
102
|
+
entity_type: "persona";
|
|
103
|
+
description: string;
|
|
104
|
+
role: string;
|
|
105
|
+
is_primary: boolean;
|
|
106
|
+
primary_jtbd: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface FeatureNode extends NodeBase {
|
|
110
|
+
entity_type: "feature";
|
|
111
|
+
name: string;
|
|
112
|
+
kebab_anchor: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ScreenNode extends NodeBase {
|
|
116
|
+
entity_type: "screen";
|
|
117
|
+
description: string;
|
|
118
|
+
feature_ids: string[];
|
|
119
|
+
count?: number; // populated when screen inventory says "Checkout (3 screens)" without naming all 3
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface StateNode extends NodeBase {
|
|
123
|
+
entity_type: "state";
|
|
124
|
+
feature_id: string;
|
|
125
|
+
is_initial: boolean;
|
|
126
|
+
meta_state: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface TransitionNode extends NodeBase {
|
|
130
|
+
entity_type: "transition";
|
|
131
|
+
from_state_id: string;
|
|
132
|
+
to_state_id: string;
|
|
133
|
+
trigger: string;
|
|
134
|
+
preconditions: string;
|
|
135
|
+
side_effects: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface BusinessRuleNode extends NodeBase {
|
|
139
|
+
entity_type: "business_rule";
|
|
140
|
+
feature_id: string;
|
|
141
|
+
text: string;
|
|
142
|
+
value: string | null;
|
|
143
|
+
decision_needed: boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface FailureModeNode extends NodeBase {
|
|
147
|
+
entity_type: "failure_mode";
|
|
148
|
+
feature_id: string;
|
|
149
|
+
trigger: string;
|
|
150
|
+
user_sees: string;
|
|
151
|
+
user_can: string;
|
|
152
|
+
system_does: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface AcceptanceCriterionNode extends NodeBase {
|
|
156
|
+
entity_type: "acceptance_criterion";
|
|
157
|
+
feature_id: string;
|
|
158
|
+
text: string;
|
|
159
|
+
verified: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface PersonaConstraintNode extends NodeBase {
|
|
163
|
+
entity_type: "persona_constraint";
|
|
164
|
+
feature_id: string;
|
|
165
|
+
persona_id: string;
|
|
166
|
+
constraint_text: string;
|
|
167
|
+
cited_source: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Slice 2 additions. Source of truth: docs/graph/05-slice2-schema.md.
|
|
171
|
+
|
|
172
|
+
export interface DesignDocRootNode extends NodeBase {
|
|
173
|
+
entity_type: "design_doc_root";
|
|
174
|
+
name: string;
|
|
175
|
+
description: string;
|
|
176
|
+
locked_at: string; // ISO-8601 from `### Locked At`
|
|
177
|
+
lint_status?: "pass" | "warn" | "fail" | null;
|
|
178
|
+
pass_complete: { pass1: boolean; pass2: boolean };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface DnaAxisNode extends NodeBase {
|
|
182
|
+
entity_type: "dna_axis";
|
|
183
|
+
axis_name: "scope" | "density" | "character" | "material" | "motion" | "type" | "copy";
|
|
184
|
+
value: string;
|
|
185
|
+
rationale: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface BrandDnaGuidelineNode extends NodeBase {
|
|
189
|
+
entity_type: "brand_dna_guideline";
|
|
190
|
+
polarity: "do" | "dont";
|
|
191
|
+
text: string;
|
|
192
|
+
axis_scope: string | null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface BrandReferenceNode extends NodeBase {
|
|
196
|
+
entity_type: "brand_reference";
|
|
197
|
+
url_or_path: string;
|
|
198
|
+
exemplifies_axes: string[];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface ComponentManifestEntryNode extends NodeBase {
|
|
202
|
+
entity_type: "component_manifest_entry";
|
|
203
|
+
slot: string;
|
|
204
|
+
library: string;
|
|
205
|
+
variant: string;
|
|
206
|
+
source_ref: string | null;
|
|
207
|
+
hard_gate: boolean;
|
|
208
|
+
fallback_plan?: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface ComponentSlotNode extends NodeBase {
|
|
212
|
+
entity_type: "component_slot";
|
|
213
|
+
slot_name: string;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Slice 3 additions. Source of truth: docs/graph/07-slice3-schema.md.
|
|
217
|
+
|
|
218
|
+
export interface TokenNode extends NodeBase {
|
|
219
|
+
entity_type: "token";
|
|
220
|
+
name: string;
|
|
221
|
+
value: string;
|
|
222
|
+
layer: "color" | "typography" | "spacing" | "shape" | "elevation" | "motion" | "type" | "component";
|
|
223
|
+
axis_provenance: "scope" | "density" | "character" | "material" | "motion" | "type" | "copy" | null;
|
|
224
|
+
category: string | null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface PageSpecNode extends NodeBase {
|
|
228
|
+
entity_type: "page_spec";
|
|
229
|
+
screen_id: string;
|
|
230
|
+
wireframe_text: string;
|
|
231
|
+
content_hierarchy: string[];
|
|
232
|
+
route: string | null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface WireframeSectionNode extends NodeBase {
|
|
236
|
+
entity_type: "wireframe_section";
|
|
237
|
+
section_name: string;
|
|
238
|
+
parent_page_spec_id: string;
|
|
239
|
+
order: number;
|
|
240
|
+
prose: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface ScreenStateSlotNode extends NodeBase {
|
|
244
|
+
entity_type: "screen_state_slot";
|
|
245
|
+
screen_id: string;
|
|
246
|
+
state_id: string;
|
|
247
|
+
appearance_text: string;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface ScreenComponentUseNode extends NodeBase {
|
|
251
|
+
entity_type: "screen_component_use";
|
|
252
|
+
screen_id: string;
|
|
253
|
+
slot: string;
|
|
254
|
+
position_in_wireframe: string;
|
|
255
|
+
prop_overrides: string;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface KeyCopyNode extends NodeBase {
|
|
259
|
+
entity_type: "key_copy";
|
|
260
|
+
screen_id: string;
|
|
261
|
+
text: string;
|
|
262
|
+
placement: string;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Slice 4 additions. Source of truth: docs/graph/09-slice4-schema.md.
|
|
266
|
+
|
|
267
|
+
export interface ArchitectureModuleNode extends NodeBase {
|
|
268
|
+
entity_type: "architecture_module";
|
|
269
|
+
name: string;
|
|
270
|
+
description: string;
|
|
271
|
+
responsibilities: string[];
|
|
272
|
+
tech_stack: string[];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface ApiContractNode extends NodeBase {
|
|
276
|
+
entity_type: "api_contract";
|
|
277
|
+
endpoint: string; // e.g. "POST /api/orders"
|
|
278
|
+
module_id: string; // FK
|
|
279
|
+
request_schema: string; // JSON-string blob
|
|
280
|
+
response_schema: string; // JSON-string blob
|
|
281
|
+
auth_required: boolean;
|
|
282
|
+
error_codes: string[];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface DataModelNode extends NodeBase {
|
|
286
|
+
entity_type: "data_model";
|
|
287
|
+
entity_name: string;
|
|
288
|
+
module_id: string;
|
|
289
|
+
fields: string[]; // "name:type" pairs
|
|
290
|
+
indexes: string[];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export interface TaskNode extends NodeBase {
|
|
294
|
+
entity_type: "task";
|
|
295
|
+
task_id: string; // e.g. "T-1"
|
|
296
|
+
title: string;
|
|
297
|
+
size: "S" | "M" | "L";
|
|
298
|
+
behavioral_test: string;
|
|
299
|
+
assigned_phase: string;
|
|
300
|
+
feature_id: string | null;
|
|
301
|
+
screen_ids: string[];
|
|
302
|
+
owns_files: string[];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export interface DecisionNode extends NodeBase {
|
|
306
|
+
entity_type: "decision";
|
|
307
|
+
decision_id: string;
|
|
308
|
+
summary: string;
|
|
309
|
+
decided_by: string; // verbatim from JSONL — could be "code-architect", "human", etc.
|
|
310
|
+
related_decision_id: string | null;
|
|
311
|
+
revisit_criterion: string | null;
|
|
312
|
+
status: "open" | "triggered" | "resolved";
|
|
313
|
+
phase: string;
|
|
314
|
+
step_id: string | null;
|
|
315
|
+
ref: string | null; // e.g. "architecture.md#backend/persistence" — anchor into source doc
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Slice 5 additions. Source of truth: docs/graph/11-slice5-schema.md.
|
|
319
|
+
|
|
320
|
+
export interface ScreenshotNode extends NodeBase {
|
|
321
|
+
entity_type: "screenshot";
|
|
322
|
+
image_path: string;
|
|
323
|
+
image_class: "reference" | "brand_drift" | "dogfood";
|
|
324
|
+
caption: string;
|
|
325
|
+
perceptual_hash: string; // 64-bit dHash as 16-char hex
|
|
326
|
+
dominant_palette: string[]; // up to 5 hex colors
|
|
327
|
+
image_dimensions: string; // e.g. "1280x720"
|
|
328
|
+
dna_axis_tags: string[]; // axes this image exemplifies
|
|
329
|
+
linked_screen_id: string | null;
|
|
330
|
+
linked_finding_id: string | null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export interface ImageComponentDetectionNode extends NodeBase {
|
|
334
|
+
entity_type: "image_component_detection";
|
|
335
|
+
screenshot_id: string;
|
|
336
|
+
component_label: string;
|
|
337
|
+
bounding_box: string | null;
|
|
338
|
+
// Vision-model detection probability (0-1). Renamed from `confidence` to avoid collision
|
|
339
|
+
// with NodeBase.confidence (EXTRACTED/INFERRED/AMBIGUOUS extraction-confidence vocabulary).
|
|
340
|
+
detection_confidence: number | null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export interface DogfoodFindingNode extends NodeBase {
|
|
344
|
+
entity_type: "dogfood_finding";
|
|
345
|
+
finding_id: string;
|
|
346
|
+
severity: "critical" | "major" | "minor";
|
|
347
|
+
description: string;
|
|
348
|
+
screenshot_id: string;
|
|
349
|
+
affected_screen_id: string | null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export interface BrandDriftObservationNode extends NodeBase {
|
|
353
|
+
entity_type: "brand_drift_observation";
|
|
354
|
+
observation_id: string;
|
|
355
|
+
prod_screenshot_id: string;
|
|
356
|
+
reference_screenshot_id: string;
|
|
357
|
+
axis: "scope" | "density" | "character" | "material" | "motion" | "type" | "copy";
|
|
358
|
+
score: number;
|
|
359
|
+
verdict: "drift" | "ok" | "needs-review";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export type GraphNode =
|
|
363
|
+
| PersonaNode
|
|
364
|
+
| FeatureNode
|
|
365
|
+
| ScreenNode
|
|
366
|
+
| StateNode
|
|
367
|
+
| TransitionNode
|
|
368
|
+
| BusinessRuleNode
|
|
369
|
+
| FailureModeNode
|
|
370
|
+
| AcceptanceCriterionNode
|
|
371
|
+
| PersonaConstraintNode
|
|
372
|
+
| DesignDocRootNode
|
|
373
|
+
| DnaAxisNode
|
|
374
|
+
| BrandDnaGuidelineNode
|
|
375
|
+
| BrandReferenceNode
|
|
376
|
+
| ComponentManifestEntryNode
|
|
377
|
+
| ComponentSlotNode
|
|
378
|
+
| TokenNode
|
|
379
|
+
| PageSpecNode
|
|
380
|
+
| WireframeSectionNode
|
|
381
|
+
| ScreenStateSlotNode
|
|
382
|
+
| ScreenComponentUseNode
|
|
383
|
+
| KeyCopyNode
|
|
384
|
+
| ArchitectureModuleNode
|
|
385
|
+
| ApiContractNode
|
|
386
|
+
| DataModelNode
|
|
387
|
+
| TaskNode
|
|
388
|
+
| DecisionNode
|
|
389
|
+
| ScreenshotNode
|
|
390
|
+
| ImageComponentDetectionNode
|
|
391
|
+
| DogfoodFindingNode
|
|
392
|
+
| BrandDriftObservationNode;
|
|
393
|
+
|
|
394
|
+
export interface GraphEdge {
|
|
395
|
+
source: string;
|
|
396
|
+
target: string;
|
|
397
|
+
relation: Relation;
|
|
398
|
+
confidence: Confidence;
|
|
399
|
+
source_file: string;
|
|
400
|
+
source_location?: string;
|
|
401
|
+
produced_by_agent?: string; // forward-compat — Slice 1 sets "product-spec-writer"
|
|
402
|
+
produced_at_step?: string; // forward-compat — Slice 1 sets "1.6"
|
|
403
|
+
label?: string; // cross-feature rule text, e.g. "user must be authenticated"
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export type Schema =
|
|
407
|
+
| "buildanything-slice-1"
|
|
408
|
+
| "buildanything-slice-2"
|
|
409
|
+
| "buildanything-slice-3"
|
|
410
|
+
| "buildanything-slice-4"
|
|
411
|
+
| "buildanything-slice-5";
|
|
412
|
+
|
|
413
|
+
export interface GraphFragment {
|
|
414
|
+
version: 1;
|
|
415
|
+
schema: Schema;
|
|
416
|
+
source_file: string;
|
|
417
|
+
source_sha: string; // sha256 of product-spec.md content
|
|
418
|
+
produced_at: string; // ISO timestamp
|
|
419
|
+
nodes: GraphNode[];
|
|
420
|
+
edges: GraphEdge[];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export interface ExtractError {
|
|
424
|
+
line: number;
|
|
425
|
+
message: string;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export interface ExtractResult {
|
|
429
|
+
ok: boolean;
|
|
430
|
+
fragment?: GraphFragment;
|
|
431
|
+
errors: ExtractError[];
|
|
432
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Difference hash (dHash) — STUB for hypothetical environment.
|
|
3
|
+
*
|
|
4
|
+
* Real production dHash requires image decoding (e.g. Sharp / @napi-rs/image / jimp):
|
|
5
|
+
* 1. Decode image to raw pixels
|
|
6
|
+
* 2. Convert to grayscale
|
|
7
|
+
* 3. Downsample to 9×8 (72 pixels)
|
|
8
|
+
* 4. For each row, compare adjacent pixels → 64 horizontal comparisons → 64 bits
|
|
9
|
+
* 5. Pack into 16 hex chars (MSB-first)
|
|
10
|
+
*
|
|
11
|
+
* This stub is deterministic for testing but is NOT a perceptual hash on visual
|
|
12
|
+
* content — it only hashes raw byte distribution by sampling 65 evenly-spaced
|
|
13
|
+
* byte positions and comparing adjacent samples.
|
|
14
|
+
*
|
|
15
|
+
* TODO(Slice 5 production): Replace with real image-decoding dHash.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Compute a 64-bit difference hash of raw bytes (stub algorithm).
|
|
22
|
+
*
|
|
23
|
+
* @returns 16-character lowercase hex string representing 64 bits.
|
|
24
|
+
*/
|
|
25
|
+
export function dhash(bytes: Uint8Array): string {
|
|
26
|
+
if (bytes.length === 0) {
|
|
27
|
+
throw new Error("dhash: input bytes empty");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const len = bytes.length;
|
|
31
|
+
|
|
32
|
+
// Sample 65 evenly-spaced byte positions
|
|
33
|
+
const samples = new Uint8Array(65);
|
|
34
|
+
for (let i = 0; i < 65; i++) {
|
|
35
|
+
const pos = Math.min(Math.floor((i * len) / 65), len - 1);
|
|
36
|
+
samples[i] = bytes[pos];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 64 boolean comparisons → 64 bits → 16 hex chars
|
|
40
|
+
let hex = "";
|
|
41
|
+
for (let nibbleIdx = 0; nibbleIdx < 16; nibbleIdx++) {
|
|
42
|
+
let nibble = 0;
|
|
43
|
+
for (let bit = 0; bit < 4; bit++) {
|
|
44
|
+
const i = nibbleIdx * 4 + bit;
|
|
45
|
+
if (samples[i] > samples[i + 1]) {
|
|
46
|
+
nibble |= 1 << (3 - bit); // MSB-first within nibble
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
hex += nibble.toString(16);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return hex;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Popcount lookup for 4-bit nibbles (0x0–0xF)
|
|
56
|
+
const NIBBLE_POPCOUNT = [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4] as const;
|
|
57
|
+
|
|
58
|
+
const HEX16_RE = /^[0-9a-fA-F]{16}$/;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Hamming distance between two 64-bit dHash hex strings.
|
|
62
|
+
*
|
|
63
|
+
* Distance interpretation:
|
|
64
|
+
* - ≤5 = highly similar
|
|
65
|
+
* - >20 = unrelated
|
|
66
|
+
*
|
|
67
|
+
* @returns integer 0–64
|
|
68
|
+
*/
|
|
69
|
+
export function hammingDistance(hashA: string, hashB: string): number {
|
|
70
|
+
if (!HEX16_RE.test(hashA)) {
|
|
71
|
+
throw new Error(`hammingDistance: hashA is not a 16-char hex string: "${hashA}"`);
|
|
72
|
+
}
|
|
73
|
+
if (!HEX16_RE.test(hashB)) {
|
|
74
|
+
throw new Error(`hammingDistance: hashB is not a 16-char hex string: "${hashB}"`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let dist = 0;
|
|
78
|
+
for (let i = 0; i < 16; i++) {
|
|
79
|
+
const a = parseInt(hashA[i], 16);
|
|
80
|
+
const b = parseInt(hashB[i], 16);
|
|
81
|
+
dist += NIBBLE_POPCOUNT[a ^ b];
|
|
82
|
+
}
|
|
83
|
+
return dist;
|
|
84
|
+
}
|
package/src/lrr/aggregator.ts
CHANGED
|
@@ -8,12 +8,25 @@ export interface ChapterResult {
|
|
|
8
8
|
follow_up_confirmed?: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export interface RoutingTarget {
|
|
12
|
+
decision_id: string;
|
|
13
|
+
resolved_decision_id: string;
|
|
14
|
+
phase: string;
|
|
15
|
+
step_id: string | null;
|
|
16
|
+
decided_by: string;
|
|
17
|
+
superseded: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
export interface AggregateResult {
|
|
12
21
|
combined_verdict: 'PRODUCTION READY' | 'NEEDS WORK' | 'BLOCKED';
|
|
13
22
|
triggered_rule: number;
|
|
14
23
|
chapters: ChapterResult[];
|
|
15
24
|
star_rule_triggered?: boolean;
|
|
16
25
|
star_rule_decision_ids?: string[];
|
|
26
|
+
cross_chapter_contradiction?: string;
|
|
27
|
+
routing_targets?: RoutingTarget[];
|
|
28
|
+
routing_source?: 'graph' | 'fallback';
|
|
29
|
+
routing_warnings?: string[];
|
|
17
30
|
}
|
|
18
31
|
|
|
19
32
|
export function aggregate(chapters: ChapterResult[]): AggregateResult {
|
|
@@ -21,23 +34,24 @@ export function aggregate(chapters: ChapterResult[]): AggregateResult {
|
|
|
21
34
|
if (chapters.some(c => c.override_blocks_launch))
|
|
22
35
|
return { combined_verdict: 'BLOCKED', triggered_rule: 1, chapters };
|
|
23
36
|
|
|
24
|
-
// Rule 6: contradictions between chapters on
|
|
25
|
-
// Two chapters contradict if they reference the same
|
|
26
|
-
// but assign conflicting verdicts (one PASS, one BLOCK
|
|
27
|
-
const
|
|
37
|
+
// Rule 6: contradictions between chapters on related_decision_id → BLOCKED
|
|
38
|
+
// Two chapters contradict if they reference the same decision_id
|
|
39
|
+
// but assign conflicting verdicts (one PASS, one BLOCK).
|
|
40
|
+
const verdictByDecisionId = new Map<string, Set<Verdict>>();
|
|
28
41
|
for (const ch of chapters) {
|
|
29
42
|
for (const f of ch.findings) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
if (!f.related_decision_id) continue;
|
|
44
|
+
const key = f.related_decision_id;
|
|
45
|
+
if (!verdictByDecisionId.has(key)) verdictByDecisionId.set(key, new Set());
|
|
46
|
+
verdictByDecisionId.get(key)!.add(ch.verdict);
|
|
33
47
|
}
|
|
34
48
|
}
|
|
35
|
-
for (const [
|
|
49
|
+
for (const [decId, verdicts] of verdictByDecisionId) {
|
|
36
50
|
if (verdicts.has('PASS') && verdicts.has('BLOCK')) {
|
|
37
51
|
return {
|
|
38
52
|
combined_verdict: 'BLOCKED', triggered_rule: 6, chapters,
|
|
39
|
-
cross_chapter_contradiction:
|
|
40
|
-
}
|
|
53
|
+
cross_chapter_contradiction: decId,
|
|
54
|
+
};
|
|
41
55
|
}
|
|
42
56
|
}
|
|
43
57
|
|
|
@@ -78,3 +92,84 @@ export function applyStarRule(result: AggregateResult): AggregateResult {
|
|
|
78
92
|
}
|
|
79
93
|
return { ...result, star_rule_triggered: false };
|
|
80
94
|
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Slice 4 graph fast-path for backward routing.
|
|
98
|
+
*
|
|
99
|
+
* Given an aggregate result with `star_rule_decision_ids`, resolve each ID to a
|
|
100
|
+
* routing target (phase + step_id + decided_by) by querying the graph layer.
|
|
101
|
+
* Walks `decision_supersedes` via DecisionView.superseded_by so a finding
|
|
102
|
+
* tagged with a resolved-and-replaced decision routes to the replacement's
|
|
103
|
+
* authoring phase.
|
|
104
|
+
*
|
|
105
|
+
* Falls back to the existing string-only path on any graph failure: returns
|
|
106
|
+
* the input result with `routing_source: "fallback"` so the caller knows to
|
|
107
|
+
* use `star_rule_decision_ids` directly with whatever legacy logic it has.
|
|
108
|
+
*/
|
|
109
|
+
export async function resolveRoutingTargets(
|
|
110
|
+
result: AggregateResult,
|
|
111
|
+
projectDir: string,
|
|
112
|
+
): Promise<AggregateResult> {
|
|
113
|
+
if (!result.star_rule_triggered || !result.star_rule_decision_ids?.length) {
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let loadAllGraphs: typeof import('../graph/storage/index.js').loadAllGraphs;
|
|
118
|
+
let queryDecisions: typeof import('../graph/storage/index.js').queryDecisions;
|
|
119
|
+
try {
|
|
120
|
+
const mod = await import('../graph/storage/index.js');
|
|
121
|
+
loadAllGraphs = mod.loadAllGraphs;
|
|
122
|
+
queryDecisions = mod.queryDecisions;
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return {
|
|
125
|
+
...result,
|
|
126
|
+
routing_source: 'fallback',
|
|
127
|
+
routing_warnings: [`graph storage import failed: ${err instanceof Error ? err.message : String(err)}`],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const graph = loadAllGraphs(projectDir);
|
|
132
|
+
if (!graph) {
|
|
133
|
+
return {
|
|
134
|
+
...result,
|
|
135
|
+
routing_source: 'fallback',
|
|
136
|
+
routing_warnings: [`no graph fragment in ${projectDir} — string-only routing path`],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const targetIds = new Set(result.star_rule_decision_ids);
|
|
141
|
+
const allViews = queryDecisions(graph, {});
|
|
142
|
+
const byDecisionId = new Map(allViews.map(v => [v.decision_id, v]));
|
|
143
|
+
|
|
144
|
+
const targets: RoutingTarget[] = [];
|
|
145
|
+
const warnings: string[] = [];
|
|
146
|
+
|
|
147
|
+
for (const decId of result.star_rule_decision_ids) {
|
|
148
|
+
const view = byDecisionId.get(decId);
|
|
149
|
+
if (!view) {
|
|
150
|
+
warnings.push(`decision_id "${decId}" not found in graph`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const replacement = view.superseded_by
|
|
155
|
+
? allViews.find(v => v.id === view.superseded_by!.id)
|
|
156
|
+
: undefined;
|
|
157
|
+
const resolved = replacement ?? view;
|
|
158
|
+
|
|
159
|
+
targets.push({
|
|
160
|
+
decision_id: decId,
|
|
161
|
+
resolved_decision_id: resolved.decision_id,
|
|
162
|
+
phase: resolved.phase,
|
|
163
|
+
step_id: resolved.step_id,
|
|
164
|
+
decided_by: resolved.decided_by,
|
|
165
|
+
superseded: replacement !== undefined,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
...result,
|
|
171
|
+
routing_source: 'graph',
|
|
172
|
+
routing_targets: targets,
|
|
173
|
+
...(warnings.length > 0 ? { routing_warnings: warnings } : {}),
|
|
174
|
+
};
|
|
175
|
+
}
|