@harness-engineering/graph 0.10.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.d.mts CHANGED
@@ -568,6 +568,22 @@ declare class BusinessKnowledgeIngestor {
568
568
  constructor(store: GraphStore);
569
569
  ingest(knowledgeDir: string): Promise<IngestResult>;
570
570
  ingestSolutions(solutionsDir: string): Promise<IngestResult>;
571
+ /**
572
+ * Ingest the repo-root STRATEGY.md anchor as `business_fact` nodes — one per
573
+ * non-empty section. Soft-fails when the file is absent (returns empty result
574
+ * with no errors) so existing projects without a strategy doc keep working.
575
+ *
576
+ * Each emitted node carries `metadata.domain === 'strategy'` and
577
+ * `metadata.source === 'STRATEGY.md'`, making the strategy domain
578
+ * discoverable through the same filters as other business-knowledge nodes.
579
+ */
580
+ ingestStrategy(strategyPath: string): Promise<IngestResult>;
581
+ /**
582
+ * Build a single STRATEGY.md `business_fact` node from a parsed section.
583
+ * Returns null when the section is unknown, empty, or carries unfilled
584
+ * template placeholder text — callers iterate and skip nulls.
585
+ */
586
+ private buildStrategyNode;
571
587
  private createNodes;
572
588
  private parseAndAddNode;
573
589
  private createEdges;
@@ -582,14 +598,51 @@ declare class BusinessKnowledgeIngestor {
582
598
  * Parses YAML frontmatter with fields: number, title, date, status, tier,
583
599
  * source, supersedes. Creates `decision` type graph nodes with `decided`
584
600
  * edges to code nodes mentioned in the body.
601
+ *
602
+ * Also supports markdown-style ADRs written by `harness-architecture-advisor`
603
+ * at `docs/architecture/<topic>/ADR-<n>.md` (no YAML frontmatter — fields are
604
+ * `**Date:**`, `**Status:**`, `**Deciders:**` lines). See `ingestArchitecture`.
585
605
  */
586
606
  declare class DecisionIngestor {
587
607
  private readonly store;
588
608
  constructor(store: GraphStore);
589
609
  ingest(decisionsDir: string): Promise<IngestResult>;
610
+ /**
611
+ * Ingest ADRs written by `harness-architecture-advisor` from
612
+ * `docs/architecture/<topic>/ADR-<n>.md`. These files do not carry YAML
613
+ * frontmatter — the canonical format is:
614
+ *
615
+ * ```
616
+ * # ADR-<n>: <Title>
617
+ *
618
+ * **Date:** <date>
619
+ * **Status:** Accepted | Proposed | Superseded | Deprecated
620
+ * **Deciders:** <who>
621
+ * ```
622
+ *
623
+ * The first directory under `architectureDir` becomes `metadata.domain`
624
+ * (the "topic"), so projects whose only knowledge substrate is ADRs surface
625
+ * `architecture/<topic>` as a documented domain rather than reporting empty.
626
+ *
627
+ * Soft-fails when the directory is absent (the common case for projects that
628
+ * do not use the architecture-advisor convention).
629
+ */
630
+ ingestArchitecture(architectureDir: string): Promise<IngestResult>;
631
+ /**
632
+ * Read, parse, and add a single architecture-advisor ADR. Returns the delta
633
+ * the caller should fold into the aggregate counts. Errors are appended to
634
+ * the shared `errors` array rather than thrown, matching the soft-fail
635
+ * contract of `ingest()`.
636
+ */
637
+ private processArchitectureAdrFile;
590
638
  private parseFrontmatter;
591
639
  private linkToCode;
592
640
  private findDecisionFiles;
641
+ /**
642
+ * Recursively find `ADR-*.md` files under `dir`. Skips default skip dirs
643
+ * (node_modules etc.) to keep the scan bounded on large repos.
644
+ */
645
+ private findArchitectureAdrFiles;
593
646
  }
594
647
 
595
648
  declare class RequirementIngestor {
@@ -927,6 +980,12 @@ interface KnowledgePipelineResult {
927
980
  readonly iterations: number;
928
981
  readonly findings: DriftResult['summary'];
929
982
  readonly extraction: ExtractionCounts;
983
+ /**
984
+ * Aggregated parse/skip errors from every ingestor invoked during phase 1.
985
+ * Surfaces frontmatter validation, malformed-markdown, and per-file read
986
+ * failures that would otherwise be silently dropped (issue #504 §1).
987
+ */
988
+ readonly errors: readonly string[];
930
989
  readonly gaps: GapReport;
931
990
  readonly remediations: readonly string[];
932
991
  readonly contradictions: ContradictionResult;
package/dist/index.d.ts CHANGED
@@ -568,6 +568,22 @@ declare class BusinessKnowledgeIngestor {
568
568
  constructor(store: GraphStore);
569
569
  ingest(knowledgeDir: string): Promise<IngestResult>;
570
570
  ingestSolutions(solutionsDir: string): Promise<IngestResult>;
571
+ /**
572
+ * Ingest the repo-root STRATEGY.md anchor as `business_fact` nodes — one per
573
+ * non-empty section. Soft-fails when the file is absent (returns empty result
574
+ * with no errors) so existing projects without a strategy doc keep working.
575
+ *
576
+ * Each emitted node carries `metadata.domain === 'strategy'` and
577
+ * `metadata.source === 'STRATEGY.md'`, making the strategy domain
578
+ * discoverable through the same filters as other business-knowledge nodes.
579
+ */
580
+ ingestStrategy(strategyPath: string): Promise<IngestResult>;
581
+ /**
582
+ * Build a single STRATEGY.md `business_fact` node from a parsed section.
583
+ * Returns null when the section is unknown, empty, or carries unfilled
584
+ * template placeholder text — callers iterate and skip nulls.
585
+ */
586
+ private buildStrategyNode;
571
587
  private createNodes;
572
588
  private parseAndAddNode;
573
589
  private createEdges;
@@ -582,14 +598,51 @@ declare class BusinessKnowledgeIngestor {
582
598
  * Parses YAML frontmatter with fields: number, title, date, status, tier,
583
599
  * source, supersedes. Creates `decision` type graph nodes with `decided`
584
600
  * edges to code nodes mentioned in the body.
601
+ *
602
+ * Also supports markdown-style ADRs written by `harness-architecture-advisor`
603
+ * at `docs/architecture/<topic>/ADR-<n>.md` (no YAML frontmatter — fields are
604
+ * `**Date:**`, `**Status:**`, `**Deciders:**` lines). See `ingestArchitecture`.
585
605
  */
586
606
  declare class DecisionIngestor {
587
607
  private readonly store;
588
608
  constructor(store: GraphStore);
589
609
  ingest(decisionsDir: string): Promise<IngestResult>;
610
+ /**
611
+ * Ingest ADRs written by `harness-architecture-advisor` from
612
+ * `docs/architecture/<topic>/ADR-<n>.md`. These files do not carry YAML
613
+ * frontmatter — the canonical format is:
614
+ *
615
+ * ```
616
+ * # ADR-<n>: <Title>
617
+ *
618
+ * **Date:** <date>
619
+ * **Status:** Accepted | Proposed | Superseded | Deprecated
620
+ * **Deciders:** <who>
621
+ * ```
622
+ *
623
+ * The first directory under `architectureDir` becomes `metadata.domain`
624
+ * (the "topic"), so projects whose only knowledge substrate is ADRs surface
625
+ * `architecture/<topic>` as a documented domain rather than reporting empty.
626
+ *
627
+ * Soft-fails when the directory is absent (the common case for projects that
628
+ * do not use the architecture-advisor convention).
629
+ */
630
+ ingestArchitecture(architectureDir: string): Promise<IngestResult>;
631
+ /**
632
+ * Read, parse, and add a single architecture-advisor ADR. Returns the delta
633
+ * the caller should fold into the aggregate counts. Errors are appended to
634
+ * the shared `errors` array rather than thrown, matching the soft-fail
635
+ * contract of `ingest()`.
636
+ */
637
+ private processArchitectureAdrFile;
590
638
  private parseFrontmatter;
591
639
  private linkToCode;
592
640
  private findDecisionFiles;
641
+ /**
642
+ * Recursively find `ADR-*.md` files under `dir`. Skips default skip dirs
643
+ * (node_modules etc.) to keep the scan bounded on large repos.
644
+ */
645
+ private findArchitectureAdrFiles;
593
646
  }
594
647
 
595
648
  declare class RequirementIngestor {
@@ -927,6 +980,12 @@ interface KnowledgePipelineResult {
927
980
  readonly iterations: number;
928
981
  readonly findings: DriftResult['summary'];
929
982
  readonly extraction: ExtractionCounts;
983
+ /**
984
+ * Aggregated parse/skip errors from every ingestor invoked during phase 1.
985
+ * Surfaces frontmatter validation, malformed-markdown, and per-file read
986
+ * failures that would otherwise be silently dropped (issue #504 §1).
987
+ */
988
+ readonly errors: readonly string[];
930
989
  readonly gaps: GapReport;
931
990
  readonly remediations: readonly string[];
932
991
  readonly contradictions: ContradictionResult;
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();
6207
+ }
6208
+ let architectureResult;
6209
+ try {
6210
+ architectureResult = await decisionIngestor.ingestArchitecture(architectureDir);
6211
+ } catch {
6212
+ architectureResult = emptyIngestResult();
5948
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 ────────────────────────────────────────────────────
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();
6092
+ }
6093
+ let architectureResult;
6094
+ try {
6095
+ architectureResult = await decisionIngestor.ingestArchitecture(architectureDir);
6096
+ } catch {
6097
+ architectureResult = emptyIngestResult();
5833
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harness-engineering/graph",
3
- "version": "0.10.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",