@harness-engineering/graph 0.10.0 → 0.11.1
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 +59 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +312 -38
- package/dist/index.mjs +315 -41
- package/package.json +3 -2
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
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
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
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
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
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
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
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
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
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
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
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
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
|
|
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 =
|
|
9078
|
-
const toRelative =
|
|
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.
|
|
3
|
+
"version": "0.11.1",
|
|
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.1"
|
|
23
24
|
},
|
|
24
25
|
"optionalDependencies": {
|
|
25
26
|
"tree-sitter": "^0.22.4",
|