@harness-engineering/graph 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +107 -2
- package/dist/index.d.ts +107 -2
- package/dist/index.js +393 -39
- package/dist/index.mjs +396 -42
- package/package.json +3 -2
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
|
-
nodesAdded: 0,
|
|
5827
|
-
nodesUpdated: 0,
|
|
5828
|
-
edgesAdded: 0,
|
|
5829
|
-
edgesUpdated: 0,
|
|
5830
|
-
errors: [],
|
|
5831
|
-
durationMs: 0
|
|
5832
|
-
};
|
|
6091
|
+
decisionResult = emptyIngestResult();
|
|
5833
6092
|
}
|
|
6093
|
+
let architectureResult;
|
|
6094
|
+
try {
|
|
6095
|
+
architectureResult = await decisionIngestor.ingestArchitecture(architectureDir);
|
|
6096
|
+
} catch {
|
|
6097
|
+
architectureResult = emptyIngestResult();
|
|
6098
|
+
}
|
|
6099
|
+
decisionResult = {
|
|
6100
|
+
...decisionResult,
|
|
6101
|
+
nodesAdded: decisionResult.nodesAdded + architectureResult.nodesAdded,
|
|
6102
|
+
edgesAdded: decisionResult.edgesAdded + architectureResult.edgesAdded,
|
|
6103
|
+
errors: [...decisionResult.errors, ...architectureResult.errors]
|
|
6104
|
+
};
|
|
5834
6105
|
const linker = new KnowledgeLinker(this.store, extractedDir);
|
|
5835
6106
|
const linkResult = await linker.link();
|
|
5836
6107
|
return {
|
|
5837
|
-
|
|
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;
|
|
@@ -9268,6 +9542,16 @@ var DesignIngestor = class {
|
|
|
9268
9542
|
};
|
|
9269
9543
|
|
|
9270
9544
|
// src/constraints/DesignConstraintAdapter.ts
|
|
9545
|
+
var CODE_PREFIX_LABELS = {
|
|
9546
|
+
"ANAT-D": "Component anatomy (definition)",
|
|
9547
|
+
"ANAT-P": "Component anatomy (pattern presence)",
|
|
9548
|
+
"ANAT-U": "Component anatomy (usage)",
|
|
9549
|
+
"CRAFT-C": "Design craft (critique)",
|
|
9550
|
+
"CRAFT-P": "Design craft (polish)",
|
|
9551
|
+
"CRAFT-B": "Design craft (benchmark)",
|
|
9552
|
+
"DESIGN-": "Design constraint (legacy)",
|
|
9553
|
+
"A11Y-": "Accessibility"
|
|
9554
|
+
};
|
|
9271
9555
|
var DesignConstraintAdapter = class {
|
|
9272
9556
|
constructor(store) {
|
|
9273
9557
|
this.store = store;
|
|
@@ -9349,6 +9633,76 @@ var DesignConstraintAdapter = class {
|
|
|
9349
9633
|
return "error";
|
|
9350
9634
|
}
|
|
9351
9635
|
}
|
|
9636
|
+
/**
|
|
9637
|
+
* Record externally-computed craft findings (audit-component-anatomy,
|
|
9638
|
+
* harness-design-craft, etc.) as graph state. Idempotent — re-running on
|
|
9639
|
+
* the same findings produces no duplicate nodes or edges thanks to
|
|
9640
|
+
* GraphStore's keyed merge semantics.
|
|
9641
|
+
*
|
|
9642
|
+
* Each finding becomes:
|
|
9643
|
+
* • a `design_constraint` node keyed by finding code (created lazily
|
|
9644
|
+
* if absent; metadata merged on re-record so the most recent message
|
|
9645
|
+
* / severity wins)
|
|
9646
|
+
* • a `violates_design` edge from the source file (id = file path) to
|
|
9647
|
+
* the constraint node, with per-finding metadata (line, severity,
|
|
9648
|
+
* message, evidence, runId)
|
|
9649
|
+
*
|
|
9650
|
+
* The file node is NOT created here — it's assumed to already exist in
|
|
9651
|
+
* the graph from a prior ingest. If it does not, the edge will still be
|
|
9652
|
+
* created (graph stores edges by key, not by referential integrity) and
|
|
9653
|
+
* a subsequent ingest will populate the file node.
|
|
9654
|
+
*/
|
|
9655
|
+
recordFindings(findings) {
|
|
9656
|
+
let constraintsAdded = 0;
|
|
9657
|
+
let edgesAdded = 0;
|
|
9658
|
+
const seenConstraintIds = /* @__PURE__ */ new Set();
|
|
9659
|
+
for (const finding of findings) {
|
|
9660
|
+
const constraintId = `design_constraint:${finding.code}`;
|
|
9661
|
+
if (!seenConstraintIds.has(finding.code)) {
|
|
9662
|
+
const existing = this.store.getNode(constraintId);
|
|
9663
|
+
if (!existing) constraintsAdded += 1;
|
|
9664
|
+
this.store.addNode({
|
|
9665
|
+
id: constraintId,
|
|
9666
|
+
type: "design_constraint",
|
|
9667
|
+
name: finding.code,
|
|
9668
|
+
metadata: {
|
|
9669
|
+
code: finding.code,
|
|
9670
|
+
label: this.labelForCode(finding.code),
|
|
9671
|
+
mostRecentMessage: finding.message,
|
|
9672
|
+
mostRecentSeverity: finding.severity
|
|
9673
|
+
}
|
|
9674
|
+
});
|
|
9675
|
+
seenConstraintIds.add(finding.code);
|
|
9676
|
+
}
|
|
9677
|
+
const fileId = finding.file;
|
|
9678
|
+
const edgeMetadata = {
|
|
9679
|
+
message: finding.message,
|
|
9680
|
+
severity: finding.severity
|
|
9681
|
+
};
|
|
9682
|
+
if (finding.line !== void 0) edgeMetadata.line = finding.line;
|
|
9683
|
+
if (finding.evidence !== void 0) edgeMetadata.evidence = finding.evidence;
|
|
9684
|
+
if (finding.runId !== void 0) edgeMetadata.runId = finding.runId;
|
|
9685
|
+
const before = this.store.getEdges({
|
|
9686
|
+
from: fileId,
|
|
9687
|
+
to: constraintId,
|
|
9688
|
+
type: "violates_design"
|
|
9689
|
+
});
|
|
9690
|
+
this.store.addEdge({
|
|
9691
|
+
from: fileId,
|
|
9692
|
+
to: constraintId,
|
|
9693
|
+
type: "violates_design",
|
|
9694
|
+
metadata: edgeMetadata
|
|
9695
|
+
});
|
|
9696
|
+
if (before.length === 0) edgesAdded += 1;
|
|
9697
|
+
}
|
|
9698
|
+
return { constraintsAdded, edgesAdded };
|
|
9699
|
+
}
|
|
9700
|
+
labelForCode(code) {
|
|
9701
|
+
for (const prefix of Object.keys(CODE_PREFIX_LABELS)) {
|
|
9702
|
+
if (code.startsWith(prefix)) return CODE_PREFIX_LABELS[prefix];
|
|
9703
|
+
}
|
|
9704
|
+
return "Design constraint";
|
|
9705
|
+
}
|
|
9352
9706
|
};
|
|
9353
9707
|
|
|
9354
9708
|
// src/feedback/GraphFeedbackAdapter.ts
|
|
@@ -9876,7 +10230,7 @@ var ConflictPredictor = class {
|
|
|
9876
10230
|
};
|
|
9877
10231
|
|
|
9878
10232
|
// src/index.ts
|
|
9879
|
-
var VERSION = "0.
|
|
10233
|
+
var VERSION = "0.9.0";
|
|
9880
10234
|
export {
|
|
9881
10235
|
ALL_EXTRACTORS,
|
|
9882
10236
|
ApiPathExtractor,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harness-engineering/graph",
|
|
3
|
-
"version": "0.
|
|
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",
|