@skill-map/cli 0.36.0 → 0.37.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.
@@ -552,6 +552,55 @@ interface LinkLocation {
552
552
  column?: number;
553
553
  offset?: number;
554
554
  }
555
+ /**
556
+ * One syntactic site in the source node's body that contributed to a
557
+ * `Link`. Multiple occurrences accumulate when the same edge is detected
558
+ * by more than one extractor (e.g. `@./foo.md` from `at-directive` and
559
+ * `[label](./foo.md)` from `markdown-link` both resolve to the same
560
+ * target), or when the same extractor walks an extractor-internal
561
+ * dedup boundary. Today the merged edge's `trigger` / `location`
562
+ * mirror the FIRST occurrence; the array carries every site so the
563
+ * `core/redundant-target-reference` analyzer can flag multi-form
564
+ * references and rename operations can find every author surface.
565
+ */
566
+ interface LinkOccurrence {
567
+ /**
568
+ * Extractor id that observed this occurrence. Matches an entry of
569
+ * the parent `Link.sources[]` (extractor + occurrence are not 1:1,
570
+ * the same extractor can produce multiple occurrences when the
571
+ * intra-extractor dedup is relaxed in the future).
572
+ */
573
+ extractor: string;
574
+ /**
575
+ * Original substring as it appeared in the body (`@./real-agent.md`,
576
+ * `[deploy](./deploy.md)`, `/help`, `@team-lead`). Preserves author
577
+ * casing and the leading sigil so the analyzer can surface it
578
+ * verbatim in fix-up messages.
579
+ */
580
+ originalTrigger: string;
581
+ /**
582
+ * Position of the occurrence in the body. Optional, an extractor
583
+ * that does not track line numbers yet (legacy emit paths) omits
584
+ * this field; the analyzer falls back to "unknown line" in messages.
585
+ */
586
+ location?: LinkLocation | null;
587
+ }
588
+ /**
589
+ * External URL referenced from a node's body. Populated by the
590
+ * `core/external-url-counter` extractor and surfaced on the node so
591
+ * the inspector can list every outgoing http(s) reference without
592
+ * re-walking the body. Distinct from internal `Link` (which connects
593
+ * nodes inside the graph), external refs are leaf metadata: no
594
+ * counterparty node, no resolution.
595
+ */
596
+ interface IExternalRef {
597
+ /** Normalised URL (lowercased host, fragment stripped). */
598
+ url: string;
599
+ /** 1-indexed line of the occurrence in the source body, when known. */
600
+ line?: number;
601
+ /** Verbatim author substring (sigil-free; usually equals `url`). */
602
+ originalTrigger?: string;
603
+ }
555
604
  interface Node {
556
605
  path: string;
557
606
  /**
@@ -570,6 +619,15 @@ interface Node {
570
619
  linksOutCount: number;
571
620
  linksInCount: number;
572
621
  externalRefsCount: number;
622
+ /**
623
+ * Distinct external URLs referenced from this node's body, in
624
+ * extractor-order (first-seen wins, dedup is by normalised URL).
625
+ * Empty / absent when the body has no http(s) URLs. The denormalised
626
+ * `externalRefsCount` MUST equal `externalRefs.length` whenever
627
+ * both are present. Surfaced via `/api/nodes` so the inspector can
628
+ * list each URL without an extra round-trip.
629
+ */
630
+ externalRefs?: IExternalRef[];
573
631
  frontmatter?: Record<string, unknown>;
574
632
  tokens?: TripleSplit;
575
633
  /**
@@ -657,6 +715,30 @@ interface Link {
657
715
  sources: string[];
658
716
  trigger?: LinkTrigger | null;
659
717
  location?: LinkLocation | null;
718
+ /**
719
+ * Every syntactic site in the source body that contributed to this
720
+ * edge. Populated by extractors at emit time (one entry per emission)
721
+ * and accumulated by `dedupeLinks` when two extractors converge on the
722
+ * same `(source, target, kind, normalizedTrigger)` key. Empty / absent
723
+ * for legacy emits or for synthetic links (frontmatter-driven
724
+ * references, sidecar annotations) that have no body position. The
725
+ * `core/redundant-target-reference` analyzer walks this array to
726
+ * detect multi-form references to the same target from one body.
727
+ */
728
+ occurrences?: LinkOccurrence[];
729
+ /**
730
+ * Node path the link resolves to, when the post-walk
731
+ * `liftResolvedLinkConfidence` transform succeeded in matching the
732
+ * (trigger-style or path-style) target against the live graph. Equal
733
+ * to `link.target` for path-style links that hit a node directly;
734
+ * different from `link.target` for trigger-style links (a Claude
735
+ * `@real-agent` mention resolves to `.claude/agents/real-agent.md`,
736
+ * but `link.target` keeps the authored trigger). Absent when the
737
+ * link is unresolved (broken). The BFF `/api/links?to=<path>` uses
738
+ * this field to surface incoming edges that reach the node by name,
739
+ * not just by literal path.
740
+ */
741
+ resolvedTarget?: string | null;
660
742
  raw?: string | null;
661
743
  }
