@decantr/core 3.4.0 → 3.6.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/README.md +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +88 -37
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -25,11 +25,11 @@ npm install @decantr/core
|
|
|
25
25
|
- execution-pack schema URLs
|
|
26
26
|
- markdown rendering for compiled packs
|
|
27
27
|
- IR and pipeline helpers used by higher-level Decantr surfaces
|
|
28
|
-
-
|
|
28
|
+
- Decantr 3 typed graph types, graph constants, Essence/IR-to-graph snapshot derivation, temporal snapshot/diff shapes, source import/reference edges, contract capsule derivation with a bounded source-artifact path index, deterministic hybrid route/impact ranking, behavior-obligation LocalRule projection support through existing graph shapes, evidence/proof ingestion, test-coverage hint edges, node impact context extraction, and an in-memory `GraphStore` adapter
|
|
29
29
|
|
|
30
30
|
In the current workflow architecture, `@decantr/core` owns the canonical adapter labels used by compiled packs, while runnable greenfield bootstrap adapters are resolved in the CLI on top of those labels.
|
|
31
31
|
|
|
32
|
-
The graph exports
|
|
32
|
+
The graph exports establish the storage boundary, typed schema shape, temporal snapshot/diff shape, payload-filterable node queries, hybrid route-context ranking, hybrid node/source impact traversal, and provider-neutral contract capsule shape for CLI, MCP, verifier, Studio, and CI integration. Ranking blends deterministic weighted traversal with local personalized PageRank and optional task-text boosts, so central graph nodes and task-relevant nodes both surface without introducing a graph database dependency. Behavior obligations remain app-owned local law in `.decantr/local-patterns.json`; higher-level packages project accepted obligations into existing `LocalRule` graph nodes with `payload.kind = "behavior-obligation"` instead of adding a new graph node type. Evidence Bundles, runtime probes, visual manifests, baseline diffs, repair plans, and proof reports are ingested through existing graph node/edge shapes where possible, and `TEST_COVERS_NODE` edges act as verification hints rather than proof of production UI behavior. The capsule keeps the contract cache key stable while listing bounded SourceArtifact paths agents can use for file-impact follow-up queries. The core package remains pure library code: filesystem graph persistence belongs in higher-level packages such as the CLI.
|
|
33
33
|
|
|
34
34
|
## FAQ
|
|
35
35
|
|
package/dist/index.d.ts
CHANGED
|
@@ -371,7 +371,7 @@ interface GraphRouteContext {
|
|
|
371
371
|
sourceHash: string;
|
|
372
372
|
routeNode: GraphNode;
|
|
373
373
|
ranking: {
|
|
374
|
-
method: 'weighted_traversal' | 'weighted_traversal_with_task_boost';
|
|
374
|
+
method: 'weighted_traversal' | 'weighted_traversal_with_task_boost' | 'hybrid_weighted_pagerank' | 'hybrid_weighted_pagerank_with_task_boost';
|
|
375
375
|
seed: string;
|
|
376
376
|
task_keywords: string[];
|
|
377
377
|
};
|
|
@@ -420,7 +420,7 @@ interface GraphImpactContext {
|
|
|
420
420
|
seedNodes: GraphNode[];
|
|
421
421
|
missingNodeIds: string[];
|
|
422
422
|
ranking: {
|
|
423
|
-
method: 'impact_traversal' | 'impact_traversal_with_task_boost';
|
|
423
|
+
method: 'impact_traversal' | 'impact_traversal_with_task_boost' | 'hybrid_impact_pagerank' | 'hybrid_impact_pagerank_with_task_boost';
|
|
424
424
|
seed: string[];
|
|
425
425
|
task_keywords: string[];
|
|
426
426
|
};
|
package/dist/index.js
CHANGED
|
@@ -415,7 +415,86 @@ function buildContractCapsuleFromSnapshot(snapshot, options = {}) {
|
|
|
415
415
|
open_findings: openFindings
|
|
416
416
|
};
|
|
417
417
|
}
|
|
418
|
-
|
|
418
|
+
var PAGERANK_DAMPING = 0.82;
|
|
419
|
+
var PAGERANK_ITERATIONS = 16;
|
|
420
|
+
var RELATION_RANK_WEIGHTS = {
|
|
421
|
+
PAGE_ROUTED_AT_ROUTE: 1,
|
|
422
|
+
PAGE_USES_SHELL: 0.9,
|
|
423
|
+
PAGE_COMPOSES_PATTERN: 0.88,
|
|
424
|
+
PATTERN_NEEDS_COMPONENT: 0.82,
|
|
425
|
+
COMPONENT_STYLED_WITH_TOKEN: 0.62,
|
|
426
|
+
LOCAL_RULE_APPLIES_TO: 0.72,
|
|
427
|
+
STYLE_BRIDGE_MAPS_TO: 0.7,
|
|
428
|
+
FINDING_ANCHORED_AT: 0.78,
|
|
429
|
+
EVIDENCE_SUPPORTS_FINDING: 0.58,
|
|
430
|
+
EVIDENCE_CAPTURED_FOR: 0.56,
|
|
431
|
+
REPAIR_FIXES_FINDING: 0.54,
|
|
432
|
+
NODE_DERIVED_FROM_SOURCE: 0.5,
|
|
433
|
+
SOURCE_IMPORTS_SOURCE: 0.42,
|
|
434
|
+
TEST_COVERS_NODE: 0.36
|
|
435
|
+
};
|
|
436
|
+
function personalizedPageRank(nodes, edges, seedIds) {
|
|
437
|
+
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
438
|
+
const seeds = [...seedIds].filter((id) => nodeIds.has(id)).sort();
|
|
439
|
+
const activeSeeds = seeds.length > 0 ? seeds : sortGraphNodes(nodes).slice(0, 1).map((node) => node.id);
|
|
440
|
+
const teleport = /* @__PURE__ */ new Map();
|
|
441
|
+
for (const node of nodes) teleport.set(node.id, 0);
|
|
442
|
+
for (const seed of activeSeeds) teleport.set(seed, 1 / activeSeeds.length);
|
|
443
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
444
|
+
for (const node of nodes) adjacency.set(node.id, []);
|
|
445
|
+
for (const edge of edges) {
|
|
446
|
+
if (!nodeIds.has(edge.src) || !nodeIds.has(edge.dst)) continue;
|
|
447
|
+
const weight = RELATION_RANK_WEIGHTS[edge.relation] ?? 0.32;
|
|
448
|
+
adjacency.get(edge.src)?.push({ id: edge.dst, weight });
|
|
449
|
+
adjacency.get(edge.dst)?.push({ id: edge.src, weight: weight * 0.82 });
|
|
450
|
+
}
|
|
451
|
+
let rank = new Map(teleport);
|
|
452
|
+
for (let pass = 0; pass < PAGERANK_ITERATIONS; pass += 1) {
|
|
453
|
+
const next = /* @__PURE__ */ new Map();
|
|
454
|
+
for (const node of nodes) {
|
|
455
|
+
next.set(node.id, (1 - PAGERANK_DAMPING) * (teleport.get(node.id) ?? 0));
|
|
456
|
+
}
|
|
457
|
+
for (const node of nodes) {
|
|
458
|
+
const outgoing = adjacency.get(node.id) ?? [];
|
|
459
|
+
const current = rank.get(node.id) ?? 0;
|
|
460
|
+
const totalWeight = outgoing.reduce((sum, edge) => sum + edge.weight, 0);
|
|
461
|
+
if (totalWeight <= 0) {
|
|
462
|
+
next.set(node.id, (next.get(node.id) ?? 0) + PAGERANK_DAMPING * current);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
for (const edge of outgoing) {
|
|
466
|
+
next.set(
|
|
467
|
+
edge.id,
|
|
468
|
+
(next.get(edge.id) ?? 0) + PAGERANK_DAMPING * current * (edge.weight / totalWeight)
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
rank = next;
|
|
473
|
+
}
|
|
474
|
+
const max = Math.max(...[...rank.values()], 1e-5);
|
|
475
|
+
return new Map([...rank.entries()].map(([id, value]) => [id, value / max]));
|
|
476
|
+
}
|
|
477
|
+
function hybridRankNodes(nodes, edges, seedIds, baseRank, keywords) {
|
|
478
|
+
const ppr = personalizedPageRank(nodes, edges, seedIds);
|
|
479
|
+
return nodes.map((node) => {
|
|
480
|
+
const ranked = baseRank(node);
|
|
481
|
+
const searchText = keywords.length > 0 ? graphNodeSearchText(node) : "";
|
|
482
|
+
const matchedTerms = keywords.filter((keyword) => searchText.includes(keyword));
|
|
483
|
+
const taskBoost = Math.min(0.12, matchedTerms.length * 0.035);
|
|
484
|
+
const graphBoost = (ppr.get(node.id) ?? 0) * 0.32;
|
|
485
|
+
const weightedScore = ranked.score * 0.68 + graphBoost + taskBoost;
|
|
486
|
+
return {
|
|
487
|
+
id: node.id,
|
|
488
|
+
type: node.type,
|
|
489
|
+
score: Number(Math.min(1, weightedScore).toFixed(3)),
|
|
490
|
+
reason: matchedTerms.length > 0 ? `${ranked.reason}+pagerank+task_match` : `${ranked.reason}+pagerank`,
|
|
491
|
+
...matchedTerms.length > 0 ? { matched_terms: matchedTerms } : {}
|
|
492
|
+
};
|
|
493
|
+
}).sort(
|
|
494
|
+
(a, b) => b.score - a.score || (b.matched_terms?.length ?? 0) - (a.matched_terms?.length ?? 0) || a.id.localeCompare(b.id)
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
function rankGraphRouteContextNodes(nodes, edges, routeNodeId, ids, keywords = []) {
|
|
419
498
|
const scoreForNode = (node) => {
|
|
420
499
|
if (node.id === routeNodeId) return { score: 1, reason: "requested_route" };
|
|
421
500
|
if (ids.pages.includes(node.id)) return { score: 0.95, reason: "route_page" };
|
|
@@ -430,21 +509,7 @@ function rankGraphRouteContextNodes(nodes, routeNodeId, ids, keywords = []) {
|
|
|
430
509
|
if (ids.sourceArtifacts.includes(node.id)) return { score: 0.32, reason: "source_provenance" };
|
|
431
510
|
return { score: 0.24, reason: "included_context" };
|
|
432
511
|
};
|
|
433
|
-
return nodes
|
|
434
|
-
const ranked = scoreForNode(node);
|
|
435
|
-
const searchText = keywords.length > 0 ? graphNodeSearchText(node) : "";
|
|
436
|
-
const matchedTerms = keywords.filter((keyword) => searchText.includes(keyword));
|
|
437
|
-
const taskBoost = Math.min(0.12, matchedTerms.length * 0.035);
|
|
438
|
-
return {
|
|
439
|
-
id: node.id,
|
|
440
|
-
type: node.type,
|
|
441
|
-
score: Number(Math.min(1, ranked.score + taskBoost).toFixed(3)),
|
|
442
|
-
reason: matchedTerms.length > 0 ? `${ranked.reason}+task_match` : ranked.reason,
|
|
443
|
-
...matchedTerms.length > 0 ? { matched_terms: matchedTerms } : {}
|
|
444
|
-
};
|
|
445
|
-
}).sort(
|
|
446
|
-
(a, b) => b.score - a.score || (b.matched_terms?.length ?? 0) - (a.matched_terms?.length ?? 0) || a.id.localeCompare(b.id)
|
|
447
|
-
);
|
|
512
|
+
return hybridRankNodes(nodes, edges, /* @__PURE__ */ new Set([routeNodeId]), scoreForNode, keywords);
|
|
448
513
|
}
|
|
449
514
|
function buildGraphRouteContext(snapshot, route, options = {}) {
|
|
450
515
|
if (!snapshot) return null;
|
|
@@ -555,7 +620,7 @@ function buildGraphRouteContext(snapshot, route, options = {}) {
|
|
|
555
620
|
sourceHash: snapshot.source_hash,
|
|
556
621
|
routeNode,
|
|
557
622
|
ranking: {
|
|
558
|
-
method: keywords.length > 0 ? "
|
|
623
|
+
method: keywords.length > 0 ? "hybrid_weighted_pagerank_with_task_boost" : "hybrid_weighted_pagerank",
|
|
559
624
|
seed: routeNode.id,
|
|
560
625
|
task_keywords: keywords
|
|
561
626
|
},
|
|
@@ -574,7 +639,7 @@ function buildGraphRouteContext(snapshot, route, options = {}) {
|
|
|
574
639
|
sourceArtifacts: graphNodeIdsByType(nodes, "SourceArtifact").length
|
|
575
640
|
},
|
|
576
641
|
ids,
|
|
577
|
-
ranked: rankGraphRouteContextNodes(nodes, routeNode.id, ids, keywords),
|
|
642
|
+
ranked: rankGraphRouteContextNodes(nodes, edges, routeNode.id, ids, keywords),
|
|
578
643
|
nodes,
|
|
579
644
|
edges
|
|
580
645
|
};
|
|
@@ -616,7 +681,7 @@ function impactIds(nodes) {
|
|
|
616
681
|
sourceArtifacts: graphNodeIdsByType(nodes, "SourceArtifact")
|
|
617
682
|
};
|
|
618
683
|
}
|
|
619
|
-
function rankGraphImpactContextNodes(nodes, seedIds, ids, keywords = []) {
|
|
684
|
+
function rankGraphImpactContextNodes(nodes, edges, seedIds, ids, keywords = []) {
|
|
620
685
|
const scoreForNode = (node) => {
|
|
621
686
|
if (seedIds.has(node.id)) return { score: 1, reason: "seed_node" };
|
|
622
687
|
if (ids.routes.includes(node.id)) return { score: 0.92, reason: "affected_route" };
|
|
@@ -633,21 +698,7 @@ function rankGraphImpactContextNodes(nodes, seedIds, ids, keywords = []) {
|
|
|
633
698
|
if (ids.sourceArtifacts.includes(node.id)) return { score: 0.34, reason: "source_provenance" };
|
|
634
699
|
return { score: 0.24, reason: "included_impact" };
|
|
635
700
|
};
|
|
636
|
-
return nodes
|
|
637
|
-
const ranked = scoreForNode(node);
|
|
638
|
-
const searchText = keywords.length > 0 ? graphNodeSearchText(node) : "";
|
|
639
|
-
const matchedTerms = keywords.filter((keyword) => searchText.includes(keyword));
|
|
640
|
-
const taskBoost = Math.min(0.12, matchedTerms.length * 0.035);
|
|
641
|
-
return {
|
|
642
|
-
id: node.id,
|
|
643
|
-
type: node.type,
|
|
644
|
-
score: Number(Math.min(1, ranked.score + taskBoost).toFixed(3)),
|
|
645
|
-
reason: matchedTerms.length > 0 ? `${ranked.reason}+task_match` : ranked.reason,
|
|
646
|
-
...matchedTerms.length > 0 ? { matched_terms: matchedTerms } : {}
|
|
647
|
-
};
|
|
648
|
-
}).sort(
|
|
649
|
-
(a, b) => b.score - a.score || (b.matched_terms?.length ?? 0) - (a.matched_terms?.length ?? 0) || a.id.localeCompare(b.id)
|
|
650
|
-
);
|
|
701
|
+
return hybridRankNodes(nodes, edges, seedIds, scoreForNode, keywords);
|
|
651
702
|
}
|
|
652
703
|
function shouldTraverseImpactEdge(edge, includedNodeIds, projectId) {
|
|
653
704
|
if (!IMPACT_TRAVERSAL_RELATIONS.has(edge.relation)) return false;
|
|
@@ -721,7 +772,7 @@ function buildGraphImpactContext(snapshot, seed, options = {}) {
|
|
|
721
772
|
const totalEdges = edges.length;
|
|
722
773
|
const keywords = taskKeywords(options.task);
|
|
723
774
|
const fullIds = impactIds(nodes);
|
|
724
|
-
const fullRanked = rankGraphImpactContextNodes(nodes, seedNodeIds, fullIds, keywords);
|
|
775
|
+
const fullRanked = rankGraphImpactContextNodes(nodes, edges, seedNodeIds, fullIds, keywords);
|
|
725
776
|
const boundedLimit = typeof options.limit === "number" && Number.isFinite(options.limit) && options.limit > 0 ? Math.floor(options.limit) : void 0;
|
|
726
777
|
let truncated = false;
|
|
727
778
|
if (boundedLimit && nodes.length > boundedLimit) {
|
|
@@ -737,14 +788,14 @@ function buildGraphImpactContext(snapshot, seed, options = {}) {
|
|
|
737
788
|
truncated = true;
|
|
738
789
|
}
|
|
739
790
|
const ids = impactIds(nodes);
|
|
740
|
-
const ranked = rankGraphImpactContextNodes(nodes, seedNodeIds, ids, keywords);
|
|
791
|
+
const ranked = rankGraphImpactContextNodes(nodes, edges, seedNodeIds, ids, keywords);
|
|
741
792
|
return {
|
|
742
793
|
snapshotId: snapshot.id,
|
|
743
794
|
sourceHash: snapshot.source_hash,
|
|
744
795
|
seedNodes: sortGraphNodes(seedNodes),
|
|
745
796
|
missingNodeIds,
|
|
746
797
|
ranking: {
|
|
747
|
-
method: keywords.length > 0 ? "
|
|
798
|
+
method: keywords.length > 0 ? "hybrid_impact_pagerank_with_task_boost" : "hybrid_impact_pagerank",
|
|
748
799
|
seed: [...seedNodeIds].sort(),
|
|
749
800
|
task_keywords: keywords
|
|
750
801
|
},
|