@decantr/core 3.4.0 → 3.5.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 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
- - draft 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, task-aware route-context ranking, behavior-obligation LocalRule projection support through existing graph shapes, node impact context extraction, and an in-memory `GraphStore` adapter for early graph-foundation work
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 are draft Decantr 3 foundation APIs. They establish the storage boundary, typed schema shape, temporal snapshot/diff shape, payload-filterable node queries, task-aware route-context ranking, node impact traversal, and provider-neutral contract capsule shape for CLI, MCP, verifier, and CI integration. 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. 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.
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
- function rankGraphRouteContextNodes(nodes, routeNodeId, ids, keywords = []) {
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.map((node) => {
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 ? "weighted_traversal_with_task_boost" : "weighted_traversal",
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.map((node) => {
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 ? "impact_traversal_with_task_boost" : "impact_traversal",
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
  },