662
744
  /**
@@ -677,11 +759,16 @@ type SignalScope = 'body' | 'frontmatter' | 'sidecar';
677
759
  type SignalContext = 'code-block' | 'inline-code' | 'escaped';
678
760
  /**
679
761
  * Byte-range location for a body-scope `Signal`. `start` is inclusive,
680
- * `end` is exclusive (one past the last char).
762
+ * `end` is exclusive (one past the last char). `line` is the optional
763
+ * 1-indexed line number containing `start`, populated by extractors
764
+ * that already compute line tracking via `computeLineStarts` so the
765
+ * resolver's materialised `Link` preserves `link.location.line`
766
+ * without re-walking the body.
681
767
  */
682
768
  interface SignalRange {
683
769
  start: number;
684
770
  end: number;
771
+ line?: number;
685
772
  }
686
773
  /**
687
774
  * One alternative interpretation of a `Signal`. The resolver picks the
@@ -735,6 +822,59 @@ interface Signal {
735
822
  context?: SignalContext | null;
736
823
  /** One or more alternative interpretations. At least one. */
737
824
  candidates: SignalCandidate[];
825
+ /**
826
+ * Resolver outcome annotation, populated by `resolveSignals`. Absent on
827
+ * raw extractor emissions (before the resolver runs). When
828
+ * `outcome === 'materialised'`, `winnerIndex` points into `candidates[]`
829
+ * of the candidate the resolver chose; a corresponding `Link` was added
830
+ * to the graph. When `outcome === 'rejected'`, one of `rejectedBy` /
831
+ * `extractorDisabled` / `belowFloor` is set and no Link materialised.
832
+ * Both materialised and rejected Signals remain on
833
+ * `IAnalyzerContext.signals` so the `core/signal-collision` analyzer can
834
+ * surface losers as `warn` issues. Mirrors
835
+ * `signal.schema.json#/properties/resolution`.
836
+ */
837
+ resolution?: ISignalResolution;
838
+ }
839
+ /**
840
+ * Why the resolver chose to materialise or reject a `Signal`. Populated by
841
+ * `resolveSignals`; carries no meaning before that pass.
842
+ */
843
+ interface ISignalResolution {
844
+ outcome: 'materialised' | 'rejected';
845
+ /** Index into `Signal.candidates[]` of the winner. Set when `outcome === 'materialised'`. */
846
+ winnerIndex?: number;
847
+ /**
848
+ * Set when the Signal lost a cross-extractor range-overlap collision
849
+ * against another Signal at the same `source`. Names the winning Signal
850
+ * so an analyzer (or the operator drilling into the sidecar) can see WHO
851
+ * won and WHY.
852
+ */
853
+ rejectedBy?: {
854
+ source: string;
855
+ range: SignalRange;
856
+ /** Qualified id (`<plugin>/<extractor>`) of the winning candidate's extractor. */
857
+ extractorId: string;
858
+ reason: 'kind-priority' | 'higher-confidence' | 'longer-range' | 'earlier-declaration';
859
+ };
860
+ /**
861
+ * Phase 4+ stub: populated when every candidate of this Signal came from
862
+ * an extractor the operator disabled via
863
+ * `plugins.<id>.extensions.<extId>.enabled`. Today the resolver never
864
+ * sets this; documented so analyzer surfaces can be built when the filter
865
+ * lands.
866
+ */
867
+ extractorDisabled?: {
868
+ extractorId: string;
869
+ };
870
+ /**
871
+ * Phase 4+ stub: populated when every candidate's `confidence` fell below
872
+ * the configured floor. Today the resolver materialises every Signal that
873
+ * survives overlap regardless of confidence.
874
+ */
875
+ belowFloor?: {
876
+ threshold: number;
877
+ };
738
878
  }
