buildanything 2.0.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +9 -1
  3. package/README.md +57 -61
  4. package/agents/a11y-architect.md +2 -0
  5. package/agents/briefing-officer.md +172 -0
  6. package/agents/business-model.md +14 -12
  7. package/agents/code-architect.md +6 -1
  8. package/agents/code-reviewer.md +3 -2
  9. package/agents/code-simplifier.md +12 -4
  10. package/agents/design-brand-guardian.md +19 -0
  11. package/agents/design-critic.md +16 -11
  12. package/agents/design-inclusive-visuals-specialist.md +2 -0
  13. package/agents/design-ui-designer.md +17 -0
  14. package/agents/design-ux-architect.md +15 -0
  15. package/agents/design-ux-researcher.md +102 -7
  16. package/agents/engineering-ai-engineer.md +2 -0
  17. package/agents/engineering-backend-architect.md +2 -0
  18. package/agents/engineering-data-engineer.md +2 -0
  19. package/agents/engineering-devops-automator.md +2 -0
  20. package/agents/engineering-frontend-developer.md +13 -0
  21. package/agents/engineering-mobile-app-builder.md +2 -0
  22. package/agents/engineering-rapid-prototyper.md +15 -2
  23. package/agents/engineering-security-engineer.md +2 -0
  24. package/agents/engineering-senior-developer.md +13 -0
  25. package/agents/engineering-sre.md +2 -0
  26. package/agents/engineering-technical-writer.md +2 -0
  27. package/agents/feature-intel.md +8 -7
  28. package/agents/ios-app-review-guardian.md +2 -0
  29. package/agents/ios-foundation-models-specialist.md +2 -0
  30. package/agents/ios-product-reality-auditor.md +292 -0
  31. package/agents/ios-storekit-specialist.md +2 -0
  32. package/agents/ios-swift-architect.md +1 -0
  33. package/agents/ios-swift-search.md +1 -0
  34. package/agents/ios-swift-ui-design.md +7 -4
  35. package/agents/marketing-app-store-optimizer.md +2 -0
  36. package/agents/planner.md +6 -1
  37. package/agents/pr-test-analyzer.md +3 -2
  38. package/agents/product-feedback-synthesizer.md +62 -0
  39. package/agents/product-owner.md +163 -0
  40. package/agents/product-reality-auditor.md +216 -0
  41. package/agents/product-spec-writer.md +176 -0
  42. package/agents/refactor-cleaner.md +9 -1
  43. package/agents/security-reviewer.md +2 -1
  44. package/agents/silent-failure-hunter.md +2 -1
  45. package/agents/swift-build-resolver.md +2 -0
  46. package/agents/swift-reviewer.md +2 -1
  47. package/agents/tech-feasibility.md +5 -3
  48. package/agents/testing-api-tester.md +2 -0
  49. package/agents/testing-evidence-collector.md +24 -0
  50. package/agents/testing-performance-benchmarker.md +2 -0
  51. package/agents/testing-reality-checker.md +2 -1
  52. package/agents/visual-research.md +7 -5
  53. package/bin/adapters/scribe-tool.ts +4 -2
  54. package/bin/adapters/write-lease-tool.ts +1 -1
  55. package/bin/buildanything-runtime.ts +20 -107
  56. package/bin/graph-index.js +24 -0
  57. package/bin/graph-index.ts +340 -0
  58. package/bin/mcp-servers/graph-mcp.js +26 -0
  59. package/bin/mcp-servers/graph-mcp.ts +481 -0
  60. package/bin/mcp-servers/orchestrator-mcp.js +26 -0
  61. package/bin/mcp-servers/orchestrator-mcp.ts +361 -0
  62. package/bin/setup.js +272 -111
  63. package/commands/build.md +424 -177
  64. package/commands/idea-sweep.md +2 -2
  65. package/commands/setup.md +15 -4
  66. package/commands/ux-review.md +3 -3
  67. package/commands/verify.md +3 -0
  68. package/docs/migration/phase-graph.yaml +573 -157
  69. package/hooks/design-md-lint +4 -0
  70. package/hooks/design-md-lint.ts +295 -0
  71. package/hooks/pre-tool-use.ts +37 -6
  72. package/hooks/record-mode-transitions.ts +63 -6
  73. package/hooks/subagent-start.ts +3 -2
  74. package/package.json +3 -1
  75. package/protocols/agent-prompt-authoring.md +165 -0
  76. package/protocols/architecture-schema.md +10 -3
  77. package/protocols/cleanup.md +4 -0
  78. package/protocols/decision-log.md +8 -4
  79. package/protocols/design-md-authoring.md +520 -0
  80. package/protocols/design-md-spec.md +362 -0
  81. package/protocols/fake-data-detector.md +1 -1
  82. package/protocols/ios-fake-data-detector.md +65 -0
  83. package/protocols/ios-phase-branches.md +128 -43
  84. package/protocols/launch-readiness.md +9 -5
  85. package/protocols/metric-loop.md +1 -1
  86. package/protocols/page-spec-schema.md +234 -0
  87. package/protocols/product-spec-schema.md +354 -0
  88. package/protocols/sprint-tasks-schema.md +53 -0
  89. package/protocols/state-schema.json +38 -3
  90. package/protocols/state-schema.md +32 -2
  91. package/protocols/verify.md +29 -1
  92. package/protocols/web-phase-branches.md +246 -76
  93. package/skills/ios/ios-bootstrap/SKILL.md +1 -1
  94. package/src/graph/ids.ts +86 -0
  95. package/src/graph/index.ts +32 -0
  96. package/src/graph/parser/architecture.ts +603 -0
  97. package/src/graph/parser/component-manifest.ts +268 -0
  98. package/src/graph/parser/decisions-jsonl.ts +407 -0
  99. package/src/graph/parser/design-md-pass2.ts +253 -0
  100. package/src/graph/parser/design-md.ts +477 -0
  101. package/src/graph/parser/page-spec.ts +496 -0
  102. package/src/graph/parser/product-spec.ts +930 -0
  103. package/src/graph/parser/screenshot.ts +342 -0
  104. package/src/graph/parser/sprint-tasks.ts +317 -0
  105. package/src/graph/storage/index.ts +1154 -0
  106. package/src/graph/types.ts +432 -0
  107. package/src/graph/util/dhash.ts +84 -0
  108. package/src/lrr/aggregator.ts +105 -10
  109. package/src/orchestrator/hooks/context-header.ts +34 -10
  110. package/src/orchestrator/hooks/token-accounting.ts +25 -14
  111. package/src/orchestrator/mcp/cycle-counter.ts +2 -1
  112. package/src/orchestrator/mcp/scribe.ts +27 -16
  113. package/src/orchestrator/mcp/write-lease.ts +30 -13
  114. package/src/orchestrator/phase4-shared-context.ts +20 -4
  115. package/protocols/visual-dna.md +0 -185
@@ -0,0 +1,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
+ }
@@ -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 typed fields → BLOCKED
25
- // Two chapters contradict if they reference the same finding description
26
- // but assign conflicting verdicts (one PASS, one BLOCK on the same topic).
27
- const findingsByDesc = new Map<string, Set<Verdict>>();
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
- const key = f.description.toLowerCase().trim();
31
- if (!findingsByDesc.has(key)) findingsByDesc.set(key, new Set());
32
- findingsByDesc.get(key)!.add(ch.verdict);
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 [desc, verdicts] of findingsByDesc) {
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: desc,
40
- } as AggregateResult;
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
+ }