@harness-engineering/graph 0.9.0 → 0.11.0

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/dist/index.js CHANGED
@@ -2327,6 +2327,23 @@ var CODE_NODE_TYPES2 = [
2327
2327
  "variable"
2328
2328
  ];
2329
2329
  var MEASURABLE_TYPES = /* @__PURE__ */ new Set(["business_process", "business_concept"]);
2330
+ var STRATEGY_REQUIRED_SECTIONS = [
2331
+ "Target problem",
2332
+ "Our approach",
2333
+ "Who it's for",
2334
+ "Key metrics",
2335
+ "Tracks"
2336
+ ];
2337
+ var STRATEGY_OPTIONAL_SECTIONS = [
2338
+ "Milestones",
2339
+ "Not working on",
2340
+ "Marketing"
2341
+ ];
2342
+ var STRATEGY_KNOWN_SECTIONS = /* @__PURE__ */ new Set([
2343
+ ...STRATEGY_REQUIRED_SECTIONS,
2344
+ ...STRATEGY_OPTIONAL_SECTIONS
2345
+ ]);
2346
+ var STRATEGY_PLACEHOLDER_RE = /^<[^>]+>\s*$/;
2330
2347
  var BusinessKnowledgeIngestor = class {
2331
2348
  constructor(store) {
2332
2349
  this.store = store;
@@ -2411,6 +2428,75 @@ var BusinessKnowledgeIngestor = class {
2411
2428
  durationMs: Date.now() - start
2412
2429
  };
2413
2430
  }
2431
+ /**
2432
+ * Ingest the repo-root STRATEGY.md anchor as `business_fact` nodes — one per
2433
+ * non-empty section. Soft-fails when the file is absent (returns empty result
2434
+ * with no errors) so existing projects without a strategy doc keep working.
2435
+ *
2436
+ * Each emitted node carries `metadata.domain === 'strategy'` and
2437
+ * `metadata.source === 'STRATEGY.md'`, making the strategy domain
2438
+ * discoverable through the same filters as other business-knowledge nodes.
2439
+ */
2440
+ async ingestStrategy(strategyPath) {
2441
+ const start = Date.now();
2442
+ const errors = [];
2443
+ let raw;
2444
+ try {
2445
+ raw = await fs3.readFile(strategyPath, "utf-8");
2446
+ } catch {
2447
+ return emptyResult(Date.now() - start);
2448
+ }
2449
+ const parsed = parseStrategyMarkdown(raw);
2450
+ if (!parsed) {
2451
+ errors.push(`${strategyPath}: no frontmatter found`);
2452
+ return { ...emptyResult(Date.now() - start), errors };
2453
+ }
2454
+ const relPath = path4.basename(strategyPath);
2455
+ let nodesAdded = 0;
2456
+ for (const section of parsed.sections) {
2457
+ const node = this.buildStrategyNode(section, parsed.frontmatter, relPath);
2458
+ if (node === null) continue;
2459
+ this.store.addNode(node);
2460
+ nodesAdded++;
2461
+ }
2462
+ return {
2463
+ nodesAdded,
2464
+ nodesUpdated: 0,
2465
+ edgesAdded: 0,
2466
+ edgesUpdated: 0,
2467
+ errors,
2468
+ durationMs: Date.now() - start
2469
+ };
2470
+ }
2471
+ /**
2472
+ * Build a single STRATEGY.md `business_fact` node from a parsed section.
2473
+ * Returns null when the section is unknown, empty, or carries unfilled
2474
+ * template placeholder text — callers iterate and skip nulls.
2475
+ */
2476
+ buildStrategyNode(section, frontmatter, relPath) {
2477
+ if (!STRATEGY_KNOWN_SECTIONS.has(section.name)) return null;
2478
+ const body = section.body.trim();
2479
+ if (body.length === 0) return null;
2480
+ if (STRATEGY_PLACEHOLDER_RE.test(body)) return null;
2481
+ const productName = typeof frontmatter.name === "string" ? frontmatter.name : "unnamed-product";
2482
+ return {
2483
+ id: `bk:strategy:${slugifyStrategySection(section.name)}`,
2484
+ type: "business_fact",
2485
+ name: section.name,
2486
+ path: relPath,
2487
+ content: body,
2488
+ metadata: {
2489
+ domain: "strategy",
2490
+ source: "STRATEGY.md",
2491
+ section_name: section.name,
2492
+ product_name: productName,
2493
+ ...typeof frontmatter.last_updated === "string" && {
2494
+ last_updated: frontmatter.last_updated
2495
+ },
2496
+ ...typeof frontmatter.version === "number" && { version: frontmatter.version }
2497
+ }
2498
+ };
2499
+ }
2414
2500
  async createNodes(files, knowledgeDir, errors) {
2415
2501
  const entries = [];
2416
2502
  for (const filePath of files) {
@@ -2533,6 +2619,49 @@ function parseFrontmatter(raw) {
2533
2619
  body
2534
2620
  };
2535
2621
  }
2622
+ function slugifyStrategySection(name) {
2623
+ return name.toLowerCase().replace(/'/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
2624
+ }
2625
+ function parseStrategyMarkdown(raw) {
2626
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
2627
+ if (!match) return null;
2628
+ const yamlBlock = match[1];
2629
+ const body = match[2];
2630
+ const frontmatter = {};
2631
+ for (const line of yamlBlock.split(/\r?\n/)) {
2632
+ const kvMatch = line.match(/^(\w+):\s*(.+)$/);
2633
+ if (!kvMatch) continue;
2634
+ const key = kvMatch[1];
2635
+ const rawValue = kvMatch[2].trim();
2636
+ const unquoted = rawValue.replace(/^["']|["']$/g, "");
2637
+ const asNumber = Number(unquoted);
2638
+ if (unquoted !== "" && !Number.isNaN(asNumber) && /^-?\d+(?:\.\d+)?$/.test(unquoted)) {
2639
+ frontmatter[key] = asNumber;
2640
+ } else {
2641
+ frontmatter[key] = unquoted;
2642
+ }
2643
+ }
2644
+ const sections = [];
2645
+ const h2Re = /^##[ \t]+(.+?)[ \t]*$/gm;
2646
+ const matches = [];
2647
+ let m;
2648
+ while ((m = h2Re.exec(body)) !== null) {
2649
+ matches.push({
2650
+ name: (m[1] ?? "").trim(),
2651
+ headingStart: m.index,
2652
+ bodyStart: m.index + m[0].length
2653
+ });
2654
+ }
2655
+ for (let i = 0; i < matches.length; i++) {
2656
+ const current = matches[i];
2657
+ const sliceEnd = matches[i + 1]?.headingStart ?? body.length;
2658
+ sections.push({
2659
+ name: current.name,
2660
+ body: body.slice(current.bodyStart, sliceEnd).trim()
2661
+ });
2662
+ }
2663
+ return { frontmatter, sections };
2664
+ }
2536
2665
  function parseSolutionFrontmatter(raw) {
2537
2666
  const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
2538
2667
  if (!match) return null;
@@ -2564,6 +2693,12 @@ var CODE_NODE_TYPES3 = [
2564
2693
  "interface",
2565
2694
  "variable"
2566
2695
  ];
2696
+ var ARCHITECTURE_ADR_H1 = /^\s*#\s+ADR[-\s]*(\d+)\s*[:\-—]\s*(.+?)\s*$/im;
2697
+ function matchField(body, field) {
2698
+ const re = new RegExp(`\\*\\*${field}:\\*\\*\\s*(.+)`, "i");
2699
+ const m = body.match(re);
2700
+ return m ? m[1].trim() : void 0;
2701
+ }
2567
2702
  var DecisionIngestor = class {
2568
2703
  constructor(store) {
2569
2704
  this.store = store;
@@ -2620,6 +2755,71 @@ var DecisionIngestor = class {
2620
2755
  durationMs: Date.now() - start
2621
2756
  };
2622
2757
  }
2758
+ /**
2759
+ * Ingest ADRs written by `harness-architecture-advisor` from
2760
+ * `docs/architecture/<topic>/ADR-<n>.md`. These files do not carry YAML
2761
+ * frontmatter — the canonical format is:
2762
+ *
2763
+ * ```
2764
+ * # ADR-<n>: <Title>
2765
+ *
2766
+ * **Date:** <date>
2767
+ * **Status:** Accepted | Proposed | Superseded | Deprecated
2768
+ * **Deciders:** <who>
2769
+ * ```
2770
+ *
2771
+ * The first directory under `architectureDir` becomes `metadata.domain`
2772
+ * (the "topic"), so projects whose only knowledge substrate is ADRs surface
2773
+ * `architecture/<topic>` as a documented domain rather than reporting empty.
2774
+ *
2775
+ * Soft-fails when the directory is absent (the common case for projects that
2776
+ * do not use the architecture-advisor convention).
2777
+ */
2778
+ async ingestArchitecture(architectureDir) {
2779
+ const start = Date.now();
2780
+ const errors = [];
2781
+ let files;
2782
+ try {
2783
+ files = await this.findArchitectureAdrFiles(architectureDir);
2784
+ } catch {
2785
+ return emptyResult(Date.now() - start);
2786
+ }
2787
+ let nodesAdded = 0;
2788
+ let edgesAdded = 0;
2789
+ for (const filePath of files) {
2790
+ const delta = await this.processArchitectureAdrFile(filePath, architectureDir, errors);
2791
+ nodesAdded += delta.nodesAdded;
2792
+ edgesAdded += delta.edgesAdded;
2793
+ }
2794
+ return {
2795
+ nodesAdded,
2796
+ nodesUpdated: 0,
2797
+ edgesAdded,
2798
+ edgesUpdated: 0,
2799
+ errors,
2800
+ durationMs: Date.now() - start
2801
+ };
2802
+ }
2803
+ /**
2804
+ * Read, parse, and add a single architecture-advisor ADR. Returns the delta
2805
+ * the caller should fold into the aggregate counts. Errors are appended to
2806
+ * the shared `errors` array rather than thrown, matching the soft-fail
2807
+ * contract of `ingest()`.
2808
+ */
2809
+ async processArchitectureAdrFile(filePath, architectureDir, errors) {
2810
+ try {
2811
+ const raw = await fs4.readFile(filePath, "utf-8");
2812
+ const parsed = parseArchitectureAdr(raw);
2813
+ if (!parsed) return { nodesAdded: 0, edgesAdded: 0 };
2814
+ const node = buildArchitectureAdrNode(parsed, filePath, architectureDir);
2815
+ this.store.addNode(node);
2816
+ const edgesAdded = this.linkToCode(parsed.body, node.id);
2817
+ return { nodesAdded: 1, edgesAdded };
2818
+ } catch (err) {
2819
+ errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
2820
+ return { nodesAdded: 0, edgesAdded: 0 };
2821
+ }
2822
+ }
2623
2823
  parseFrontmatter(raw) {
2624
2824
  const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
2625
2825
  if (!match) return null;
@@ -2661,7 +2861,63 @@ var DecisionIngestor = class {
2661
2861
  const entries = await fs4.readdir(dir, { withFileTypes: true });
2662
2862
  return entries.filter((e) => e.isFile() && e.name.endsWith(".md") && e.name !== "README.md").map((e) => path5.join(dir, e.name));
2663
2863
  }
2864
+ /**
2865
+ * Recursively find `ADR-*.md` files under `dir`. Skips default skip dirs
2866
+ * (node_modules etc.) to keep the scan bounded on large repos.
2867
+ */
2868
+ async findArchitectureAdrFiles(dir) {
2869
+ const results = [];
2870
+ const entries = await fs4.readdir(dir, { withFileTypes: true });
2871
+ for (const entry of entries) {
2872
+ const full = path5.join(dir, entry.name);
2873
+ if (entry.isDirectory()) {
2874
+ if (DEFAULT_SKIP_DIRS.has(entry.name)) continue;
2875
+ results.push(...await this.findArchitectureAdrFiles(full));
2876
+ } else if (entry.isFile() && entry.name.endsWith(".md") && /^ADR[-_]?\d/i.test(entry.name)) {
2877
+ results.push(full);
2878
+ }
2879
+ }
2880
+ return results;
2881
+ }
2664
2882
  };
2883
+ function buildArchitectureAdrNode(parsed, filePath, architectureDir) {
2884
+ const filename = path5.basename(filePath, ".md");
2885
+ const relFromArch = path5.relative(architectureDir, filePath).replaceAll("\\", "/");
2886
+ const topic = relFromArch.includes("/") ? relFromArch.split("/")[0] : "";
2887
+ const nodeId = topic ? `decision:architecture:${topic}:${filename}` : `decision:architecture:${filename}`;
2888
+ return {
2889
+ id: nodeId,
2890
+ type: "decision",
2891
+ name: parsed.title,
2892
+ path: filePath,
2893
+ content: parsed.body.trim(),
2894
+ metadata: {
2895
+ number: parsed.number,
2896
+ ...topic && { domain: topic },
2897
+ source: "architecture",
2898
+ ...parsed.date && { date: parsed.date },
2899
+ ...parsed.status && { status: parsed.status },
2900
+ ...parsed.deciders && { deciders: parsed.deciders }
2901
+ }
2902
+ };
2903
+ }
2904
+ function parseArchitectureAdr(raw) {
2905
+ const h1 = raw.match(ARCHITECTURE_ADR_H1);
2906
+ if (!h1) return null;
2907
+ const number = h1[1];
2908
+ const title = h1[2].trim();
2909
+ const date = matchField(raw, "Date");
2910
+ const status = matchField(raw, "Status");
2911
+ const deciders = matchField(raw, "Deciders");
2912
+ return {
2913
+ number,
2914
+ title,
2915
+ body: raw,
2916
+ ...date && { date },
2917
+ ...status && { status },
2918
+ ...deciders && { deciders }
2919
+ };
2920
+ }
2665
2921
 
2666
2922
  // src/ingest/RequirementIngestor.ts
2667
2923
  var fs5 = __toESM(require("fs/promises"));
@@ -3011,7 +3267,7 @@ function emptyResult2(diagramType = "unknown") {
3011
3267
  function detectDiagramType(content) {
3012
3268
  for (const line of content.split("\n")) {
3013
3269
  const trimmed = line.trim();
3014
- if (!trimmed) continue;
3270
+ if (!trimmed || trimmed.startsWith("%%")) continue;
3015
3271
  if (/^(?:graph|flowchart)\b/i.test(trimmed)) return "flowchart";
3016
3272
  if (/^sequenceDiagram\b/.test(trimmed)) return "sequence";
3017
3273
  if (/^classDiagram\b/.test(trimmed)) return "class";
@@ -5783,6 +6039,16 @@ var SNAPSHOT_NODE_TYPES = [
5783
6039
  "aesthetic_intent",
5784
6040
  "image_annotation"
5785
6041
  ];
6042
+ function emptyIngestResult() {
6043
+ return {
6044
+ nodesAdded: 0,
6045
+ nodesUpdated: 0,
6046
+ edgesAdded: 0,
6047
+ edgesUpdated: 0,
6048
+ errors: [],
6049
+ durationMs: 0
6050
+ };
6051
+ }
5786
6052
  var KnowledgePipelineRunner = class {
5787
6053
  constructor(store) {
5788
6054
  this.store = store;
@@ -5793,8 +6059,10 @@ var KnowledgePipelineRunner = class {
5793
6059
  async run(options) {
5794
6060
  this.inferenceOptions = options.inferenceOptions ?? {};
5795
6061
  const remediations = [];
6062
+ const ingestErrors = [];
5796
6063
  const preSnapshot = this.buildSnapshot(options.domain);
5797
6064
  const extraction = await this.extract(options);
6065
+ ingestErrors.push(...extraction.errors);
5798
6066
  const postSnapshot = this.buildSnapshot(options.domain);
5799
6067
  let driftResult = this.reconcile(preSnapshot, postSnapshot);
5800
6068
  const contradictions = new ContradictionDetector().detect(this.store);
@@ -5807,7 +6075,8 @@ var KnowledgePipelineRunner = class {
5807
6075
  options,
5808
6076
  driftResult,
5809
6077
  gapReport,
5810
- remediations
6078
+ remediations,
6079
+ ingestErrors
5811
6080
  );
5812
6081
  iterations = loopResult.iterations;
5813
6082
  materialization = loopResult.materialization;
@@ -5821,7 +6090,8 @@ var KnowledgePipelineRunner = class {
5821
6090
  return this.buildResult(
5822
6091
  driftResult,
5823
6092
  iterations,
5824
- extraction,
6093
+ extraction.counts,
6094
+ ingestErrors,
5825
6095
  gapReport,
5826
6096
  remediations,
5827
6097
  contradictions,
@@ -5830,7 +6100,7 @@ var KnowledgePipelineRunner = class {
5830
6100
  );
5831
6101
  }
5832
6102
  /** Run the remediation convergence loop; returns iteration count and accumulated materialization. */
5833
- async runRemediationLoop(options, driftResult, gapReport, remediations) {
6103
+ async runRemediationLoop(options, driftResult, gapReport, remediations, ingestErrors) {
5834
6104
  const maxIterations = options.maxIterations ?? 5;
5835
6105
  let iterations = 1;
5836
6106
  let currentDrift = driftResult;
@@ -5851,7 +6121,8 @@ var KnowledgePipelineRunner = class {
5851
6121
  }
5852
6122
  }
5853
6123
  const preSnapshot = this.buildSnapshot(options.domain);
5854
- await this.extract(options);
6124
+ const reExtract = await this.extract(options);
6125
+ ingestErrors.push(...reExtract.errors);
5855
6126
  const postSnapshot = this.buildSnapshot(options.domain);
5856
6127
  currentDrift = this.reconcile(preSnapshot, postSnapshot);
5857
6128
  currentGapReport = await this.detect(options);
@@ -5866,13 +6137,14 @@ var KnowledgePipelineRunner = class {
5866
6137
  };
5867
6138
  }
5868
6139
  /** Assemble the final pipeline result. */
5869
- buildResult(driftResult, iterations, extraction, gaps, remediations, contradictions, coverage, materialization) {
6140
+ buildResult(driftResult, iterations, extraction, errors, gaps, remediations, contradictions, coverage, materialization) {
5870
6141
  return {
5871
6142
  verdict: this.computeVerdict(driftResult),
5872
6143
  driftScore: driftResult.driftScore,
5873
6144
  iterations,
5874
6145
  findings: driftResult.summary,
5875
6146
  extraction,
6147
+ errors: Array.from(new Set(errors)),
5876
6148
  gaps,
5877
6149
  remediations,
5878
6150
  contradictions,
@@ -5903,58 +6175,60 @@ var KnowledgePipelineRunner = class {
5903
6175
  try {
5904
6176
  bkResult = await bkIngestor.ingest(knowledgeDir);
5905
6177
  } catch {
5906
- bkResult = {
5907
- nodesAdded: 0,
5908
- nodesUpdated: 0,
5909
- edgesAdded: 0,
5910
- edgesUpdated: 0,
5911
- errors: [],
5912
- durationMs: 0
5913
- };
6178
+ bkResult = emptyIngestResult();
5914
6179
  }
5915
6180
  const solutionsDir = path12.join(options.projectDir, "docs", "solutions");
5916
6181
  let solutionsResult;
5917
6182
  try {
5918
6183
  solutionsResult = await bkIngestor.ingestSolutions(solutionsDir);
5919
6184
  } catch {
5920
- solutionsResult = {
5921
- nodesAdded: 0,
5922
- nodesUpdated: 0,
5923
- edgesAdded: 0,
5924
- edgesUpdated: 0,
5925
- errors: [],
5926
- durationMs: 0
5927
- };
6185
+ solutionsResult = emptyIngestResult();
6186
+ }
6187
+ const strategyPath = path12.join(options.projectDir, "STRATEGY.md");
6188
+ let strategyResult;
6189
+ try {
6190
+ strategyResult = await bkIngestor.ingestStrategy(strategyPath);
6191
+ } catch {
6192
+ strategyResult = emptyIngestResult();
5928
6193
  }
5929
6194
  bkResult = {
5930
6195
  ...bkResult,
5931
- nodesAdded: bkResult.nodesAdded + solutionsResult.nodesAdded,
5932
- errors: [...bkResult.errors, ...solutionsResult.errors]
6196
+ nodesAdded: bkResult.nodesAdded + solutionsResult.nodesAdded + strategyResult.nodesAdded,
6197
+ errors: [...bkResult.errors, ...solutionsResult.errors, ...strategyResult.errors]
5933
6198
  };
5934
6199
  const decisionsDir = path12.join(options.projectDir, "docs", "knowledge", "decisions");
6200
+ const architectureDir = path12.join(options.projectDir, "docs", "architecture");
5935
6201
  const decisionIngestor = new DecisionIngestor(this.store);
5936
6202
  let decisionResult;
5937
6203
  try {
5938
6204
  decisionResult = await decisionIngestor.ingest(decisionsDir);
5939
6205
  } catch {
5940
- decisionResult = {
5941
- nodesAdded: 0,
5942
- nodesUpdated: 0,
5943
- edgesAdded: 0,
5944
- edgesUpdated: 0,
5945
- errors: [],
5946
- durationMs: 0
5947
- };
6206
+ decisionResult = emptyIngestResult();
5948
6207
  }
6208
+ let architectureResult;
6209
+ try {
6210
+ architectureResult = await decisionIngestor.ingestArchitecture(architectureDir);
6211
+ } catch {
6212
+ architectureResult = emptyIngestResult();
6213
+ }
6214
+ decisionResult = {
6215
+ ...decisionResult,
6216
+ nodesAdded: decisionResult.nodesAdded + architectureResult.nodesAdded,
6217
+ edgesAdded: decisionResult.edgesAdded + architectureResult.edgesAdded,
6218
+ errors: [...decisionResult.errors, ...architectureResult.errors]
6219
+ };
5949
6220
  const linker = new KnowledgeLinker(this.store, extractedDir);
5950
6221
  const linkResult = await linker.link();
5951
6222
  return {
5952
- codeSignals: extractionResult.nodesAdded,
5953
- diagrams: diagramResult.nodesAdded,
5954
- linkerFacts: linkResult.factsCreated,
5955
- businessKnowledge: bkResult.nodesAdded,
5956
- decisions: decisionResult.nodesAdded,
5957
- images: imageCount
6223
+ counts: {
6224
+ codeSignals: extractionResult.nodesAdded,
6225
+ diagrams: diagramResult.nodesAdded,
6226
+ linkerFacts: linkResult.factsCreated,
6227
+ businessKnowledge: bkResult.nodesAdded,
6228
+ decisions: decisionResult.nodesAdded,
6229
+ images: imageCount
6230
+ },
6231
+ errors: [...bkResult.errors, ...decisionResult.errors]
5958
6232
  };
5959
6233
  }
5960
6234
  // ── Phase 2: RECONCILE ────────────────────────────────────────────────────
@@ -9383,6 +9657,16 @@ var DesignIngestor = class {
9383
9657
  };
9384
9658
 
9385
9659
  // src/constraints/DesignConstraintAdapter.ts
9660
+ var CODE_PREFIX_LABELS = {
9661
+ "ANAT-D": "Component anatomy (definition)",
9662
+ "ANAT-P": "Component anatomy (pattern presence)",
9663
+ "ANAT-U": "Component anatomy (usage)",
9664
+ "CRAFT-C": "Design craft (critique)",
9665
+ "CRAFT-P": "Design craft (polish)",
9666
+ "CRAFT-B": "Design craft (benchmark)",
9667
+ "DESIGN-": "Design constraint (legacy)",
9668
+ "A11Y-": "Accessibility"
9669
+ };
9386
9670
  var DesignConstraintAdapter = class {
9387
9671
  constructor(store) {
9388
9672
  this.store = store;
@@ -9464,6 +9748,76 @@ var DesignConstraintAdapter = class {
9464
9748
  return "error";
9465
9749
  }
9466
9750
  }
9751
+ /**
9752
+ * Record externally-computed craft findings (audit-component-anatomy,
9753
+ * harness-design-craft, etc.) as graph state. Idempotent — re-running on
9754
+ * the same findings produces no duplicate nodes or edges thanks to
9755
+ * GraphStore's keyed merge semantics.
9756
+ *
9757
+ * Each finding becomes:
9758
+ * • a `design_constraint` node keyed by finding code (created lazily
9759
+ * if absent; metadata merged on re-record so the most recent message
9760
+ * / severity wins)
9761
+ * • a `violates_design` edge from the source file (id = file path) to
9762
+ * the constraint node, with per-finding metadata (line, severity,
9763
+ * message, evidence, runId)
9764
+ *
9765
+ * The file node is NOT created here — it's assumed to already exist in
9766
+ * the graph from a prior ingest. If it does not, the edge will still be
9767
+ * created (graph stores edges by key, not by referential integrity) and
9768
+ * a subsequent ingest will populate the file node.
9769
+ */
9770
+ recordFindings(findings) {
9771
+ let constraintsAdded = 0;
9772
+ let edgesAdded = 0;
9773
+ const seenConstraintIds = /* @__PURE__ */ new Set();
9774
+ for (const finding of findings) {
9775
+ const constraintId = `design_constraint:${finding.code}`;
9776
+ if (!seenConstraintIds.has(finding.code)) {
9777
+ const existing = this.store.getNode(constraintId);
9778
+ if (!existing) constraintsAdded += 1;
9779
+ this.store.addNode({
9780
+ id: constraintId,
9781
+ type: "design_constraint",
9782
+ name: finding.code,
9783
+ metadata: {
9784
+ code: finding.code,
9785
+ label: this.labelForCode(finding.code),
9786
+ mostRecentMessage: finding.message,
9787
+ mostRecentSeverity: finding.severity
9788
+ }
9789
+ });
9790
+ seenConstraintIds.add(finding.code);
9791
+ }
9792
+ const fileId = finding.file;
9793
+ const edgeMetadata = {
9794
+ message: finding.message,
9795
+ severity: finding.severity
9796
+ };
9797
+ if (finding.line !== void 0) edgeMetadata.line = finding.line;
9798
+ if (finding.evidence !== void 0) edgeMetadata.evidence = finding.evidence;
9799
+ if (finding.runId !== void 0) edgeMetadata.runId = finding.runId;
9800
+ const before = this.store.getEdges({
9801
+ from: fileId,
9802
+ to: constraintId,
9803
+ type: "violates_design"
9804
+ });
9805
+ this.store.addEdge({
9806
+ from: fileId,
9807
+ to: constraintId,
9808
+ type: "violates_design",
9809
+ metadata: edgeMetadata
9810
+ });
9811
+ if (before.length === 0) edgesAdded += 1;
9812
+ }
9813
+ return { constraintsAdded, edgesAdded };
9814
+ }
9815
+ labelForCode(code) {
9816
+ for (const prefix of Object.keys(CODE_PREFIX_LABELS)) {
9817
+ if (code.startsWith(prefix)) return CODE_PREFIX_LABELS[prefix];
9818
+ }
9819
+ return "Design constraint";
9820
+ }
9467
9821
  };
9468
9822
 
9469
9823
  // src/feedback/GraphFeedbackAdapter.ts
@@ -9991,7 +10345,7 @@ var ConflictPredictor = class {
9991
10345
  };
9992
10346
 
9993
10347
  // src/index.ts
9994
- var VERSION = "0.6.0";
10348
+ var VERSION = "0.9.0";
9995
10349
  // Annotate the CommonJS export names for ESM import in node:
9996
10350
  0 && (module.exports = {
9997
10351
  ALL_EXTRACTORS,