739
879
  interface IssueFix {
740
880
  summary?: string;
@@ -1969,6 +2109,34 @@ interface IProvider extends IExtensionBase {
1969
2109
  * confidence-lift contract, which runs against the merged Link graph.
1970
2110
  */
1971
2111
  resolution?: Record<string, string[]>;
2112
+ /**
2113
+ * Lens gating flag for vendor providers. When `true`, this Provider's
2114
+ * `classify()` only runs (and the walker only iterates its territory)
2115
+ * if `provider.id === activeProvider` (the project's active lens).
2116
+ * When `false` or omitted (default), the Provider is universal and
2117
+ * classifies unconditionally, regardless of the active lens.
2118
+ *
2119
+ * Vendor providers (`claude`, `openai`, `antigravity`) MUST set this
2120
+ * to `true`: the actual runtimes never read each other's on-disk
2121
+ * formats (Claude Code does not consume `.codex/`; Codex CLI does not
2122
+ * consume `.claude/`), and offering every file to every provider
2123
+ * fabricates cross-vendor graph edges the runtimes themselves reject.
2124
+ *
2125
+ * Universal providers (the open-standard `agent-skills`, the markdown
2126
+ * fallback `core/markdown`, any future format-based fallback) keep
2127
+ * this `false`: their territory is consumed by every vendor and they
2128
+ * MUST run on every scan, regardless of the active lens.
2129
+ *
2130
+ * When `activeProvider === null` (no lens resolved, e.g. a project
2131
+ * with no provider markers), the walker bypasses the gate entirely
2132
+ * and every gated Provider runs, mirroring the permissive
2133
+ * extractor-side fallback for unlensed projects.
2134
+ *
2135
+ * Default `undefined` ≡ `false` ≡ universal. The field affects
2136
+ * classification ONLY; extractors continue to filter via their own
2137
+ * `precondition.provider` allowlist and are unaffected by this flag.
2138
+ */
2139
+ gatedByActiveLens?: boolean;
1972
2140
  /**
1973
2141
  * Reserved invocation names this Provider's runtime owns for each
1974
2142
  * kind. Maps a `node.kind` to the set of normalised names the runtime
@@ -2004,6 +2172,44 @@ interface IProvider extends IExtensionBase {
2004
2172
  * consumes.
2005
2173
  */
2006
2174
  reservedNames?: Record<string, readonly string[]>;
2175
+ /**
2176
+ * Per-Provider ranking hints consumed by the Signal IR **resolver
2177
+ * phase** (kernel `resolveSignals`). Drives intra-Signal candidate
2178
+ * selection AND cross-Signal range-overlap tiebreaks.
2179
+ *
2180
+ * Optional, when absent the resolver uses the default tiebreak chain:
2181
+ * `confidence` DESC → `range` length DESC → extractor declaration
2182
+ * order. Most Providers do not need to declare this; the default chain
2183
+ * is correct unless the Provider has a kind-specific preference (e.g.
2184
+ * "treat `invokes` edges as more important than `mentions` of the
2185
+ * same range").
2186
+ *
2187
+ * Distinct from the `resolution` field above: `resolverRules` ranks
2188
+ * candidates INSIDE the Signal IR (the candidate that becomes a Link
2189
+ * in the first place); `resolution` ranks Links AFTER they exist
2190
+ * (confidence lift on already-emitted edges). The two surfaces share
2191
+ * no mechanism and intentionally do not compose.
2192
+ */
2193
+ resolverRules?: IResolverRules;
2194
+ }
2195
+ /**
2196
+ * Per-Provider Signal IR resolver ranking hints. Mirrors
2197
+ * `extensions/provider.schema.json#/properties/resolverRules`.
2198
+ */
2199
+ interface IResolverRules {
2200
+ /**
2201
+ * When present, the resolver ranks candidates whose `kind` appears
2202
+ * earlier in this array ABOVE candidates whose `kind` appears later.
2203
+ * Candidates whose `kind` is absent from the array drop to the end
2204
+ * (after every listed kind). Ties inside the same `kindPriority`
2205
+ * bucket fall through to the `confidence` → range-length → declaration
2206
+ * order tiebreaks.
2207
+ *
2208
+ * Example: a Provider that wants `invokes` edges to win against
2209
+ * `mentions` / `references` of the same byte range declares
2210
+ * `['invokes', 'references', 'mentions']`.
2211
+ */
2212
+ kindPriority?: readonly LinkKind[];
2007
2213
  }
2008
2214
  /**
2009
2215
  * Declarative read config a Provider declares via `IProvider.read`.
@@ -4092,4 +4298,4 @@ interface Kernel {
4092
4298
  }
4093
4299
  declare function createKernel(): Kernel;
4094
4300
 
4095
- export { type Confidence, DuplicateExtensionError, EXTENSION_KINDS, type ExecutionFailureReason, type ExecutionKind, type ExecutionRecord, type ExecutionRunner, type ExecutionStatus, ExportQueryError, type Extension, type ExtensionKind, type FilesystemPort, HOOK_TRIGGERS, type HistoryStats, type HistoryStatsErrorRates, type HistoryStatsExecutionsPerPeriod, type HistoryStatsPerActionRate, type HistoryStatsTokensPerAction, type HistoryStatsTopNode, type HistoryStatsTotals, type IAction, type IActionContext, type IActionPrecondition, type IActionResult, type IAnalyzer, type IAnalyzerContext, type IAnnotationContribution, type ICreateFsWatcherOptions, type IDedicatedStorePersist, type IDedicatedStoreWrapper, type IDiscoveredPlugin, type IEnrichmentRecord, type IExportQuery, type IExportSubset, type IExtensionBase, type IExtractor, type IExtractorCallbacks, type IExtractorContext, type IExtractorRunRecord, type IFormatter, type IFormatterContext, type IFsWatcher, type IHook, type IHookContext, type IHookDispatcher, type IIssueRow, type IKvStorePersist, type IKvStoreWrapper, type ILoadedExtension, type INodeBundle, type INodeChange, type INodeCounts, type INodeFilter, type IPersistOptions, type IPersistedEnrichment, type IPluginManifest, type IPluginStorageSchema, type IPluginStore, type IProvider, type IRawNode, type IRegisteredAnnotationKey, type IRegisteredViewContribution, type IRunOptions, type IRunResult, type IScanDelta, type ISettingDeclaration, type ITransactionalStorage, type IViewContribution, type IWalkOptions, type IWatchBatch, type IWatchEvent, InMemoryProgressEmitter, type Issue, type IssueFix, KV_SCHEMA_KEY, type Kernel, LOG_LEVELS, type Link, type LinkKind, type LinkLocation, type LinkTrigger, type LogRecord, type LoggerPort, type Node, type NodeKind, type NodeStat, type PluginLoaderPort, type ProgressEmitterPort, type ProgressEvent, Registry, type RenameOp, type RunScanOptions, type RunnerPort, type ScanResult, type ScanScannedBy, type ScanStats, type Severity, SilentLogger, type Stability, type StoragePort, type TActionWrite, type TExecutionMode, type TGranularity, type THookFilter, type THookTrigger, type TInputTypeName, type TLogLevel, type TLogMethodLevel, type TNodeChangeReason, type TPluginLoadStatus, type TPluginStorage, type TProgressListener, type TSettingValue, type TSeverity, type TSlotName, type TWatchEventKind, type TripleSplit, applyExportQuery, computeScanDelta, configureLogger, createChokidarWatcher, createKernel, detectRenamesAndOrphans, getActiveLogger, isEmptyDelta, isLogLevel, log, logLevelRank, makeDedicatedStoreWrapper, makeEvent, makeHookDispatcher, makeKvStoreWrapper, makePluginStore, mergeNodeWithEnrichments, parseExportQuery, parseLogLevel, qualifiedExtensionId, resetLogger, runExtractorsForNode, runScan, runScanWithRenames };
4301
+ export { type Confidence, DuplicateExtensionError, EXTENSION_KINDS, type ExecutionFailureReason, type ExecutionKind, type ExecutionRecord, type ExecutionRunner, type ExecutionStatus, ExportQueryError, type Extension, type ExtensionKind, type FilesystemPort, HOOK_TRIGGERS, type HistoryStats, type HistoryStatsErrorRates, type HistoryStatsExecutionsPerPeriod, type HistoryStatsPerActionRate, type HistoryStatsTokensPerAction, type HistoryStatsTopNode, type HistoryStatsTotals, type IAction, type IActionContext, type IActionPrecondition, type IActionResult, type IAnalyzer, type IAnalyzerContext, type IAnnotationContribution, type ICreateFsWatcherOptions, type IDedicatedStorePersist, type IDedicatedStoreWrapper, type IDiscoveredPlugin, type IEnrichmentRecord, type IExportQuery, type IExportSubset, type IExtensionBase, type IExternalRef, type IExtractor, type IExtractorCallbacks, type IExtractorContext, type IExtractorRunRecord, type IFormatter, type IFormatterContext, type IFsWatcher, type IHook, type IHookContext, type IHookDispatcher, type IIssueRow, type IKvStorePersist, type IKvStoreWrapper, type ILoadedExtension, type INodeBundle, type INodeChange, type INodeCounts, type INodeFilter, type IPersistOptions, type IPersistedEnrichment, type IPluginManifest, type IPluginStorageSchema, type IPluginStore, type IProvider, type IRawNode, type IRegisteredAnnotationKey, type IRegisteredViewContribution, type IRunOptions, type IRunResult, type IScanDelta, type ISettingDeclaration, type ITransactionalStorage, type IViewContribution, type IWalkOptions, type IWatchBatch, type IWatchEvent, InMemoryProgressEmitter, type Issue, type IssueFix, KV_SCHEMA_KEY, type Kernel, LOG_LEVELS, type Link, type LinkKind, type LinkLocation, type LinkOccurrence, type LinkTrigger, type LogRecord, type LoggerPort, type Node, type NodeKind, type NodeStat, type PluginLoaderPort, type ProgressEmitterPort, type ProgressEvent, Registry, type RenameOp, type RunScanOptions, type RunnerPort, type ScanResult, type ScanScannedBy, type ScanStats, type Severity, SilentLogger, type Stability, type StoragePort, type TActionWrite, type TExecutionMode, type TGranularity, type THookFilter, type THookTrigger, type TInputTypeName, type TLogLevel, type TLogMethodLevel, type TNodeChangeReason, type TPluginLoadStatus, type TPluginStorage, type TProgressListener, type TSettingValue, type TSeverity, type TSlotName, type TWatchEventKind, type TripleSplit, applyExportQuery, computeScanDelta, configureLogger, createChokidarWatcher, createKernel, detectRenamesAndOrphans, getActiveLogger, isEmptyDelta, isLogLevel, log, logLevelRank, makeDedicatedStoreWrapper, makeEvent, makeHookDispatcher, makeKvStoreWrapper, makePluginStore, mergeNodeWithEnrichments, parseExportQuery, parseLogLevel, qualifiedExtensionId, resetLogger, runExtractorsForNode, runScan, runScanWithRenames };
@@ -101,7 +101,7 @@ import cl100k_base from "js-tiktoken/ranks/cl100k_base";
101
101
  // package.json
102
102
  var package_default = {
103
103
  name: "@skill-map/cli",
104
- version: "0.36.0",
104
+ version: "0.37.0",
105
105
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
106
106
  license: "MIT",
107
107
  type: "module",
@@ -197,7 +197,7 @@ var package_default = {
197
197
  eslint: "10.2.1",
198
198
  "eslint-plugin-import-x": "4.16.2",
199
199
  tsup: "8.5.1",
200
- tsx: "4.21.0",
200
+ tsx: "4.22.3",
201
201
  typescript: "5.9.3",
202
202
  "typescript-eslint": "8.59.1"
203
203
  },
@@ -1396,6 +1396,20 @@ function dedupeLinks(links) {
1396
1396
  if (link.confidence > existing.confidence) {
1397
1397
  existing.confidence = link.confidence;
1398
1398
  }
1399
+ if (link.occurrences && link.occurrences.length > 0) {
1400
+ const existingOccurrences = existing.occurrences ?? [];
1401
+ const occKey = (o) => `${o.extractor}\0${o.originalTrigger}\0${o.location?.line ?? -1}`;
1402
+ const occSeen = new Set(existingOccurrences.map(occKey));
1403
+ const merged = [...existingOccurrences];
1404
+ for (const occ of link.occurrences) {
1405
+ const k = occKey(occ);
1406
+ if (!occSeen.has(k)) {
1407
+ occSeen.add(k);
1408
+ merged.push(occ);
1409
+ }
1410
+ }
1411
+ existing.occurrences = merged;
1412
+ }
1399
1413
  continue;
1400
1414
  }
1401
1415
  out.set(key, link);
@@ -1419,12 +1433,23 @@ function recomputeLinkCounts(nodes, links) {
1419
1433
  function recomputeExternalRefsCount(nodes, externalLinks, cachedPaths) {
1420
1434
  const byPath2 = /* @__PURE__ */ new Map();
1421
1435
  for (const node of nodes) {
1422
- if (!cachedPaths.has(node.path)) node.externalRefsCount = 0;
1436
+ if (!cachedPaths.has(node.path)) {
1437
+ node.externalRefsCount = 0;
1438
+ delete node.externalRefs;
1439
+ }
1423
1440
  byPath2.set(node.path, node);
1424
1441
  }
1425
1442
  for (const link of externalLinks) {
1426
1443
  const source = byPath2.get(link.source);
1427
- if (source && !cachedPaths.has(source.path)) source.externalRefsCount += 1;
1444
+ if (!source || cachedPaths.has(source.path)) continue;
1445
+ source.externalRefsCount += 1;
1446
+ const refs = source.externalRefs ?? [];
1447
+ refs.push({
1448
+ url: link.target,
1449
+ ...link.location?.line !== void 0 ? { line: link.location.line } : {},
1450
+ ...link.trigger?.originalTrigger ? { originalTrigger: link.trigger.originalTrigger } : {}
1451
+ });
1452
+ source.externalRefs = refs;
1428
1453
  }
1429
1454
  }
