@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.mjs CHANGED
@@ -2212,6 +2212,23 @@ var CODE_NODE_TYPES2 = [
2212
2212
  "variable"
2213
2213
  ];
2214
2214
  var MEASURABLE_TYPES = /* @__PURE__ */ new Set(["business_process", "business_concept"]);
2215
+ var STRATEGY_REQUIRED_SECTIONS = [
2216
+ "Target problem",
2217
+ "Our approach",
2218
+ "Who it's for",
2219
+ "Key metrics",
2220
+ "Tracks"
2221
+ ];
2222
+ var STRATEGY_OPTIONAL_SECTIONS = [
2223
+ "Milestones",
2224
+ "Not working on",
2225
+ "Marketing"
2226
+ ];
2227
+ var STRATEGY_KNOWN_SECTIONS = /* @__PURE__ */ new Set([
2228
+ ...STRATEGY_REQUIRED_SECTIONS,
2229
+ ...STRATEGY_OPTIONAL_SECTIONS
2230
+ ]);
2231
+ var STRATEGY_PLACEHOLDER_RE = /^<[^>]+>\s*$/;
2215
2232
  var BusinessKnowledgeIngestor = class {
2216
2233
  constructor(store) {
2217
2234
  this.store = store;
@@ -2296,6 +2313,75 @@ var BusinessKnowledgeIngestor = class {
2296
2313
  durationMs: Date.now() - start
2297
2314
  };
2298
2315
  }
2316
+ /**
2317
+ * Ingest the repo-root STRATEGY.md anchor as `business_fact` nodes — one per
2318
+ * non-empty section. Soft-fails when the file is absent (returns empty result
2319
+ * with no errors) so existing projects without a strategy doc keep working.
2320
+ *
2321
+ * Each emitted node carries `metadata.domain === 'strategy'` and
2322
+ * `metadata.source === 'STRATEGY.md'`, making the strategy domain
2323
+ * discoverable through the same filters as other business-knowledge nodes.
2324
+ */
2325
+ async ingestStrategy(strategyPath) {
2326
+ const start = Date.now();
2327
+ const errors = [];
2328
+ let raw;
2329
+ try {
2330
+ raw = await fs3.readFile(strategyPath, "utf-8");
2331
+ } catch {
2332
+ return emptyResult(Date.now() - start);
2333
+ }
2334
+ const parsed = parseStrategyMarkdown(raw);
2335
+ if (!parsed) {
2336
+ errors.push(`${strategyPath}: no frontmatter found`);
2337
+ return { ...emptyResult(Date.now() - start), errors };
2338
+ }
2339
+ const relPath = path4.basename(strategyPath);
2340
+ let nodesAdded = 0;
2341
+ for (const section of parsed.sections) {
2342
+ const node = this.buildStrategyNode(section, parsed.frontmatter, relPath);
2343
+ if (node === null) continue;
2344
+ this.store.addNode(node);
2345
+ nodesAdded++;
2346
+ }
2347
+ return {
2348
+ nodesAdded,
2349
+ nodesUpdated: 0,
2350
+ edgesAdded: 0,
2351
+ edgesUpdated: 0,
2352
+ errors,
2353
+ durationMs: Date.now() - start
2354
+ };
2355
+ }
2356
+ /**
2357
+ * Build a single STRATEGY.md `business_fact` node from a parsed section.
2358
+ * Returns null when the section is unknown, empty, or carries unfilled
2359
+ * template placeholder text — callers iterate and skip nulls.
2360
+ */
2361
+ buildStrategyNode(section, frontmatter, relPath) {
2362
+ if (!STRATEGY_KNOWN_SECTIONS.has(section.name)) return null;
2363
+ const body = section.body.trim();
2364
+ if (body.length === 0) return null;
2365
+ if (STRATEGY_PLACEHOLDER_RE.test(body)) return null;
2366
+ const productName = typeof frontmatter.name === "string" ? frontmatter.name : "unnamed-product";
2367
+ return {
2368
+ id: `bk:strategy:${slugifyStrategySection(section.name)}`,
2369
+ type: "business_fact",
2370
+ name: section.name,
2371
+ path: relPath,
2372
+ content: body,
2373
+ metadata: {
2374
+ domain: "strategy",
2375
+ source: "STRATEGY.md",
2376
+ section_name: section.name,
2377
+ product_name: productName,
2378
+ ...typeof frontmatter.last_updated === "string" && {
2379
+ last_updated: frontmatter.last_updated
2380
+ },
2381
+ ...typeof frontmatter.version === "number" && { version: frontmatter.version }
2382
+ }
2383
+ };
2384
+ }
2299
2385
  async createNodes(files, knowledgeDir, errors) {
2300
2386
  const entries = [];
2301
2387
  for (const filePath of files) {
@@ -2418,6 +2504,49 @@ function parseFrontmatter(raw) {
2418
2504
  body
2419
2505
  };
2420
2506
  }
2507
+ function slugifyStrategySection(name) {
2508
+ return name.toLowerCase().replace(/'/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
2509
+ }
2510
+ function parseStrategyMarkdown(raw) {
2511
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
2512
+ if (!match) return null;
2513
+ const yamlBlock = match[1];
2514
+ const body = match[2];
2515
+ const frontmatter = {};
2516
+ for (const line of yamlBlock.split(/\r?\n/)) {
2517
+ const kvMatch = line.match(/^(\w+):\s*(.+)$/);
2518
+ if (!kvMatch) continue;
2519
+ const key = kvMatch[1];
2520
+ const rawValue = kvMatch[2].trim();
2521
+ const unquoted = rawValue.replace(/^["']|["']$/g, "");
2522
+ const asNumber = Number(unquoted);
2523
+ if (unquoted !== "" && !Number.isNaN(asNumber) && /^-?\d+(?:\.\d+)?$/.test(unquoted)) {
2524
+ frontmatter[key] = asNumber;
2525
+ } else {
2526
+ frontmatter[key] = unquoted;
2527
+ }
2528
+ }
2529
+ const sections = [];
2530
+ const h2Re = /^##[ \t]+(.+?)[ \t]*$/gm;
2531
+ const matches = [];
2532
+ let m;
2533
+ while ((m = h2Re.exec(body)) !== null) {
2534
+ matches.push({
2535
+ name: (m[1] ?? "").trim(),
2536
+ headingStart: m.index,
2537
+ bodyStart: m.index + m[0].length
2538
+ });
2539
+ }
2540
+ for (let i = 0; i < matches.length; i++) {
2541
+ const current = matches[i];
2542
+ const sliceEnd = matches[i + 1]?.headingStart ?? body.length;
2543
+ sections.push({
2544
+ name: current.name,
2545
+ body: body.slice(current.bodyStart, sliceEnd).trim()
2546
+ });
2547
+ }
2548
+ return { frontmatter, sections };
2549
+ }
2421
2550
  function parseSolutionFrontmatter(raw) {
2422
2551
  const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
2423
2552
  if (!match) return null;
@@ -2449,6 +2578,12 @@ var CODE_NODE_TYPES3 = [
2449
2578
  "interface",
2450
2579
  "variable"
2451
2580
  ];
2581
+ var ARCHITECTURE_ADR_H1 = /^\s*#\s+ADR[-\s]*(\d+)\s*[:\-—]\s*(.+?)\s*$/im;
2582
+ function matchField(body, field) {
2583
+ const re = new RegExp(`\\*\\*${field}:\\*\\*\\s*(.+)`, "i");
2584
+ const m = body.match(re);
2585
+ return m ? m[1].trim() : void 0;
2586
+ }
2452
2587
  var DecisionIngestor = class {
2453
2588
  constructor(store) {
2454
2589
  this.store = store;
@@ -2505,6 +2640,71 @@ var DecisionIngestor = class {
2505
2640
  durationMs: Date.now() - start
2506
2641
  };
2507
2642
  }
2643
+ /**
2644
+ * Ingest ADRs written by `harness-architecture-advisor` from
2645
+ * `docs/architecture/<topic>/ADR-<n>.md`. These files do not carry YAML
2646
+ * frontmatter — the canonical format is:
2647
+ *
2648
+ * ```
2649
+ * # ADR-<n>: <Title>
2650
+ *
2651
+ * **Date:** <date>
2652
+ * **Status:** Accepted | Proposed | Superseded | Deprecated
2653
+ * **Deciders:** <who>
2654
+ * ```
2655
+ *
2656
+ * The first directory under `architectureDir` becomes `metadata.domain`
2657
+ * (the "topic"), so projects whose only knowledge substrate is ADRs surface
2658
+ * `architecture/<topic>` as a documented domain rather than reporting empty.
2659
+ *
2660
+ * Soft-fails when the directory is absent (the common case for projects that
2661
+ * do not use the architecture-advisor convention).
2662
+ */
2663
+ async ingestArchitecture(architectureDir) {
2664
+ const start = Date.now();
2665
+ const errors = [];
2666
+ let files;
2667
+ try {
2668
+ files = await this.findArchitectureAdrFiles(architectureDir);
2669
+ } catch {
2670
+ return emptyResult(Date.now() - start);
2671
+ }
2672
+ let nodesAdded = 0;
2673
+ let edgesAdded = 0;
2674
+ for (const filePath of files) {
2675
+ const delta = await this.processArchitectureAdrFile(filePath, architectureDir, errors);
2676
+ nodesAdded += delta.nodesAdded;
2677
+ edgesAdded += delta.edgesAdded;
2678
+ }
2679
+ return {
2680
+ nodesAdded,
2681
+ nodesUpdated: 0,
2682
+ edgesAdded,
2683
+ edgesUpdated: 0,
2684
+ errors,
2685
+ durationMs: Date.now() - start
2686
+ };
2687
+ }
2688
+ /**
2689
+ * Read, parse, and add a single architecture-advisor ADR. Returns the delta
2690
+ * the caller should fold into the aggregate counts. Errors are appended to
2691
+ * the shared `errors` array rather than thrown, matching the soft-fail
2692
+ * contract of `ingest()`.
2693
+ */
2694
+ async processArchitectureAdrFile(filePath, architectureDir, errors) {
2695
+ try {
2696
+ const raw = await fs4.readFile(filePath, "utf-8");
2697
+ const parsed = parseArchitectureAdr(raw);
2698
+ if (!parsed) return { nodesAdded: 0, edgesAdded: 0 };
2699
+ const node = buildArchitectureAdrNode(parsed, filePath, architectureDir);
2700
+ this.store.addNode(node);
2701
+ const edgesAdded = this.linkToCode(parsed.body, node.id);
2702
+ return { nodesAdded: 1, edgesAdded };
2703
+ } catch (err) {
2704
+ errors.push(`${filePath}: ${err instanceof Error ? err.message : String(err)}`);
2705
+ return { nodesAdded: 0, edgesAdded: 0 };
2706
+ }
2707
+ }
2508
2708
  parseFrontmatter(raw) {
2509
2709
  const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
2510
2710
  if (!match) return null;
@@ -2546,7 +2746,63 @@ var DecisionIngestor = class {
2546
2746
  const entries = await fs4.readdir(dir, { withFileTypes: true });
2547
2747
  return entries.filter((e) => e.isFile() && e.name.endsWith(".md") && e.name !== "README.md").map((e) => path5.join(dir, e.name));
2548
2748
  }
2749
+ /**
2750
+ * Recursively find `ADR-*.md` files under `dir`. Skips default skip dirs
2751
+ * (node_modules etc.) to keep the scan bounded on large repos.
2752
+ */
2753
+ async findArchitectureAdrFiles(dir) {
2754
+ const results = [];
2755
+ const entries = await fs4.readdir(dir, { withFileTypes: true });
2756
+ for (const entry of entries) {
2757
+ const full = path5.join(dir, entry.name);
2758
+ if (entry.isDirectory()) {
2759
+ if (DEFAULT_SKIP_DIRS.has(entry.name)) continue;
2760
+ results.push(...await this.findArchitectureAdrFiles(full));
2761
+ } else if (entry.isFile() && entry.name.endsWith(".md") && /^ADR[-_]?\d/i.test(entry.name)) {
2762
+ results.push(full);
2763
+ }
2764
+ }
2765
+ return results;
2766
+ }
2549
2767
  };
2768
+ function buildArchitectureAdrNode(parsed, filePath, architectureDir) {
2769
+ const filename = path5.basename(filePath, ".md");
2770
+ const relFromArch = path5.relative(architectureDir, filePath).replaceAll("\\", "/");
2771
+ const topic = relFromArch.includes("/") ? relFromArch.split("/")[0] : "";
2772
+ const nodeId = topic ? `decision:architecture:${topic}:${filename}` : `decision:architecture:${filename}`;
2773
+ return {
2774
+ id: nodeId,
2775
+ type: "decision",
2776
+ name: parsed.title,
2777
+ path: filePath,
2778
+ content: parsed.body.trim(),
2779
+ metadata: {
2780
+ number: parsed.number,
2781
+ ...topic && { domain: topic },
2782
+ source: "architecture",
2783
+ ...parsed.date && { date: parsed.date },
2784
+ ...parsed.status && { status: parsed.status },
2785
+ ...parsed.deciders && { deciders: parsed.deciders }
2786
+ }
2787
+ };
2788
+ }
2789
+ function parseArchitectureAdr(raw) {
2790
+ const h1 = raw.match(ARCHITECTURE_ADR_H1);
2791
+ if (!h1) return null;
2792
+ const number = h1[1];
2793
+ const title = h1[2].trim();
2794
+ const date = matchField(raw, "Date");
2795
+ const status = matchField(raw, "Status");
2796
+ const deciders = matchField(raw, "Deciders");
2797
+ return {
2798
+ number,
2799
+ title,
2800
+ body: raw,
2801
+ ...date && { date },
2802
+ ...status && { status },
2803
+ ...deciders && { deciders }
2804
+ };
2805
+ }
2550
2806
 
2551
2807
  // src/ingest/RequirementIngestor.ts
2552
2808
  import * as fs5 from "fs/promises";
@@ -2896,7 +3152,7 @@ function emptyResult2(diagramType = "unknown") {
2896
3152
  function detectDiagramType(content) {
2897
3153
  for (const line of content.split("\n")) {
2898
3154
  const trimmed = line.trim();
2899
- if (!trimmed) continue;
3155
+ if (!trimmed || trimmed.startsWith("%%")) continue;
2900
3156
  if (/^(?:graph|flowchart)\b/i.test(trimmed)) return "flowchart";
2901
3157
  if (/^sequenceDiagram\b/.test(trimmed)) return "sequence";
2902
3158
  if (/^classDiagram\b/.test(trimmed)) return "class";
@@ -5668,6 +5924,16 @@ var SNAPSHOT_NODE_TYPES = [
5668
5924
  "aesthetic_intent",
5669
5925
  "image_annotation"
5670
5926
  ];
5927
+ function emptyIngestResult() {
5928
+ return {
5929
+ nodesAdded: 0,
5930
+ nodesUpdated: 0,
5931
+ edgesAdded: 0,
5932
+ edgesUpdated: 0,
5933
+ errors: [],
5934
+ durationMs: 0
5935
+ };
5936
+ }
5671
5937
  var KnowledgePipelineRunner = class {
5672
5938
  constructor(store) {
5673
5939
  this.store = store;
@@ -5678,8 +5944,10 @@ var KnowledgePipelineRunner = class {
5678
5944
  async run(options) {
5679
5945
  this.inferenceOptions = options.inferenceOptions ?? {};
5680
5946
  const remediations = [];
5947
+ const ingestErrors = [];
5681
5948
  const preSnapshot = this.buildSnapshot(options.domain);
5682
5949
  const extraction = await this.extract(options);
5950
+ ingestErrors.push(...extraction.errors);
5683
5951
  const postSnapshot = this.buildSnapshot(options.domain);
5684
5952
  let driftResult = this.reconcile(preSnapshot, postSnapshot);
5685
5953
  const contradictions = new ContradictionDetector().detect(this.store);
@@ -5692,7 +5960,8 @@ var KnowledgePipelineRunner = class {
5692
5960
  options,
5693
5961
  driftResult,
5694
5962
  gapReport,
5695
- remediations
5963
+ remediations,
5964
+ ingestErrors
5696
5965
  );
5697
5966
  iterations = loopResult.iterations;
5698
5967
  materialization = loopResult.materialization;
@@ -5706,7 +5975,8 @@ var KnowledgePipelineRunner = class {
5706
5975
  return this.buildResult(
5707
5976
  driftResult,
5708
5977
  iterations,
5709
- extraction,
5978
+ extraction.counts,
5979
+ ingestErrors,
5710
5980
  gapReport,
5711
5981
  remediations,
5712
5982
  contradictions,
@@ -5715,7 +5985,7 @@ var KnowledgePipelineRunner = class {
5715
5985
  );
5716
5986
  }
5717
5987
  /** Run the remediation convergence loop; returns iteration count and accumulated materialization. */
5718
- async runRemediationLoop(options, driftResult, gapReport, remediations) {
5988
+ async runRemediationLoop(options, driftResult, gapReport, remediations, ingestErrors) {
5719
5989
  const maxIterations = options.maxIterations ?? 5;
5720
5990
  let iterations = 1;
5721
5991
  let currentDrift = driftResult;
@@ -5736,7 +6006,8 @@ var KnowledgePipelineRunner = class {
5736
6006
  }
5737
6007
  }
5738
6008
  const preSnapshot = this.buildSnapshot(options.domain);
5739
- await this.extract(options);
6009
+ const reExtract = await this.extract(options);
6010
+ ingestErrors.push(...reExtract.errors);
5740
6011
  const postSnapshot = this.buildSnapshot(options.domain);
5741
6012
  currentDrift = this.reconcile(preSnapshot, postSnapshot);
5742
6013
  currentGapReport = await this.detect(options);
@@ -5751,13 +6022,14 @@ var KnowledgePipelineRunner = class {
5751
6022
  };
5752
6023
  }
5753
6024
  /** Assemble the final pipeline result. */
5754
- buildResult(driftResult, iterations, extraction, gaps, remediations, contradictions, coverage, materialization) {
6025
+ buildResult(driftResult, iterations, extraction, errors, gaps, remediations, contradictions, coverage, materialization) {
5755
6026
  return {
5756
6027
  verdict: this.computeVerdict(driftResult),
5757
6028
  driftScore: driftResult.driftScore,
5758
6029
  iterations,
5759
6030
  findings: driftResult.summary,
5760
6031
  extraction,
6032
+ errors: Array.from(new Set(errors)),
5761
6033
  gaps,
5762
6034
  remediations,
5763
6035
  contradictions,
@@ -5788,58 +6060,60 @@ var KnowledgePipelineRunner = class {
5788
6060
  try {
5789
6061
  bkResult = await bkIngestor.ingest(knowledgeDir);
5790
6062
  } catch {
5791
- bkResult = {
5792
- nodesAdded: 0,
5793
- nodesUpdated: 0,
5794
- edgesAdded: 0,
5795
- edgesUpdated: 0,
5796
- errors: [],
5797
- durationMs: 0
5798
- };
6063
+ bkResult = emptyIngestResult();
5799
6064
  }
5800
6065
  const solutionsDir = path12.join(options.projectDir, "docs", "solutions");
5801
6066
  let solutionsResult;
5802
6067
  try {
5803
6068
  solutionsResult = await bkIngestor.ingestSolutions(solutionsDir);
5804
6069
  } catch {
5805
- solutionsResult = {
5806
- nodesAdded: 0,
5807
- nodesUpdated: 0,
5808
- edgesAdded: 0,
5809
- edgesUpdated: 0,
5810
- errors: [],
5811
- durationMs: 0
5812
- };
6070
+ solutionsResult = emptyIngestResult();
6071
+ }
6072
+ const strategyPath = path12.join(options.projectDir, "STRATEGY.md");
6073
+ let strategyResult;
6074
+ try {
6075
+ strategyResult = await bkIngestor.ingestStrategy(strategyPath);
6076
+ } catch {
6077
+ strategyResult = emptyIngestResult();
5813
6078
  }
5814
6079
  bkResult = {
5815
6080
  ...bkResult,
5816
- nodesAdded: bkResult.nodesAdded + solutionsResult.nodesAdded,
5817
- errors: [...bkResult.errors, ...solutionsResult.errors]
6081
+ nodesAdded: bkResult.nodesAdded + solutionsResult.nodesAdded + strategyResult.nodesAdded,
6082
+ errors: [...bkResult.errors, ...solutionsResult.errors, ...strategyResult.errors]
5818
6083
  };
5819
6084
  const decisionsDir = path12.join(options.projectDir, "docs", "knowledge", "decisions");
6085
+ const architectureDir = path12.join(options.projectDir, "docs", "architecture");
5820
6086
  const decisionIngestor = new DecisionIngestor(this.store);
5821
6087
  let decisionResult;
5822
6088
  try {
5823
6089
  decisionResult = await decisionIngestor.ingest(decisionsDir);
5824
6090
  } catch {
5825
- decisionResult = {
5826
- nodesAdded: 0,
5827
- nodesUpdated: 0,
5828
- edgesAdded: 0,
5829
- edgesUpdated: 0,
5830
- errors: [],
5831
- durationMs: 0
5832
- };
6091
+ decisionResult = emptyIngestResult();
5833
6092
  }
6093
+ let architectureResult;
6094
+ try {
6095
+ architectureResult = await decisionIngestor.ingestArchitecture(architectureDir);
6096
+ } catch {
6097
+ architectureResult = emptyIngestResult();
6098
+ }
6099
+ decisionResult = {
6100
+ ...decisionResult,
6101
+ nodesAdded: decisionResult.nodesAdded + architectureResult.nodesAdded,
6102
+ edgesAdded: decisionResult.edgesAdded + architectureResult.edgesAdded,
6103
+ errors: [...decisionResult.errors, ...architectureResult.errors]
6104
+ };
5834
6105
  const linker = new KnowledgeLinker(this.store, extractedDir);
5835
6106
  const linkResult = await linker.link();
5836
6107
  return {
5837
- codeSignals: extractionResult.nodesAdded,
5838
- diagrams: diagramResult.nodesAdded,
5839
- linkerFacts: linkResult.factsCreated,
5840
- businessKnowledge: bkResult.nodesAdded,
5841
- decisions: decisionResult.nodesAdded,
5842
- images: imageCount
6108
+ counts: {
6109
+ codeSignals: extractionResult.nodesAdded,
6110
+ diagrams: diagramResult.nodesAdded,
6111
+ linkerFacts: linkResult.factsCreated,
6112
+ businessKnowledge: bkResult.nodesAdded,
6113
+ decisions: decisionResult.nodesAdded,
6114
+ images: imageCount
6115
+ },
6116
+ errors: [...bkResult.errors, ...decisionResult.errors]
5843
6117
  };
5844
6118
  }
5845
6119
  // ── Phase 2: RECONCILE ────────────────────────────────────────────────────
@@ -9045,7 +9319,7 @@ function queryTraceability(store, options) {
9045
9319
 
9046
9320
  // src/constraints/GraphConstraintAdapter.ts
9047
9321
  import { minimatch as minimatch2 } from "minimatch";
9048
- import { relative as relative6 } from "path";
9322
+ import { relative as relative7 } from "path";
9049
9323
  var GraphConstraintAdapter = class {
9050
9324
  constructor(store) {
9051
9325
  this.store = store;
@@ -9074,8 +9348,8 @@ var GraphConstraintAdapter = class {
9074
9348
  const { edges } = this.computeDependencyGraph();
9075
9349
  const violations = [];
9076
9350
  for (const edge of edges) {
9077
- const fromRelative = relative6(rootDir, edge.from).replaceAll("\\", "/");
9078
- const toRelative = relative6(rootDir, edge.to).replaceAll("\\", "/");
9351
+ const fromRelative = relative7(rootDir, edge.from).replaceAll("\\", "/");
9352
+ const toRelative = relative7(rootDir, edge.to).replaceAll("\\", "/");
9079
9353
  const fromLayer = this.resolveLayer(fromRelative, layers);
9080
9354
  const toLayer = this.resolveLayer(toRelative, layers);
9081
9355
  if (!fromLayer || !toLayer) continue;
@@ -9268,6 +9542,16 @@ var DesignIngestor = class {
9268
9542
  };
9269
9543
 
9270
9544
  // src/constraints/DesignConstraintAdapter.ts
9545
+ var CODE_PREFIX_LABELS = {
9546
+ "ANAT-D": "Component anatomy (definition)",
9547
+ "ANAT-P": "Component anatomy (pattern presence)",
9548
+ "ANAT-U": "Component anatomy (usage)",
9549
+ "CRAFT-C": "Design craft (critique)",
9550
+ "CRAFT-P": "Design craft (polish)",
9551
+ "CRAFT-B": "Design craft (benchmark)",
9552
+ "DESIGN-": "Design constraint (legacy)",
9553
+ "A11Y-": "Accessibility"
9554
+ };
9271
9555
  var DesignConstraintAdapter = class {
9272
9556
  constructor(store) {
9273
9557
  this.store = store;
@@ -9349,6 +9633,76 @@ var DesignConstraintAdapter = class {
9349
9633
  return "error";
9350
9634
  }
9351
9635
  }
9636
+ /**
9637
+ * Record externally-computed craft findings (audit-component-anatomy,
9638
+ * harness-design-craft, etc.) as graph state. Idempotent — re-running on
9639
+ * the same findings produces no duplicate nodes or edges thanks to
9640
+ * GraphStore's keyed merge semantics.
9641
+ *
9642
+ * Each finding becomes:
9643
+ * • a `design_constraint` node keyed by finding code (created lazily
9644
+ * if absent; metadata merged on re-record so the most recent message
9645
+ * / severity wins)
9646
+ * • a `violates_design` edge from the source file (id = file path) to
9647
+ * the constraint node, with per-finding metadata (line, severity,
9648
+ * message, evidence, runId)
9649
+ *
9650
+ * The file node is NOT created here — it's assumed to already exist in
9651
+ * the graph from a prior ingest. If it does not, the edge will still be
9652
+ * created (graph stores edges by key, not by referential integrity) and
9653
+ * a subsequent ingest will populate the file node.
9654
+ */
9655
+ recordFindings(findings) {
9656
+ let constraintsAdded = 0;
9657
+ let edgesAdded = 0;
9658
+ const seenConstraintIds = /* @__PURE__ */ new Set();
9659
+ for (const finding of findings) {
9660
+ const constraintId = `design_constraint:${finding.code}`;
9661
+ if (!seenConstraintIds.has(finding.code)) {
9662
+ const existing = this.store.getNode(constraintId);
9663
+ if (!existing) constraintsAdded += 1;
9664
+ this.store.addNode({
9665
+ id: constraintId,
9666
+ type: "design_constraint",
9667
+ name: finding.code,
9668
+ metadata: {
9669
+ code: finding.code,
9670
+ label: this.labelForCode(finding.code),
9671
+ mostRecentMessage: finding.message,
9672
+ mostRecentSeverity: finding.severity
9673
+ }
9674
+ });
9675
+ seenConstraintIds.add(finding.code);
9676
+ }
9677
+ const fileId = finding.file;
9678
+ const edgeMetadata = {
9679
+ message: finding.message,
9680
+ severity: finding.severity
9681
+ };
9682
+ if (finding.line !== void 0) edgeMetadata.line = finding.line;
9683
+ if (finding.evidence !== void 0) edgeMetadata.evidence = finding.evidence;
9684
+ if (finding.runId !== void 0) edgeMetadata.runId = finding.runId;
9685
+ const before = this.store.getEdges({
9686
+ from: fileId,
9687
+ to: constraintId,
9688
+ type: "violates_design"
9689
+ });
9690
+ this.store.addEdge({
9691
+ from: fileId,
9692
+ to: constraintId,
9693
+ type: "violates_design",
9694
+ metadata: edgeMetadata
9695
+ });
9696
+ if (before.length === 0) edgesAdded += 1;
9697
+ }
9698
+ return { constraintsAdded, edgesAdded };
9699
+ }
9700
+ labelForCode(code) {
9701
+ for (const prefix of Object.keys(CODE_PREFIX_LABELS)) {
9702
+ if (code.startsWith(prefix)) return CODE_PREFIX_LABELS[prefix];
9703
+ }
9704
+ return "Design constraint";
9705
+ }
9352
9706
  };
9353
9707
 
9354
9708
  // src/feedback/GraphFeedbackAdapter.ts
@@ -9876,7 +10230,7 @@ var ConflictPredictor = class {
9876
10230
  };
9877
10231
 
9878
10232
  // src/index.ts
9879
- var VERSION = "0.6.0";
10233
+ var VERSION = "0.9.0";
9880
10234
  export {
9881
10235
  ALL_EXTRACTORS,
9882
10236
  ApiPathExtractor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-engineering/graph",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "license": "MIT",
5
5
  "description": "Knowledge graph for context assembly in Harness Engineering",
6
6
  "main": "./dist/index.js",
@@ -19,7 +19,8 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "minimatch": "^10.2.5",
22
- "zod": "^3.25.76"
22
+ "zod": "^3.25.76",
23
+ "@harness-engineering/types": "0.16.0"
23
24
  },
24
25
  "optionalDependencies": {
25
26
  "tree-sitter": "^0.22.4",