@weftlet/toolkit 0.7.1 → 0.7.2

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
@@ -14,7 +14,7 @@ Proposed future capabilities are recorded in the [Weftlet 2.0 product plan](docs
14
14
  ## Set up a project
15
15
 
16
16
  <!-- weftlet:release:start -->
17
- Package versions in this checkout/archive: `@weftlet/toolkit@0.7.1` and `@weftlet/core@0.7.1`.
17
+ Package versions in this checkout/archive: `@weftlet/toolkit@0.7.2` and `@weftlet/core@0.7.2`.
18
18
  <!-- weftlet:release:end -->
19
19
 
20
20
  Weftlet uses `@weftlet/toolkit`; its neutral `@weftlet/core` dependency is installed
@@ -468,7 +468,9 @@ the search's total ceiling is 100,000 examinations. Responses report replay work
468
468
  Follow `search.continuation` with identical inputs even if a work page contains no completed
469
469
  path. Depth cuts and total-work exhaustion remain explicit without pretending all dependencies
470
470
  were checked; inspect `frontier` and restart with greater depth or narrower endpoints/relations.
471
- Paths are deterministic cited associations, not shortest routes or proof of business compatibility.
471
+ Paths enumerate shorter cited routes first, prioritizing a direct connection before unrelated branches.
472
+ They are associations, not proof of business compatibility. Pending frontiers report exact counts
473
+ and up to 32 examples with explicit omissions; candidate-work and depth limits still apply.
472
474
  The HTML viewer uses the same engine and continuation on its displayed facts and selected layers.
473
475
  See [path semantics and limits](docs/ADR-030-bounded-evidence-paths.md).
474
476
 
@@ -484,9 +486,12 @@ pending re-extraction. Queries compare captured file digests with current workin
484
486
  report subsequent edits separately from captured and excluded changes. Older overlays without
485
487
  capture metadata have unknown freshness. Historical or draft evidence cannot validate a current diff.
486
488
 
487
- Impact queries [rank nearby consumers and varied evidence sources](docs/ADR-012-impact-ranking.md)
489
+ Impact queries [rank nearby consumers and varied evidence sources](docs/ADR-041-bounded-relationship-retrieval.md)
488
490
  ahead of repeated detail and shared-import context. Ranking changes ordering, not saved facts or
489
- the reachable scope. Responses identify the ranking version; after a ranking upgrade, restart
491
+ the reachable scope. Saved local calls, their exact references and consumer relationships precede dense local-variable
492
+ detail within each source; unindexed symbol hubs receive a penalty unless explicitly queried.
493
+ These are ordering signals, not new assertions or proof of execution. All evidence remains in
494
+ the reachable set. Responses identify the ranking version; after a ranking upgrade, restart
490
495
  scans whose old continuation tokens are rejected. No snapshot refresh is needed for new ordering.
491
496
 
492
497
  The published toolkit includes a [coverage reporting correction](docs/ADR-024-implementation-coverage.md)
@@ -37,7 +37,7 @@ export declare class SavedGraphPathService implements GraphPathQueryService {
37
37
  diagnostics: import("@weftlet/core").ContextPack["diagnostics"];
38
38
  search: {
39
39
  readonly version: string;
40
- readonly order: "fact-id-depth-first";
40
+ readonly order: "fact-id-depth-first" | "shortest-first-target-first";
41
41
  readonly direction: import("@weftlet/core").GraphDirection;
42
42
  readonly predicates: readonly import("@weftlet/core").GraphPredicate[];
43
43
  readonly maxDepth: number;
@@ -0,0 +1,7 @@
1
+ import type { GraphFact, GraphNode } from '@weftlet/core';
2
+ /** Query-order signals from saved evidence, never new assertions or authority. */
3
+ export declare function impactRankingSignals(facts: readonly GraphFact[], anchorTerms: readonly string[]): {
4
+ penalty: (fact: GraphFact) => 0 | 8;
5
+ seedNodes: (fact: GraphFact) => GraphNode[];
6
+ relationshipOrder: (fact: GraphFact) => 0 | 2 | 1;
7
+ };
@@ -0,0 +1,54 @@
1
+ /** Query-order signals from saved evidence, never new assertions or authority. */
2
+ export function impactRankingSignals(facts, anchorTerms) {
3
+ const definitions = new Map();
4
+ const callsiteReferences = new Set();
5
+ const callableNativeSymbols = new Set();
6
+ const localCall = (fact) => fact.predicate === 'calls' && fact.codeCall !== undefined &&
7
+ fact.codeCall.target !== 'external' && fact.codeCall.targetDefinitions.length > 0;
8
+ for (const fact of facts) {
9
+ if (fact.codeOccurrence?.role === 'definition' && fact.predicate === 'definedBy') {
10
+ const paths = definitions.get(fact.subject.id) ?? new Set();
11
+ paths.add(fact.provenance.path);
12
+ definitions.set(fact.subject.id, paths);
13
+ }
14
+ if (localCall(fact)) {
15
+ for (const proof of fact.codeCall.targetOccurrences)
16
+ callsiteReferences.add(proof.factId);
17
+ if (fact.object.codeSymbol)
18
+ callableNativeSymbols.add(fact.object.codeSymbol.nativeSymbol);
19
+ }
20
+ }
21
+ const explicit = (node) => anchorTerms.some(term => `${node.id} ${node.label} ${node.codeSymbol?.nativeSymbol ?? ''}`.toLowerCase().includes(term));
22
+ const unindexed = (node) => node.kind === 'CodeSymbol' &&
23
+ node.codeSymbol?.resolution === 'no-indexed-definition';
24
+ const hub = (node) => unindexed(node) && !explicit(node);
25
+ const penalty = (fact) => [fact.subject, fact.object].some(hub) ? 8 : 0;
26
+ const seedNodes = (fact) => {
27
+ const eligible = [fact.subject, fact.object].filter(node => node.kind !== 'Project');
28
+ const matching = eligible.filter(explicit);
29
+ if (matching.length)
30
+ return matching;
31
+ // A cited local operation's library references must not turn those shared
32
+ // library identities into zero-distance anchors for all their co-users.
33
+ const local = eligible.filter(node => !unindexed(node));
34
+ return local.length ? local : eligible;
35
+ };
36
+ const relationshipOrder = (fact) => {
37
+ if (localCall(fact) || callsiteReferences.has(fact.id))
38
+ return 0;
39
+ if (fact.predicate === 'consumes' && fact.object.id.startsWith('symbol:scip:') &&
40
+ callableNativeSymbols.has(fact.object.label))
41
+ return 0;
42
+ if (fact.codeOccurrence?.role === 'reference') {
43
+ if (explicit(fact.object))
44
+ return 0;
45
+ const targetPaths = definitions.get(fact.object.id);
46
+ if (targetPaths && [...targetPaths].some(path => path !== fact.provenance.path))
47
+ return 1;
48
+ }
49
+ if (fact.codeOccurrence || fact.codeCall)
50
+ return 2;
51
+ return 0; // Keep intent, queue/data contracts, validation and legacy adapter relations available.
52
+ };
53
+ return { penalty, seedNodes, relationshipOrder };
54
+ }
@@ -16,11 +16,13 @@ export declare function searchEvidencePaths(edges: readonly PathEdge[], start: s
16
16
  paths: EvidencePath[];
17
17
  frontier: {
18
18
  pending: {
19
- nodeId: string;
20
- viaFactIds: string[];
21
- nextFactId: string;
22
- remainingCandidates: number;
19
+ readonly nodeId: string;
20
+ readonly viaFactIds: readonly string[];
21
+ readonly nextFactId: string;
22
+ readonly remainingCandidates: number;
23
23
  }[];
24
+ pendingCount: number;
25
+ omittedPending: number;
24
26
  depthLimited: {
25
27
  count: number;
26
28
  examples: {
@@ -14,7 +14,10 @@ export function searchEvidencePaths(edges, start, target, options, knownNodeIds)
14
14
  validateBounds();
15
15
  const adjacency = new Map();
16
16
  const nodeIds = new Set();
17
- for (const edge of [...edges].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) {
17
+ // Building adjacency already visits every input edge. Prefer closing edges in
18
+ // that index; candidate examination below still counts every selected edge.
19
+ for (const edge of [...edges].sort((a, b) => Number(b.to === target) - Number(a.to === target) ||
20
+ (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
18
21
  const entries = adjacency.get(edge.from) ?? [];
19
22
  entries.push(edge);
20
23
  adjacency.set(edge.from, entries);
@@ -22,40 +25,55 @@ export function searchEvidencePaths(edges, start, target, options, knownNodeIds)
22
25
  nodeIds.add(edge.to);
23
26
  }
24
27
  const paths = [];
25
- const stack = nodeIds.has(start) && nodeIds.has(target) && start !== target ? [{ nodeId: start, next: 0 }] : [];
28
+ // At most one prefix is allocated per examined edge, bounded by searchLimit.
29
+ // Parent links retain distinct simple paths without copying whole path arrays.
30
+ const queue = nodeIds.has(start) && nodeIds.has(target) && start !== target
31
+ ? [{ nodeId: start, next: 0, depth: 0 }] : [];
32
+ let head = 0;
26
33
  const depthExamples = [];
27
34
  let depthCount = 0;
28
35
  let totalSteps = 0;
29
36
  const trim = () => {
30
- while (stack.length && stack.at(-1).next >= (adjacency.get(stack.at(-1).nodeId)?.length ?? 0))
31
- stack.pop();
37
+ while (head < queue.length && queue[head].next >= (adjacency.get(queue[head].nodeId)?.length ?? 0))
38
+ head++;
39
+ };
40
+ const prefixPath = (frame) => {
41
+ const nodeIds = [], factIds = [];
42
+ for (let cursor = frame; cursor; cursor = cursor.parent) {
43
+ nodeIds.push(cursor.nodeId);
44
+ if (cursor.via !== undefined)
45
+ factIds.push(cursor.via);
46
+ }
47
+ return { nodeIds: nodeIds.reverse(), factIds: factIds.reverse() };
32
48
  };
33
49
  const step = () => {
34
- const frame = stack.at(-1);
50
+ const frame = queue[head];
35
51
  const edge = adjacency.get(frame.nodeId)[frame.next++];
36
52
  totalSteps++;
37
- if (stack.some(item => item.nodeId === edge.to))
38
- return;
39
- const viaFactIds = stack.slice(1).map(item => item.via);
40
- if (viaFactIds.length === options.maxDepth) {
53
+ for (let cursor = frame; cursor; cursor = cursor.parent) {
54
+ if (cursor.nodeId === edge.to)
55
+ return;
56
+ }
57
+ if (frame.depth === options.maxDepth) {
41
58
  depthCount++;
42
59
  if (depthExamples.length < 20)
43
- depthExamples.push({ nodeId: frame.nodeId, viaFactIds,
60
+ depthExamples.push({ nodeId: frame.nodeId, viaFactIds: prefixPath(frame).factIds,
44
61
  nextFactId: edge.id, nextNodeId: edge.to });
45
62
  }
46
63
  else if (edge.to === target) {
47
- return { nodeIds: [...stack.map(item => item.nodeId), target], factIds: [...viaFactIds, edge.id] };
64
+ const path = prefixPath(frame);
65
+ return { nodeIds: [...path.nodeIds, target], factIds: [...path.factIds, edge.id] };
48
66
  }
49
- else {
50
- stack.push({ nodeId: edge.to, next: 0, via: edge.id });
67
+ else if (adjacency.has(edge.to)) {
68
+ queue.push({ nodeId: edge.to, next: 0, depth: frame.depth + 1, via: edge.id, parent: frame });
51
69
  }
52
70
  };
53
71
  trim();
54
- // Reconstruct only bounded search state. Never accept a caller-supplied stack or facts.
72
+ // Reconstruct bounded search state; never accept a caller-supplied queue or facts.
55
73
  const replay = () => {
56
74
  let replayPageSteps = 0;
57
75
  let replayPagePaths = 0;
58
- while (totalSteps < offset && stack.length) {
76
+ while (totalSteps < offset && head < queue.length) {
59
77
  if (step())
60
78
  replayPagePaths++;
61
79
  replayPageSteps++;
@@ -65,26 +83,33 @@ export function searchEvidencePaths(edges, start, target, options, knownNodeIds)
65
83
  }
66
84
  trim();
67
85
  }
68
- if (totalSteps !== offset || (offset && (!stack.length || replayPagePaths || replayPageSteps))) {
86
+ if (totalSteps !== offset || (offset && (head === queue.length || replayPagePaths || replayPageSteps))) {
69
87
  throw new Error('Invalid path continuation boundary');
70
88
  }
71
89
  };
72
90
  replay();
73
91
  if (!offset && start === target && (nodeIds.has(start) || knownNodeIds?.has(start)))
74
92
  paths.push({ nodeIds: [start], factIds: [] });
75
- while (stack.length && paths.length < options.maxPaths && totalSteps - offset < options.maxSteps && totalSteps < searchLimit) {
93
+ while (head < queue.length && paths.length < options.maxPaths && totalSteps - offset < options.maxSteps && totalSteps < searchLimit) {
76
94
  const path = step();
77
95
  if (path)
78
96
  paths.push(path);
79
97
  trim();
80
98
  }
81
- const pending = stack.flatMap((frame, index) => {
99
+ const pending = [];
100
+ let pendingCount = 0;
101
+ for (let index = head; index < queue.length; index++) {
102
+ const frame = queue[index];
82
103
  const entries = adjacency.get(frame.nodeId) ?? [];
83
- return frame.next < entries.length ? [{ nodeId: frame.nodeId,
84
- viaFactIds: stack.slice(1, index + 1).map(item => item.via), nextFactId: entries[frame.next].id,
85
- remainingCandidates: entries.length - frame.next }] : [];
86
- });
87
- const exhaustedWithinDepth = stack.length === 0;
104
+ if (frame.next >= entries.length)
105
+ continue;
106
+ pendingCount++;
107
+ if (pending.length < 32)
108
+ pending.push({ nodeId: frame.nodeId,
109
+ viaFactIds: prefixPath(frame).factIds, nextFactId: entries[frame.next].id,
110
+ remainingCandidates: entries.length - frame.next });
111
+ }
112
+ const exhaustedWithinDepth = head === queue.length;
88
113
  const stopReason = () => {
89
114
  if (exhaustedWithinDepth)
90
115
  return 'exhausted';
@@ -94,7 +119,8 @@ export function searchEvidencePaths(edges, start, target, options, knownNodeIds)
94
119
  return 'path-page-limit';
95
120
  return 'work-page-limit';
96
121
  };
97
- return { paths, frontier: { pending, depthLimited: { count: depthCount, examples: depthExamples,
122
+ return { paths, frontier: { pending, pendingCount, omittedPending: pendingCount - pending.length,
123
+ depthLimited: { count: depthCount, examples: depthExamples,
98
124
  omitted: depthCount - depthExamples.length } }, search: { searchLimit, replayedSteps: offset,
99
125
  examinedSteps: totalSteps - offset, totalSteps, stopReason: stopReason(), exhaustedWithinDepth,
100
126
  depthLimitReached: depthCount > 0, truncated: !exhaustedWithinDepth || depthCount > 0 } };
@@ -16,7 +16,7 @@ export function renderGraphPaths(result) {
16
16
  `Direction: ${search.direction}; predicates: ${search.predicates.join(', ') || 'all'}; order: ${search.order}`,
17
17
  `Depth: ${search.maxDepth}; paths on page: ${result.paths.length}/${search.maxPaths}; stop: ${search.stopReason}; truncated: ${search.truncated}`,
18
18
  `Candidate steps: ${search.examinedSteps} new + ${search.replayedSteps} replay; ${search.totalSteps}/${search.searchLimit} total ceiling.`,
19
- `Unexamined prefixes: ${frontier.pending.length}; depth-limited extensions: ${frontier.depthLimited.count} (${frontier.depthLimited.omitted} examples omitted).`,
19
+ `Unexamined prefixes: ${frontier.pendingCount ?? frontier.pending.length} (${frontier.omittedPending ?? 0} examples omitted); depth-limited extensions: ${frontier.depthLimited.count} (${frontier.depthLimited.omitted} examples omitted).`,
20
20
  ...(search.continuation ? [`Continuation (same request): ${search.continuation}`] : []),
21
21
  ...(search.depthLimitReached ? ['Depth limit reached: restart with a larger maxDepth or explore a frontier node. Continuation does not lift the depth bound.'] : []),
22
22
  ...(search.stopReason === 'search-limit' ? ['Total search limit reached: narrow the relations or explore an intermediate node; no further continuation is available.'] : []),
@@ -1,5 +1,5 @@
1
1
  import type { GraphPathQueryService, GraphPaths, GraphPathsRequest, SnapshotStore } from '@weftlet/core';
2
- export declare const GRAPH_PATHS_VERSION = "simple-evidence-paths-v1";
2
+ export declare const GRAPH_PATHS_VERSION = "simple-evidence-paths-v2";
3
3
  export declare class SnapshotGraphPathService implements GraphPathQueryService {
4
4
  private readonly store;
5
5
  constructor(store: Pick<SnapshotStore, 'read'>);
@@ -6,7 +6,7 @@ import { selectDiagnostics } from './diagnostics.js';
6
6
  import { parseGraphPathsRequest } from './path-request.js';
7
7
  import { searchEvidencePaths } from './path-search.js';
8
8
  import { renderGraphPaths } from './paths-renderer.js';
9
- export const GRAPH_PATHS_VERSION = 'simple-evidence-paths-v1';
9
+ export const GRAPH_PATHS_VERSION = 'simple-evidence-paths-v2';
10
10
  function continuationOffset(token, binding) {
11
11
  if (!token)
12
12
  return 0;
@@ -65,7 +65,7 @@ export class SnapshotGraphPathService {
65
65
  paths: result.paths, facts, coverage: coverageForFacts(facts, saved),
66
66
  diagnostics: selectDiagnostics(saved, {}, facts, [], [], new Set([request.fromNodeId, request.toNodeId,
67
67
  ...facts.flatMap(fact => [fact.subject.id, fact.object.id])])),
68
- search: { version: GRAPH_PATHS_VERSION, order: 'fact-id-depth-first', direction: request.direction,
68
+ search: { version: GRAPH_PATHS_VERSION, order: 'shortest-first-target-first', direction: request.direction,
69
69
  predicates: request.predicates, maxDepth: request.maxDepth, maxPaths: request.maxPaths, maxSteps: request.maxSteps,
70
70
  ...result.search, ...(next ? { continuation: next } : {}) }, frontier: result.frontier,
71
71
  };
@@ -1,6 +1,6 @@
1
1
  import type { GraphFact } from '@weftlet/core';
2
2
  /** Binds continuation offsets to the ordering policy; changing this requires restarting a scan. */
3
- export declare const IMPACT_RANKING_VERSION = "proximity-diversity-v1";
3
+ export declare const IMPACT_RANKING_VERSION = "local-relationships-diversity-v2";
4
4
  export declare function factSearchText(fact: GraphFact): string;
5
5
  export interface RankedImpactFact {
6
6
  readonly fact: GraphFact;
@@ -1,6 +1,7 @@
1
1
  import { stableJson } from '../infrastructure/util.js';
2
+ import { impactRankingSignals } from './impact-signals.js';
2
3
  /** Binds continuation offsets to the ordering policy; changing this requires restarting a scan. */
3
- export const IMPACT_RANKING_VERSION = 'proximity-diversity-v1';
4
+ export const IMPACT_RANKING_VERSION = 'local-relationships-diversity-v2';
4
5
  export function factSearchText(fact) {
5
6
  return [fact.subject.id, fact.subject.label, fact.predicate, fact.object.id,
6
7
  fact.object.label, fact.provenance.path].join(' ').toLowerCase();
@@ -24,6 +25,9 @@ function stableTie(fact) {
24
25
  }
25
26
  /** Order an already bounded reachable set. No facts are pruned and no authority is changed. */
26
27
  export function rankImpactFacts(facts, direct, anchorTerms, focus) {
28
+ const signals = impactRankingSignals(facts, anchorTerms);
29
+ const costs = new Map(facts.map(fact => [fact, signals.penalty(fact)]));
30
+ const cost = (fact, forward) => Math.max(edgeCost(fact, forward), costs.get(fact));
27
31
  const adjacent = new Map();
28
32
  const connect = (from, to, cost) => {
29
33
  if (from.kind === 'Project' || to.kind === 'Project')
@@ -33,8 +37,8 @@ export function rankImpactFacts(facts, direct, anchorTerms, focus) {
33
37
  adjacent.set(from.id, edges);
34
38
  };
35
39
  for (const fact of facts) {
36
- connect(fact.subject, fact.object, edgeCost(fact, true));
37
- connect(fact.object, fact.subject, edgeCost(fact, false));
40
+ connect(fact.subject, fact.object, cost(fact, true));
41
+ connect(fact.object, fact.subject, cost(fact, false));
38
42
  }
39
43
  const distance = new Map();
40
44
  // Integer edge costs permit a bucket queue: O(edges + maximum path cost), without a heap dependency.
@@ -46,10 +50,8 @@ export function rankImpactFacts(facts, direct, anchorTerms, focus) {
46
50
  (pending[cost] ??= []).push(id);
47
51
  };
48
52
  for (const fact of direct) {
49
- const eligible = [fact.subject, fact.object].filter((node) => node.kind !== 'Project');
50
- const matching = eligible.filter((node) => anchorTerms.some((term) => `${node.id} ${node.label}`.toLowerCase().includes(term)));
51
53
  // Some adapters only identify an anchor through its citation. Keep those queries supported.
52
- for (const node of matching.length ? matching : eligible)
54
+ for (const node of signals.seedNodes(fact))
53
55
  enqueue(node.id, 0);
54
56
  }
55
57
  for (let cost = 0; cost < pending.length; cost++) {
@@ -65,14 +67,20 @@ export function rankImpactFacts(facts, direct, anchorTerms, focus) {
65
67
  const text = factSearchText(fact);
66
68
  return {
67
69
  fact,
68
- proximity: directIds.has(fact.id) ? 0 : Math.min((distance.get(fact.subject.id) ?? Infinity) + edgeCost(fact, true), (distance.get(fact.object.id) ?? Infinity) + edgeCost(fact, false)),
70
+ proximity: costs.get(fact) + (directIds.has(fact.id) ? 0 : Math.min((distance.get(fact.subject.id) ?? Infinity) + cost(fact, true), (distance.get(fact.object.id) ?? Infinity) + cost(fact, false))),
71
+ relationshipOrder: signals.relationshipOrder(fact),
69
72
  focus: focus.filter((term) => text.includes(term)).length,
70
73
  tie: stableTie(fact),
71
74
  };
72
75
  });
73
76
  const lexical = (a, b) => a < b ? -1 : a > b ? 1 : 0;
74
- const compare = (a, b) => b.focus - a.focus || lexical(a.tie, b.tie) || lexical(a.fact.id, b.fact.id);
75
- rows.sort((a, b) => a.proximity - b.proximity || compare(a, b));
77
+ const compare = (a, b) => a.relationshipOrder - b.relationshipOrder || b.focus - a.focus || lexical(a.tie, b.tie) || lexical(a.fact.id, b.fact.id);
78
+ // Allocate each source's early slots to relationships before local occurrence
79
+ // detail. Otherwise hundreds of nearer variable references consume a file's
80
+ // diversity allowance before its file-to-symbol consumer can be considered.
81
+ // Final ordering below still charges distance and shared-library penalties.
82
+ rows.sort((a, b) => costs.get(a.fact) - costs.get(b.fact) ||
83
+ a.relationshipOrder - b.relationshipOrder || a.proximity - b.proximity || compare(a, b));
76
84
  const counts = new Map();
77
85
  const diverse = rows.map((row) => {
78
86
  const seen = counts.get(row.fact.provenance.path) ?? 0;
@@ -102,7 +102,7 @@ export function viewerScript() {
102
102
  if (!nextPath.disabled) status.textContent += ' Continue to examine remaining branches.';
103
103
  if (result.search.depthLimitReached) status.textContent += ' Depth limit reached; increase depth and restart.';
104
104
  if (result.search.stopReason === 'search-limit') status.textContent += ' Narrow the selection or choose an intermediate node.';
105
- status.textContent += ' Pending prefixes: ' + result.frontier.pending.length + '; depth-limited extensions: ' + result.frontier.depthLimited.count + '.';
105
+ status.textContent += ' Pending prefixes: ' + result.frontier.pendingCount + ' (' + result.frontier.omittedPending + ' examples omitted); depth-limited extensions: ' + result.frontier.depthLimited.count + '.';
106
106
  search.value = ''; cluster.value = ''; document.getElementById('neighbors-only').checked = false;
107
107
  update();
108
108
  } catch (error) { clearPath(); status.textContent = error.message; update(); }
@@ -21,6 +21,13 @@ strict SHACL/publication. [ADR 040](ADR-040-bounded-graph-io.md) tracks the hash
21
21
  RDF/query and bounded-validation patch. Its public large-corpus lifecycle and hosted Linux/Windows checks
22
22
  pass; patch publication and private acceptance remain separate steps. No 1.0 approval is implied.
23
23
 
24
+ Consumer follow-up: **0.7.1 is published**. The maintainer reports successful strict hydration,
25
+ persistence/reload and unchanged repeat of 561,777 facts / 174,710 nodes with all 18 cached
26
+ artifacts preserved. That capacity blocker is resolved in the reported run. B6b remains open:
27
+ known direct paths and specific worker `references`/`consumes` assertions were displaced outside
28
+ fixed retrieval budgets. [ADR 041](ADR-041-bounded-relationship-retrieval.md) tracks the bounded
29
+ query-order patch and its public regression; no extraction refresh is needed for that patch.
30
+
24
31
  All three public 554,516-fact strict SHACL passes succeeded but took about 35–37 minutes each
25
32
  in this cloud. This is a capacity result, not a 30-minute hydration qualification. B6b must record
26
33
  full hydration and agent-query latency on the consumer workload as well as correctness.
@@ -1,6 +1,7 @@
1
1
  # ADR 012: order impact evidence by proximity and source diversity
2
2
 
3
- Status: implemented as a query-only heuristic; not product policy or an evidence gate.
3
+ Status: historical v1 query heuristic. [ADR 041](ADR-041-bounded-relationship-retrieval.md)
4
+ records the v2 library-hub and relationship-priority revision. Neither is product policy or an evidence gate.
4
5
 
5
6
  The self-repository evaluation found distractions connected through common imports such as
6
7
  `node:fs/promises`. The previous ordering prioritized keyword counts, then Codex/document-link
@@ -1,7 +1,7 @@
1
1
  # ADR 030: bounded cited dependency paths
2
2
 
3
- Status: B1b implemented for the next candidate. Hosted validation and merge identity are recorded
4
- in the change PR; this entry does not announce npm publication or private consumer acceptance.
3
+ Status: B1b shipped in 0.7.0. The query-order revision in [ADR 041](ADR-041-bounded-relationship-retrieval.md)
4
+ replaces depth-first enumeration; publication and consumer acceptance are recorded separately.
5
5
 
6
6
  ## Meaning and scope
7
7
 
@@ -18,8 +18,8 @@ An empty predicate list selects all relations; a nonempty list is an OR filter.
18
18
  cited associations, not causality, compatible business obligations or behavior verification.
19
19
  They preserve authority, review state, source citations and node variants on every returned fact.
20
20
 
21
- Read the same lossless RDF/SPARQL projection as other queries. Preserve its existing explicit
22
- 100,000-fact projection ceiling; exceeding it fails rather than truncating silently. No model,
21
+ Read the same lossless RDF/SPARQL projection as other queries. The 0.7.1 streaming changes
22
+ removed the former 100,000-fact projection ceiling without truncating evidence. No model,
23
23
  indexer, SHACL rerun or graph write is involved. Duplicate fact IDs and mismatched stored snapshot
24
24
  IDs fail. Responses reuse coverage, scoped diagnostic and checkout/overlay-state helpers.
25
25
  Coverage describes the page's supporting evidence, not a repository completion percentage;
@@ -28,9 +28,10 @@ in snapshot metadata. An exhausted search says nothing about excluded or unextra
28
28
 
29
29
  ## Enumeration, work and continuation
30
30
 
31
- Use deterministic depth-first traversal with adjacency sorted by assertion ID, independent of
32
- locale. This is not a shortest-path or importance ranking: alternatives reached later can be
33
- shorter or more relevant. Do not interpret the first path as the preferred design.
31
+ Use deterministic breadth-first traversal. Within each adjacency list, edges ending at the
32
+ requested target precede other edges, followed by assertion ID order independent of locale.
33
+ Completed paths arrive in nondecreasing edge count; a known direct connection is considered
34
+ first even on a high-branching source. This is not relevance, causality or a preferred design.
34
35
 
35
36
  Defaults are depth 8 (maximum 32), five paths per page (maximum 20) and 1,000 new candidate-edge
36
37
  examinations per page (maximum 10,000). Every considered edge counts, including cycles and depth
@@ -40,10 +41,11 @@ candidate count. Adjacency sorts are O(E log E); the existing RDF projection als
40
41
  There is no wall-clock latency guarantee.
41
42
 
42
43
  Continuations bind the operation version, normalized selectors and page budgets, saved manifest,
43
- actual facts and diagnostics. A cursor carries only a step offset, never an external stack or
44
+ actual facts and diagnostics. A cursor carries only a step offset, never an external queue or
44
45
  assertions. Bounded replay reconstructs state and verifies the offset is a valid page boundary.
45
- Responses report replayed and new steps separately. Replay is at most the total ceiling; a DFS
46
- stack is at most depth + 1. This trades bounded repeated CPU work for stateless continuation
46
+ Responses report replayed and new steps separately. Replay is at most the total ceiling.
47
+ At most one queued prefix is allocated per examined edge (100,001 including the root); parent
48
+ links share path prefixes, whose depth is bounded by 32. This trades repeated CPU work for stateless continuation
47
49
  without snapshot/cache writes or a large serialized queue. Tokens provide query consistency,
48
50
  not authentication. Changing selectors, depth, budgets or saved evidence requires restarting.
49
51
  Cumulative cost over many tiny pages can exceed one search's ceiling because replay is repeated;
@@ -53,8 +55,9 @@ The page includes only its completed paths and the union of their unchanged supp
53
55
  Work-limited pages may be empty and still have a continuation. Returned path count is not a
54
56
  count of all possible paths. A continuation can lead to a final empty page after dead ends/cycles.
55
57
 
56
- Report every active DFS prefix with its fact-ID prefix, next candidate fact ID and remaining
57
- adjacency-candidate count. These are not counts of valid paths. Depth cuts retain a total count,
58
+ Report the exact pending-prefix count and up to 32 examples with fact-ID prefixes, next
59
+ candidate fact IDs and remaining adjacency-candidate counts. Report the omitted example count
60
+ explicitly. These are not counts of valid paths. Depth cuts retain a total count,
58
61
  up to 20 concrete extension examples and an explicit omitted-example count. Frontier IDs refer
59
62
  to saved assertions; use `graph.neighbors` at that node/snapshot to retrieve their full citations.
60
63
 
@@ -73,9 +76,9 @@ path controls use one result per page, 1,000 new examinations, selectable direct
73
76
  and the same total ceiling. Changing selection resets continuation. Targets show exact IDs next
74
77
  to labels. Each highlighted edge is an unchanged fact from that displayed pack.
75
78
 
76
- The prior viewer-only shortest-path implementation is replaced by shared deterministic
77
- enumeration; labels now say "cited path" rather than promising a shortest route. `impact.scan`
78
- and `graph.neighbors` remain unchanged. No extraction identity, semantic artifact/cache key,
79
+ The viewer and service share this deterministic enumeration; labels say "cited path".
80
+ ADR 041 separately revises `impact.scan` ranking; `graph.neighbors` is unchanged.
81
+ No extraction identity, semantic artifact/cache key,
79
82
  snapshot format, dependency, rule policy or language support changes.
80
83
 
81
84
  ## Validation boundary
@@ -92,6 +95,6 @@ fresh installed CLI/MCP assets and continuation. Execute rendered viewer control
92
95
  DOM test harness and compare every highlighted path with the RDF service; this is script and
93
96
  projection parity, not interactive browser rendering or autonomous-agent usefulness evidence.
94
97
 
95
- B1a/B1b mechanisms are implemented; qualification and live product usefulness remain B6. B2's
96
- Graphify/SCIP comparison is next. No backend replacement or business compatibility inference
97
- is implied by paths through the existing evidence graph.
98
+ B1a/B1b mechanisms are implemented; qualification and live product usefulness remain B6.
99
+ No backend replacement or business compatibility inference is implied by paths through the
100
+ existing evidence graph.
@@ -0,0 +1,77 @@
1
+ # ADR 041: useful relationships within bounded retrieval
2
+
3
+ Status: query-only patch; validation and publication status are recorded in VALIDATION.md.
4
+
5
+ ## Evidence and scope
6
+
7
+ The maintainer reports successful 0.7.1 hydration, strict validation, persistence/reload and
8
+ unchanged repeat on the consumer's 561,777-fact graph, preserving all 18 cached artifacts.
9
+ Two subsequent failures concern retrieval: a known direct path was displaced by deep branches,
10
+ and specific consumer assertions were displaced by standard-library references.
11
+
12
+ The consumer independently confirmed that the missing `references` and `consumes` assertions
13
+ exist in the same snapshot and are reachable in expansion round two. Their calculated balanced
14
+ positions were 24,801 and 69,207, outside three pages of 60 assertions at depth three. The
15
+ diagnostic reproduced all 180 original result IDs in order without fetching larger pages or
16
+ modifying evidence. This is ranking displacement for those assertions, not missing extraction.
17
+ No private source, artifact or prompt was imported into this repository.
18
+
19
+ ## Path order
20
+
21
+ Replace fact-ID depth-first traversal with breadth-first traversal. Adjacency preparation places
22
+ edges closing on the exact target first, then sorts by fact ID. A known direct edge therefore
23
+ appears before unrelated branches consume the first page. All distinct simple paths and parallel
24
+ assertions remain eligible; no global visited-node pruning is introduced. Completed paths are
25
+ ordered by nondecreasing edge count, not business importance or causal confidence.
26
+
27
+ Retain the existing depth, page, candidate-step and cumulative work limits, including counted
28
+ cycle/depth-cut examinations. Graph loading/indexing is still outside the candidate-step budget.
29
+ The FIFO queue allocates at most one parent-linked prefix per examined edge, bounded by 100,001
30
+ including the root. Return exact pending counts, 32 pending-prefix examples and an explicit
31
+ omitted count; retain the existing bounded depth-cut examples. Stateless replay validates page
32
+ boundaries. `simple-evidence-paths-v2` rejects old cursors; the shared HTML engine uses the same
33
+ search. No full-graph completeness or latency guarantee is added.
34
+
35
+ ## Impact order
36
+
37
+ Keep the reachable set, extraction, depth semantics and evidence-lane balancing unchanged.
38
+ The `local-relationships-diversity-v2` policy revises the existing weighted-distance/source
39
+ diversity heuristic:
40
+
41
+ - A citation-only match seeds local endpoints before unresolved symbol endpoints. Referencing a
42
+ common library symbol in a changed function must not make all its co-users distance-zero peers.
43
+ - Edges touching a `CodeSymbol` with `no-indexed-definition` cost at least eight, and assertions
44
+ touching such a symbol receive eight additional ordering points. Explicit node/label/native-symbol
45
+ anchor-text matches exempt that node. Missing definitions are not classified as proven libraries;
46
+ omitted repository code can also be unindexed. All those assertions remain retrievable.
47
+ - Allocate early slots within each provenance source to saved local `calls`, their exact cited
48
+ reference proofs, and legacy file-to-symbol consumers matching saved local callable identities.
49
+ Other cross-file references follow before local occurrence detail. Existing product intent,
50
+ queue/data contracts, validation and adapter assertions retain their ordinary category.
51
+ - Within these categories use distance, focus and stable semantic tie keys. Add the per-source
52
+ ordinal to the distance score, then retain the existing evidence-lane balancing among equal
53
+ scores. Distant relationships still pay distance; local variable repetition cannot consume
54
+ all of a file's early slots before its consumer relationship is considered.
55
+
56
+ These are explainable ordering signals, not business rules or authority changes. A file-level
57
+ `consumes` relationship still does not establish invocation; a saved static call does not prove
58
+ runtime execution. Explicit library-symbol queries remain supported. Pagination retains every
59
+ reachable assertion exactly once; old ranking cursors fail with restart instructions.
60
+
61
+ ## Acceptance and compatibility
62
+
63
+ The frozen [public protocol](../evaluations/relationship-retrieval/protocol.md) uses three actual
64
+ TypeScript producer/queue/worker/test flows, real SCIP and call extraction, and deterministic
65
+ cited prose. It scores 36 specific calls, callsite references and legacy consumers separately
66
+ from 12-file discovery. The budget remains depth three, three pages of 60, all 540 assertions
67
+ in the denominator. Compare the same immutable saved snapshot and unchanged reachable counts.
68
+ Report standard-library share independently of the production unindexed-symbol heuristic.
69
+
70
+ Also check a 1,500-branch direct-edge case at 1,000 steps, every directed four-node graph against
71
+ an exhaustive path oracle, continuation/replay, incoming/both directions, authority/citation
72
+ preservation, shared viewer parity, explicit library queries and strict extraction/publication.
73
+
74
+ No extractor/assembly identity, semantic cache, persisted encoding, RDF constraints or snapshot
75
+ content changes. Existing 0.7.1 snapshots use the new ordering without hydration or model calls.
76
+ The 0.7.1 streaming/canonical-hash fixes remain intact. This public regression does not close
77
+ private consumer acceptance; TradePilot must retry the original cases with unchanged budgets.
package/docs/RELEASING.md CHANGED
@@ -202,14 +202,14 @@ Verify the matching public version in a fresh disposable project after publicati
202
202
 
203
203
  ```sh
204
204
  npm init -y
205
- npx --yes @weftlet/toolkit@0.7.1 init --yes
206
- npx --yes @weftlet/toolkit@0.7.1 config validate
205
+ npx --yes @weftlet/toolkit@0.7.2 init --yes
206
+ npx --yes @weftlet/toolkit@0.7.2 config validate
207
207
  ```
208
208
 
209
209
  For local installation, use both prepared archives from the same reviewed release:
210
210
 
211
211
  ```sh
212
- npm install --save-dev "/path/to/weftlet/.release/weftlet-core-0.7.1.tgz" "/path/to/weftlet/.release/weftlet-toolkit-0.7.1.tgz"
212
+ npm install --save-dev "/path/to/weftlet/.release/weftlet-core-0.7.2.tgz" "/path/to/weftlet/.release/weftlet-toolkit-0.7.2.tgz"
213
213
  ```
214
214
  <!-- weftlet:release:end -->
215
215
 
@@ -1,5 +1,42 @@
1
1
  # Validation evidence
2
2
 
3
+ ## 0.7.1 consumer follow-up and bounded retrieval patch, 2026-11-03
4
+
5
+ **Maintainer-reported consumer pass:** the published 0.7.1 toolkit hydrates, strictly validates,
6
+ persists and reloads TradePilot's 561,777 facts / 174,710 nodes. Unchanged repeat preserves the
7
+ snapshot and all 18 semantic artifacts. This closes the reported whole-string capacity blocker
8
+ for that workload; it is not general query-usefulness or official 1.0 acceptance.
9
+
10
+ **Maintainer-reported remaining defect:** a direct connection was absent from a work-limited
11
+ path page, and relevant worker `references`/`consumes` assertions were absent from three 60-fact
12
+ impact pages at depth three. A read-only consumer diagnostic independently reproduced all
13
+ 180 query IDs and the 420,837 reachable count. Both assertions exist in that same snapshot,
14
+ are reached in expansion round two, and were ranked at 24,801 and 69,207. This establishes
15
+ ranking displacement for those assertions. No private source or graph was accessed here.
16
+
17
+ **Public patch evidence:** [ADR 041](ADR-041-bounded-relationship-retrieval.md) and the
18
+ [frozen comparison](../evaluations/relationship-retrieval/README.md) record query-only changes.
19
+ On the same strictly validated 15,549-fact public TypeScript snapshot, both policies retain
20
+ 5,128 reachable assertions per case and the full 540 returned-assertion denominator. Expected
21
+ call, callsite-reference and legacy-consumer retrieval improves from **0/36 to 36/36**;
22
+ 12/12 file discovery is unchanged. TypeScript standard-library references change from
23
+ 450/540 to 0/540 on those bounded pages. Library facts remain stored/reachable, and explicit
24
+ library-symbol queries pass. Seven evidence files, including all three semantic artifacts,
25
+ retain their exact byte digests after rescoring. No hydration or model calls occur in comparison.
26
+
27
+ A fresh public extraction/strict-publication test passes with all 36 targets, exact repeated
28
+ page IDs and unchanged evidence bytes. The high-branching query-service regression returns the
29
+ same cited direct edge as impact.scan on page one at depth three, three paths and 1,000 steps,
30
+ with explicit pending/omitted frontier counts. Exhaustive four-node path and shared viewer
31
+ parity checks pass. Extraction/assembly identities, canonical hashing, storage and strict SHACL
32
+ constraints are unchanged. Old query cursors require restart; old snapshots need no refresh.
33
+
34
+ Exact-head release qualification and hosted platform results are recorded on
35
+ [PR 67](https://github.com/jchewitt/weftlet/pull/67); the evidence above records the local focused checks.
36
+ Private fixed-budget retrieval acceptance remains open; the public comparison is not a claim
37
+ about TradePilot's new result positions. Browser interaction and live provider quality retain
38
+ their existing separate validation status.
39
+
3
40
  ## 0.7.0 large-graph field blocker follow-up, 2026-11-02
4
41
 
5
42
  Status: public capacity qualification and hosted Linux/Windows checks pass; patch release pending.
@@ -108,7 +108,7 @@
108
108
  "destinationPath": "package.json",
109
109
  "sourceSha256": "a575dbb2fed8c554b95e8e9b3e03a0c21cab1be7249860bbebc18e046b2ff78d",
110
110
  "modified": true,
111
- "destinationSha256": "0a47b52a4b8b852572adb9104ea383b243793342ef62f58126991e2f8054284c"
111
+ "destinationSha256": "5ca2ea1f62e6b8393e731f761a1732a8ccf464948a905ab9fe246ae5d941dda8"
112
112
  },
113
113
  {
114
114
  "sourcePath": "tools/context-graph/packages/core/LICENSE",
@@ -129,7 +129,7 @@
129
129
  "destinationPath": "packages/core/README.md",
130
130
  "sourceSha256": "6044bf7b9a3dc793f41e2ff048546ed11938957d10a5b143eb1739ac4cd7477c",
131
131
  "modified": true,
132
- "destinationSha256": "0e2b6e8a2f81fb8ed1cd158964fbaad66b7f4cad12dc099a78f41399aacd79d3"
132
+ "destinationSha256": "1c0c80c8a85f9240ace5a2f01d102646fa293ef246a5f056eb94c7efa0f6e401"
133
133
  },
134
134
  {
135
135
  "sourcePath": "tools/context-graph/packages/core/THIRD_PARTY_NOTICES.md",
@@ -143,7 +143,7 @@
143
143
  "destinationPath": "packages/core/package.json",
144
144
  "sourceSha256": "2a29066eb8452f22fafcb06bb035a84dda93c5705dba32aba600a1b7e5aaf19c",
145
145
  "modified": true,
146
- "destinationSha256": "8651c8521653e12aeb1c7bf32a25dde3c82b76b1bdb455d234a6f45a43dd64c2"
146
+ "destinationSha256": "4a17cd04c7ec5a3a96ec0767d3883d588dfe6724822f254ed91f452b30b939bc"
147
147
  },
148
148
  {
149
149
  "sourcePath": "tools/context-graph/packages/core/src/core.ts",
@@ -388,7 +388,7 @@
388
388
  "destinationPath": "src/graph/query.test.ts",
389
389
  "sourceSha256": "ba07c52566da979fcbb451b0c0c99b466521993cb49415ff8b35df2b01d4ecaf",
390
390
  "modified": true,
391
- "destinationSha256": "7b6653f767c2e7445ec19f0cafc9d3fe97494ae139835ec63d335a7210e62c6c"
391
+ "destinationSha256": "20f0766299e66d4b5e7bc4577b6b0937c5bf87f2f1858f7b0205172231f0bb4c"
392
392
  },
393
393
  {
394
394
  "sourcePath": "tools/context-graph/src/query.ts",
@@ -634,6 +634,7 @@
634
634
  "docs/ADR-038-monorepo-code-coverage.md",
635
635
  "docs/ADR-039-mapping-proposals.md",
636
636
  "docs/ADR-040-bounded-graph-io.md",
637
+ "docs/ADR-041-bounded-relationship-retrieval.md",
637
638
  "docs/CLOUD-ENVIRONMENT.md",
638
639
  "docs/CONSUMER-TRIAL.md",
639
640
  "docs/ENVIRONMENT-VALIDATION.md",
@@ -779,6 +780,10 @@
779
780
  "evaluations/mapping-proposals/inputs.lock.json",
780
781
  "evaluations/mapping-proposals/protocol.json",
781
782
  "evaluations/monorepo-coverage/README.md",
783
+ "evaluations/relationship-retrieval/README.md",
784
+ "evaluations/relationship-retrieval/baseline.json",
785
+ "evaluations/relationship-retrieval/candidate.json",
786
+ "evaluations/relationship-retrieval/protocol.md",
782
787
  "evaluations/static-calls/README.md",
783
788
  "evaluations/static-calls/results/2026-10-29.json",
784
789
  "evaluations/weftlet/baseline.json",
@@ -894,6 +899,8 @@
894
899
  "scripts/release-records.test.ts",
895
900
  "scripts/release.mjs",
896
901
  "scripts/restore-publication-state.mjs",
902
+ "scripts/retrieval-study/preservation.ts",
903
+ "scripts/retrieval-study/run.ts",
897
904
  "scripts/smoke-mapping-proposals.mjs",
898
905
  "scripts/smoke-traversal.mjs",
899
906
  "scripts/update-inventory.mjs",
@@ -1012,6 +1019,7 @@
1012
1019
  "src/graph/evidence-description.ts",
1013
1020
  "src/graph/identity-preflight.test.ts",
1014
1021
  "src/graph/identity-preflight.ts",
1022
+ "src/graph/impact-signals.ts",
1015
1023
  "src/graph/neighborhood-renderer.ts",
1016
1024
  "src/graph/neighborhood-request.ts",
1017
1025
  "src/graph/neighborhood.test.ts",
@@ -1095,6 +1103,7 @@
1095
1103
  "tests/fixtures/monorepo-project.ts",
1096
1104
  "tests/fixtures/obligation-project.ts",
1097
1105
  "tests/fixtures/rdf-070-reference.ts",
1106
+ "tests/fixtures/relationship-retrieval-project.ts",
1098
1107
  "tests/fixtures/shacl.ts",
1099
1108
  "tests/fixtures/static-call-project.ts",
1100
1109
  "tests/fixtures/symbol-bindings.ts",
@@ -1111,6 +1120,7 @@
1111
1120
  "tests/rdf-ontology.test.ts",
1112
1121
  "tests/rdf-scaling-parity.test.ts",
1113
1122
  "tests/rdf.test.ts",
1123
+ "tests/relationship-retrieval.test.ts",
1114
1124
  "tests/routing.test.ts",
1115
1125
  "tests/runtime.test.ts",
1116
1126
  "tests/semantic-cache-reuse.test.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weftlet/toolkit",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Evidence-backed context graphs for AI development agents: product intent, contracts, code, tests and runtime behavior",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -66,7 +66,7 @@
66
66
  "rdf-validate-shacl": "0.6.5",
67
67
  "sparqljs": "3.7.4",
68
68
  "typescript": "5.5.4",
69
- "@weftlet/core": "^0.7.1"
69
+ "@weftlet/core": "^0.7.2"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@changesets/cli": "3.0.3",
@@ -121,6 +121,7 @@
121
121
  "release:prepare": "pnpm context-graph:publication-check && node scripts/release.mjs --prepare",
122
122
  "release:publish": "node scripts/release.mjs --publish",
123
123
  "govern": "tsx src/cli.ts govern --policy architecture.policy.json --output .context-graph/architecture-report.json",
124
+ "evaluate:retrieval": "tsx scripts/retrieval-study/run.ts",
124
125
  "evaluate:impact": "tsx scripts/evaluation/run.ts",
125
126
  "evaluate:code:check": "node scripts/code-graph-study/check.mjs",
126
127
  "benchmark:shacl": "node scripts/benchmark-shacl.mjs",