1430
1455
  var EXTERNAL_URL_SCHEME_RE = /^[a-z][a-z0-9+\-.]+:/i;
@@ -1435,7 +1460,7 @@ function isExternalUrlLink(link) {
1435
1460
  }
1436
1461
 
1437
1462
  // kernel/orchestrator/analyzers.ts
1438
- async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher, reservedNodePaths) {
1463
+ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher, reservedNodePaths, signals) {
1439
1464
  const issues = [];
1440
1465
  const contributions = [];
1441
1466
  const validators = loadSchemaValidators();
@@ -1500,6 +1525,7 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
1500
1525
  ...referenceablePaths ? { referenceablePaths } : {},
1501
1526
  ...cwd ? { cwd } : {},
1502
1527
  ...reservedNodePaths ? { reservedNodePaths } : {},
1528
+ ...signals && signals.length > 0 ? { signals } : {},
1503
1529
  emitContribution
1504
1530
  });
1505
1531
  for (const issue of emitted) {
@@ -1780,6 +1806,7 @@ function liftResolvedLinkConfidence(links, nodes, ctx) {
1780
1806
  const resolution = resolve7(link, indexes, ctx);
1781
1807
  if (resolution === "none") continue;
1782
1808
  link.confidence = ctx.reservedNodePaths.has(resolution) ? RESERVED_TARGET_CONFIDENCE : 1;
1809
+ link.resolvedTarget = resolution;
1783
1810
  }
1784
1811
  }
1785
1812
  function buildIndexes(nodes, ctx) {
@@ -1860,6 +1887,190 @@ function applyPostWalkTransforms(links, nodes, ctx, transforms = POST_WALK_TRANS
1860
1887
  return current;
1861
1888
  }
1862
1889
 
1890
+ // kernel/orchestrator/resolver.ts
1891
+ function resolveSignals(opts) {
1892
+ const kindPriority = opts.activeProvider?.resolverRules?.kindPriority ?? [];
1893
+ const extractorRank = buildExtractorRank(opts.extractorOrder);
1894
+ for (const signal of opts.signals) {
1895
+ signal.resolution = pickIntraWinner(signal, kindPriority, extractorRank);
1896
+ }
1897
+ const bySource = /* @__PURE__ */ new Map();
1898
+ for (const signal of opts.signals) {
1899
+ if (signal.resolution?.outcome !== "materialised") continue;
1900
+ const bucket = bySource.get(signal.source) ?? [];
1901
+ bucket.push(signal);
1902
+ bySource.set(signal.source, bucket);
1903
+ }
1904
+ for (const [, signals] of bySource) {
1905
+ const clusters = buildOverlapClusters(signals);
1906
+ for (const cluster of clusters) {
1907
+ if (cluster.length < 2) continue;
1908
+ if (everyCandidateIsExternalUrl(cluster)) continue;
1909
+ resolveCluster(cluster, kindPriority, extractorRank);
1910
+ }
1911
+ }
1912
+ const links = [];
1913
+ for (const signal of opts.signals) {
1914
+ if (signal.resolution?.outcome !== "materialised") continue;
1915
+ const winnerIndex = signal.resolution.winnerIndex ?? 0;
1916
+ const winner = signal.candidates[winnerIndex];
1917
+ if (!winner) continue;
1918
+ links.push(materialise(signal, winner));
1919
+ }
1920
+ return { links, resolvedSignals: [...opts.signals] };
1921
+ }
1922
+ function buildExtractorRank(order) {
1923
+ const rank = /* @__PURE__ */ new Map();
1924
+ for (let i = 0; i < order.length; i += 1) rank.set(order[i], i);
1925
+ return rank;
1926
+ }
1927
+ function pickIntraWinner(signal, kindPriority, extractorRank) {
1928
+ let bestIndex = 0;
1929
+ for (let i = 1; i < signal.candidates.length; i += 1) {
1930
+ if (compareCandidates(signal.candidates[i], signal.candidates[bestIndex], kindPriority, extractorRank) < 0) {
1931
+ bestIndex = i;
1932
+ }
1933
+ }
1934
+ return { outcome: "materialised", winnerIndex: bestIndex };
1935
+ }
1936
+ function compareCandidates(a, b, kindPriority, extractorRank) {
1937
+ const kindCmp = compareKindPriority(a.kind, b.kind, kindPriority);
1938
+ if (kindCmp !== 0) return kindCmp;
1939
+ if (a.confidence !== b.confidence) return b.confidence - a.confidence;
1940
+ const aRank = extractorRank.get(a.extractorId) ?? Number.MAX_SAFE_INTEGER;
1941
+ const bRank = extractorRank.get(b.extractorId) ?? Number.MAX_SAFE_INTEGER;
1942
+ return aRank - bRank;
1943
+ }
1944
+ function compareKindPriority(aKind, bKind, kindPriority) {
1945
+ if (kindPriority.length === 0) return 0;
1946
+ const aIdx = kindPriority.indexOf(aKind);
1947
+ const bIdx = kindPriority.indexOf(bKind);
1948
+ const aEff = aIdx === -1 ? Number.MAX_SAFE_INTEGER : aIdx;
1949
+ const bEff = bIdx === -1 ? Number.MAX_SAFE_INTEGER : bIdx;
1950
+ return aEff - bEff;
1951
+ }
1952
+ function buildOverlapClusters(signals) {
1953
+ const bodySignals = signals.filter((s) => s.scope === "body" && s.range);
1954
+ const others = signals.filter((s) => !(s.scope === "body" && s.range));
1955
+ const parent = bodySignals.map((_, i) => i);
1956
+ const find = (i) => {
1957
+ while (parent[i] !== i) {
1958
+ parent[i] = parent[parent[i]];
1959
+ i = parent[i];
1960
+ }
1961
+ return i;
1962
+ };
1963
+ const union = (a, b) => {
1964
+ const ra = find(a);
1965
+ const rb = find(b);
1966
+ if (ra !== rb) parent[ra] = rb;
1967
+ };
1968
+ for (let i = 0; i < bodySignals.length; i += 1) {
1969
+ for (let j = i + 1; j < bodySignals.length; j += 1) {
1970
+ if (rangesOverlap(bodySignals[i].range, bodySignals[j].range)) {
1971
+ union(i, j);
1972
+ }
1973
+ }
1974
+ }
1975
+ const byRoot = /* @__PURE__ */ new Map();
1976
+ for (let i = 0; i < bodySignals.length; i += 1) {
1977
+ const root = find(i);
1978
+ const bucket = byRoot.get(root) ?? [];
1979
+ bucket.push(bodySignals[i]);
1980
+ byRoot.set(root, bucket);
1981
+ }
1982
+ return [...byRoot.values(), ...others.map((s) => [s])];
1983
+ }
1984
+ function rangesOverlap(a, b) {
1985
+ return a.end > b.start && b.end > a.start;
1986
+ }
1987
+ function everyCandidateIsExternalUrl(cluster) {
1988
+ for (const signal of cluster) {
1989
+ const winnerIndex = signal.resolution?.winnerIndex ?? 0;
1990
+ const winner = signal.candidates[winnerIndex];
1991
+ if (!winner) return false;
1992
+ if (!isExternalUrl(winner.target)) return false;
1993
+ }
1994
+ return true;
1995
+ }
1996
+ function isExternalUrl(target) {
1997
+ return target.startsWith("http://") || target.startsWith("https://");
1998
+ }
1999
+ function resolveCluster(cluster, kindPriority, extractorRank) {
2000
+ let winnerIdx = 0;
2001
+ for (let i = 1; i < cluster.length; i += 1) {
2002
+ const result = compareClusterMembers(cluster[i], cluster[winnerIdx], kindPriority, extractorRank);
2003
+ if (result.winner === "challenger") winnerIdx = i;
2004
+ }
2005
+ const winner = cluster[winnerIdx];
2006
+ const winnerCandidate = winner.candidates[winner.resolution?.winnerIndex ?? 0];
2007
+ for (let i = 0; i < cluster.length; i += 1) {
2008
+ if (i === winnerIdx) continue;
2009
+ const loser = cluster[i];
2010
+ loser.resolution = {
2011
+ outcome: "rejected",
2012
+ rejectedBy: {
2013
+ source: winner.source,
2014
+ range: winner.range,
2015
+ extractorId: winnerCandidate.extractorId,
2016
+ reason: whyLost(loser, winner, kindPriority, extractorRank)
2017
+ }
2018
+ };
2019
+ }
2020
+ }
2021
+ function compareClusterMembers(challenger, champion, kindPriority, extractorRank) {
2022
+ const cWinner = challenger.candidates[challenger.resolution?.winnerIndex ?? 0];
2023
+ const xWinner = champion.candidates[champion.resolution?.winnerIndex ?? 0];
2024
+ const kindCmp = compareKindPriority(cWinner.kind, xWinner.kind, kindPriority);
2025
+ if (kindCmp < 0) return { winner: "challenger", reason: "kind-priority" };
2026
+ if (kindCmp > 0) return { winner: "champion", reason: "kind-priority" };
2027
+ if (cWinner.confidence !== xWinner.confidence) {
2028
+ return cWinner.confidence > xWinner.confidence ? { winner: "challenger", reason: "higher-confidence" } : { winner: "champion", reason: "higher-confidence" };
2029
+ }
2030
+ const cLen = rangeLength(challenger.range);
2031
+ const xLen = rangeLength(champion.range);
2032
+ if (cLen !== xLen) {
2033
+ return cLen > xLen ? { winner: "challenger", reason: "longer-range" } : { winner: "champion", reason: "longer-range" };
2034
+ }
2035
+ const cRank = extractorRank.get(cWinner.extractorId) ?? Number.MAX_SAFE_INTEGER;
2036
+ const xRank = extractorRank.get(xWinner.extractorId) ?? Number.MAX_SAFE_INTEGER;
2037
+ if (cRank !== xRank) {
2038
+ return cRank < xRank ? { winner: "challenger", reason: "earlier-declaration" } : { winner: "champion", reason: "earlier-declaration" };
2039
+ }
2040
+ return { winner: "tie", reason: "earlier-declaration" };
2041
+ }
2042
+ function rangeLength(range) {
2043
+ if (!range) return 0;
2044
+ return range.end - range.start;
2045
+ }
2046
+ function whyLost(loser, winner, kindPriority, extractorRank) {
2047
+ return compareClusterMembers(winner, loser, kindPriority, extractorRank).reason;
2048
+ }
2049
+ function materialise(signal, winner) {
2050
+ const link = {
2051
+ source: signal.source,
2052
+ target: winner.target,
2053
+ kind: winner.kind,
2054
+ confidence: winner.confidence,
2055
+ sources: [winner.extractorId],
2056
+ raw: signal.raw
2057
+ };
2058
+ if (winner.trigger) link.trigger = winner.trigger;
2059
+ if (signal.range) {
2060
+ link.location = { line: signal.range.line ?? 1, offset: signal.range.start };
2061
+ }
2062
+ const occurrenceTrigger = winner.trigger?.originalTrigger ?? signal.raw;
2063
+ const occurrenceLocation = signal.range ? { line: signal.range.line ?? 1 } : null;
2064
+ link.occurrences = [
2065
+ {
2066
+ extractor: winner.extractorId,
2067
+ originalTrigger: occurrenceTrigger,
2068
+ location: occurrenceLocation
2069
+ }
2070
+ ];
2071
+ return link;
2072
+ }
2073
+
1863
2074
  // kernel/orchestrator/renames.ts
1864
2075
  function findHighConfidenceRenames(opts) {
1865
2076
  const ops = [];
@@ -2632,7 +2843,12 @@ async function walkAndExtract(opts) {
2632
2843
  const walkOptions = opts.ignoreFilter ? { ignoreFilter: opts.ignoreFilter } : {};
2633
2844
  let filesWalked = 0;
2634
2845
  let index = 0;
2635
- for (const provider of opts.providers) {
2846
+ const activeProviders = opts.providers.filter((provider) => {
2847
+ if (!provider.gatedByActiveLens) return true;
2848
+ if (opts.activeProvider === null) return true;
2849
+ return provider.id === opts.activeProvider;
2850
+ });
2851
+ for (const provider of activeProviders) {
2636
2852
  for await (const raw of resolveProviderWalk(provider)(opts.roots, walkOptions)) {
2637
2853
  filesWalked += 1;
2638
2854
  if (claimedPaths.has(raw.path)) continue;
@@ -2653,7 +2869,8 @@ async function walkAndExtract(opts) {
2653
2869
  contributions: accum.contributionsBuffer,
2654
2870
  freshlyRunTuples: accum.freshlyRunTuples,
2655
2871
  orphanSidecars,
2656
- sidecarRoots: accum.sidecarRoots
2872
+ sidecarRoots: accum.sidecarRoots,
2873
+ signals: accum.signals
2657
2874
  };
2658
2875
  }
2659
2876
  function createWalkAccumulators() {
@@ -2661,6 +2878,7 @@ function createWalkAccumulators() {
2661
2878
  nodes: [],
2662
2879
  internalLinks: [],
2663
2880
  externalLinks: [],
2881
+ signals: [],
2664
2882
  cachedPaths: /* @__PURE__ */ new Set(),
2665
2883
  frontmatterIssues: [],
2666
2884
  enrichmentBuffer: /* @__PURE__ */ new Map(),
@@ -2806,6 +3024,7 @@ function recordFreshlyRunTuples(extractors, nodePath, accum) {
2806
3024
  function mergeExtractResult(extractResult, accum) {
2807
3025
  for (const link of extractResult.internalLinks) accum.internalLinks.push(link);
2808
3026
  for (const link of extractResult.externalLinks) accum.externalLinks.push(link);
3027
+ for (const signal of extractResult.signals) accum.signals.push(signal);
2809
3028
  for (const enr of extractResult.enrichments) {
2810
3029
  accum.enrichmentBuffer.set(`${enr.nodePath}\0${enr.extractorId}`, enr);
2811
3030
  }
@@ -2908,6 +3127,7 @@ async function runScanInternal(_kernel, options) {
2908
3127
  const scanStartedEvent = makeEvent("scan.started", { roots: options.roots });
2909
3128
  emitter.emit(scanStartedEvent);
2910
3129
  await hookDispatcher.dispatch("scan.started", scanStartedEvent);
3130
+ const activeProviderId = resolveActiveProviderOption(options.activeProvider, options.roots);
2911
3131
  const walked = await walkAndExtract({
2912
3132
  providers: exts.providers,
2913
3133
  extractors: exts.extractors,
@@ -2922,9 +3142,20 @@ async function runScanInternal(_kernel, options) {
2922
3142
  priorExtractorRuns: setup.priorExtractorRuns,
2923
3143
  providerFrontmatter: setup.providerFrontmatter,
2924
3144
  pluginStores: options.pluginStores,
2925
- activeProvider: resolveActiveProviderOption(options.activeProvider, options.roots)
3145
+ activeProvider: activeProviderId
3146
+ });
3147
+ const activeProvider = activeProviderId ? exts.providers.find((p) => p.id === activeProviderId) ?? null : null;
3148
+ const resolved = resolveSignals({
3149
+ signals: walked.signals,
3150
+ activeProvider,
3151
+ extractorOrder: exts.extractors.map((e) => e.id)
2926
3152
  });
2927
- const postWalkCtx = buildPostWalkTransformCtx(_kernel, walked.nodes);
3153
+ for (const link of resolved.links) {
3154
+ if (isExternalUrlLink(link)) walked.externalLinks.push(link);
3155
+ else walked.internalLinks.push(link);
3156
+ }
3157
+ walked.signals = resolved.resolvedSignals;
3158
+ const postWalkCtx = buildPostWalkTransformCtx(exts.providers, walked.nodes);
2928
3159
  walked.internalLinks = applyPostWalkTransforms(walked.internalLinks, walked.nodes, postWalkCtx);
2929
3160
  recomputeLinkCounts(walked.nodes, walked.internalLinks);
2930
3161
  recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);
@@ -2946,7 +3177,8 @@ async function runScanInternal(_kernel, options) {
2946
3177
  registeredActionIds,
2947
3178
  emitter,
2948
3179
  hookDispatcher,
2949
- postWalkCtx.reservedNodePaths
3180
+ postWalkCtx.reservedNodePaths,
3181
+ walked.signals
2950
3182
  );
2951
3183
  mergeAnalyzerEmissions(walked, analyzerResult, exts.analyzers);
2952
3184
  const issues = analyzerResult.issues;
@@ -2959,8 +3191,8 @@ async function runScanInternal(_kernel, options) {
2959
3191
  await hookDispatcher.dispatch("scan.completed", scanCompletedEvent);
2960
3192
  return buildScanReturn(walked, issues, renameOps, stats, options, setup);
2961
3193
  }
2962
- function buildPostWalkTransformCtx(kernel, nodes) {
2963
- const { kindRegistry, providerResolution, reservedNamesByProviderKind } = buildProviderIndexes(kernel);
3194
+ function buildPostWalkTransformCtx(providers, nodes) {
3195
+ const { kindRegistry, providerResolution, reservedNamesByProviderKind } = buildProviderIndexes(providers);
2964
3196
  const reservedNodePaths = buildReservedNodePaths(
2965
3197
  nodes,
2966
3198
  kindRegistry,
@@ -2968,11 +3200,10 @@ function buildPostWalkTransformCtx(kernel, nodes) {
2968
3200
  );
2969
3201
  return { kindRegistry, providerResolution, reservedNodePaths };
2970
3202
  }
2971
- function buildProviderIndexes(kernel) {
3203
+ function buildProviderIndexes(providers) {
2972
3204
  const kindRegistry = /* @__PURE__ */ new Map();
2973
3205
  const providerResolution = /* @__PURE__ */ new Map();
2974
3206
  const reservedNamesByProviderKind = /* @__PURE__ */ new Map();
2975
- const providers = kernel.registry.all("provider");
2976
3207
  for (const provider of providers) {
2977
3208
  if (provider.kinds) {
2978
3209
  for (const [kindName, descriptor] of Object.entries(provider.kinds)) {