@skill-map/cli 0.31.0 → 0.33.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.
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * **Spec § A.6, qualified ids.** Every extension is keyed in the registry
10
10
  * by `<pluginId>/<id>` (e.g. `core/annotations`, `core/slash`,
11
- * `hello-world/greet`). `Extension.id` carries the **short** id as authored;
11
+ * `my-plugin/my-extractor`). `Extension.id` carries the **short** id as authored;
12
12
  * `Extension.pluginId` carries the namespace; the registry composes the
13
13
  * qualifier internally and exposes lookup APIs that operate on either form
14
14
  * (qualified for direct lookup, kind-scoped listing for enumeration).
@@ -501,7 +501,26 @@ interface IExtensionBase {
501
501
  */
502
502
  type NodeKind = 'skill' | 'agent' | 'command' | 'markdown';
503
503
  type LinkKind = 'invokes' | 'references' | 'mentions' | 'supersedes';
504
- type Confidence = 'high' | 'medium' | 'low';
504
+ /**
505
+ * Extractor's self-assessed confidence, normalized to `[0..1]`. Drives
506
+ * UI edge opacity in the graph view (more confident = more opaque edge).
507
+ * Migrated from the legacy `'high' | 'medium' | 'low'` string union to
508
+ * a numeric range so callers can express finer granularity than three
509
+ * buckets. The named tiers below (`ConfidenceTier`) preserve the
510
+ * legacy buckets as constants for callers that prefer bucket-thinking.
511
+ *
512
+ * Reference scoring (guideline, not contract):
513
+ *
514
+ * `1.0` structured input (sidecar `supersedes`)
515
+ * `0.95` unambiguous syntax (`[text](file.md)`, `https://…`)
516
+ * `0.85` strong signal with one inference (`@file.md`)
517
+ * `0.5` genuine ambiguity (`@bare-handle`)
518
+ *
519
+ * Validation: the orchestrator's `validateLink` rejects values outside
520
+ * `[0..1]` with an `extension.error` event, mirroring the LinkKind
521
+ * enum check. Missing confidence defaults to `ConfidenceTier.MEDIUM`.
522
+ */
523
+ type Confidence = number;
505
524
  type Severity = 'error' | 'warn' | 'info';
506
525
  type Stability = 'experimental' | 'stable' | 'deprecated';
507
526
  /**
@@ -569,6 +588,25 @@ interface Node {
569
588
  * truthy `isFavorite` only ever lands when the BFF set it.
570
589
  */
571
590
  isFavorite?: boolean;
591
+ /**
592
+ * When `true`, the node is synthetic / derived: it does not correspond
593
+ * to a single file on disk. Reconstructed on every scan from the
594
+ * file(s) listed in `derivedFrom`. Synthetic nodes use a non-filesystem
595
+ * path scheme (e.g. `mcp://github`) so the identifier is stable and
596
+ * visibly non-physical. See
597
+ * [`node.schema.json`](../../spec/schemas/node.schema.json) for the
598
+ * normative contract. Absent / `false` for ordinary filesystem-backed
599
+ * entities. Stability: experimental.
600
+ */
601
+ virtual?: boolean;
602
+ /**
603
+ * Paths of the source files this node was derived from. Required (and
604
+ * only meaningful) when `virtual === true`. Drives invalidation: any
605
+ * change to a listed source between scans propagates into the virtual
606
+ * node's hashes. Empty / absent when the node is a regular filesystem
607
+ * entity (the `path` itself is the source).
608
+ */
609
+ derivedFrom?: string[];
572
610
  }
573
611
  /**
574
612
  * Drift status of a co-located `.sm` sidecar relative to the live
@@ -621,6 +659,83 @@ interface Link {
621
659
  location?: LinkLocation | null;
622
660
  raw?: string | null;
623
661
  }
662
+ /**
663
+ * Scope of a `Signal` within its originating node. Mirrors
664
+ * `signal.schema.json#/properties/scope`.
665
+ *
666
+ * - `body` = markdown body or equivalent prose payload.
667
+ * - `frontmatter` = parsed metadata block at the top of the file.
668
+ * - `sidecar` = co-located `.sm` overlay.
669
+ */
670
+ type SignalScope = 'body' | 'frontmatter' | 'sidecar';
671
+ /**
672
+ * Surface context for a body-scope `Signal`. Mirrors
673
+ * `signal.schema.json#/properties/context/enum`. Null when the signal is in
674
+ * normal prose or when the context concept does not apply (frontmatter /
675
+ * sidecar scopes).
676
+ */
677
+ type SignalContext = 'code-block' | 'inline-code' | 'escaped';
678
+ /**
679
+ * Byte-range location for a body-scope `Signal`. `start` is inclusive,
680
+ * `end` is exclusive (one past the last char).
681
+ */
682
+ interface SignalRange {
683
+ start: number;
684
+ end: number;
685
+ }
686
+ /**
687
+ * One alternative interpretation of a `Signal`. The resolver picks the
688
+ * winning candidate per Signal and materialises it as a `Link`; the
689
+ * rejected candidates remain on `IAnalyzerContext.signals` for
690
+ * collision-detection and conflict-visualisation analyzers.
691
+ *
692
+ * `confidence` is numeric `[0..1]`, identical shape to the `Link`'s
693
+ * `Confidence` type after the Phase 4 migration. No conversion needed
694
+ * when the resolver materialises a winning candidate.
695
+ */
696
+ interface SignalCandidate {
697
+ extractorId: string;
698
+ kind: LinkKind;
699
+ target: string;
700
+ /** `[0..1]`. Reference scoring guideline lives in `signal.schema.json`. */
701
+ confidence: number;
702
+ rationale?: string;
703
+ trigger?: LinkTrigger | null;
704
+ }
705
+ /**
706
+ * Intermediate Representation (IR) emitted by extractors via
707
+ * `ctx.emitSignal(signal)`. The kernel's resolver phase consumes
708
+ * `Signal[]` and produces final `Link[]` per the active Provider's
709
+ * `resolverRules`. Opt-in: extractors with unambiguous detections keep
710
+ * using `ctx.emitLink(link)` directly. See
711
+ * [`signal.schema.json`](../../spec/schemas/signal.schema.json) for the
712
+ * normative contract.
713
+ */
714
+ interface Signal {
715
+ /** `node.path` of the originating node. */
716
+ source: string;
717
+ scope: SignalScope;
718
+ /**
719
+ * Byte-range location within the source. Required for `scope: 'body'`,
720
+ * optional otherwise. Powers collision detection between extractors
721
+ * (overlapping ranges) and code-block awareness (the orchestrator can
722
+ * mark ranges that fall inside code spans).
723
+ */
724
+ range?: SignalRange | null;
725
+ /**
726
+ * Structured-data location for `frontmatter` / `sidecar` scopes. Each
727
+ * entry is a step of the path: object keys are strings, array indices
728
+ * are integers serialised as strings. Example: `['tools', '0']`. Null
729
+ * for body scope or when the extractor does not track field locations.
730
+ */
731
+ fieldPath?: string[] | null;
732
+ /** Verbatim matched text (body) or stringified value (frontmatter / sidecar). */
733
+ raw: string;
734
+ /** Surface context. Null when in normal prose or when not applicable. */
735
+ context?: SignalContext | null;
736
+ /** One or more alternative interpretations. At least one. */
737
+ candidates: SignalCandidate[];
738
+ }
624
739
  interface IssueFix {
625
740
  summary?: string;
626
741
  autofixable?: boolean;
@@ -1828,6 +1943,36 @@ interface IProviderReadConfig {
1828
1943
  * Confidence is per-emit (no manifest-level default).
1829
1944
  */
1830
1945
 
1946
+ /**
1947
+ * Payload accepted by `IExtractorCallbacks.emitNode`. A loose subset of
1948
+ * `Node` because the kernel fills the rest from the emission context:
1949
+ *
1950
+ * - `bodyHash`, `frontmatterHash` are computed from `derivedFrom` (the
1951
+ * hash of the sources concatenated in declared order, so the
1952
+ * virtual node's hashes drift when any source changes).
1953
+ * - `bytes`, `linksOutCount`, `linksInCount`, `externalRefsCount`
1954
+ * default to zero counts on emission; the orchestrator's
1955
+ * post-extraction recompute pass fills them in once links resolve.
1956
+ *
1957
+ * The emitter MUST supply `path` (canonical id), `kind` (registered in
1958
+ * a Provider's catalog), `derivedFrom` (one or more existing-node paths
1959
+ * the virtual node is derived from), and SHOULD supply `frontmatter`
1960
+ * with the metadata the UI / analyzers will surface.
1961
+ */
1962
+ interface IEmittedNode {
1963
+ /** Synthetic identifier. Use a non-filesystem scheme (`mcp://`, etc). */
1964
+ path: string;
1965
+ /** Kind declared in some Provider's `kinds` catalog. */
1966
+ kind: string;
1967
+ /** Required for virtual nodes: paths of the source(s). */
1968
+ derivedFrom: string[];
1969
+ /** Always true on this surface; the kernel mirrors it to `Node.virtual`. */
1970
+ virtual: true;
1971
+ /** Provider id the node is attributed to (e.g. `'claude'`). */
1972
+ provider: string;
1973
+ /** Optional structured metadata the UI / analyzers read. */
1974
+ frontmatter?: Record<string, unknown>;
1975
+ }
1831
1976
  /**
1832
1977
  * Output callbacks supplied by the kernel on the extractor context.
1833
1978
  */
@@ -1839,6 +1984,35 @@ interface IExtractorCallbacks {
1839
1984
  * `extension.error` event.
1840
1985
  */
1841
1986
  emitLink(link: Link): void;
1987
+ /**
1988
+ * Emit a multi-candidate `Signal` for the kernel's resolver phase to
1989
+ * collapse into a single Link (or reject). Use this instead of
1990
+ * `emitLink` when the detection carries genuine ambiguity (multiple
1991
+ * plausible kinds / targets), needs byte-range awareness for
1992
+ * collision detection, or needs numeric confidence with
1993
+ * sub-tier granularity. Unambiguous detectors should keep using
1994
+ * `emitLink` directly. See
1995
+ * [`signal.schema.json`](../../../spec/schemas/signal.schema.json) for the
1996
+ * normative contract. Validated against the same closed kind enum;
1997
+ * off-spec Signals (no candidates, off-enum kind, confidence outside
1998
+ * `[0..1]`) drop silently with an `extension.error` event.
1999
+ */
2000
+ emitSignal(signal: Signal): void;
2001
+ /**
2002
+ * Phase 5, emit a synthetic / virtual `Node` derived from the
2003
+ * scanning context (frontmatter, sidecar, config). Used by the
2004
+ * `core/mcp-tools` extractor to materialise an `mcp://<name>` node
2005
+ * out of a `tools: [mcp__<name>__*]` frontmatter entry, and by the
2006
+ * future Cursor / Codex MCP-config extractors that walk
2007
+ * `.cursor/mcp.json` / `~/.codex/config.toml`. The kernel
2008
+ * deduplicates by `node.path` against the walker's nodes AND across
2009
+ * extractor emissions: the FIRST emission of a given path wins,
2010
+ * subsequent emissions are silently ignored (idempotent semantics so
2011
+ * N skills referencing the same MCP collapse into one node). Emitted
2012
+ * nodes carry `virtual: true` and `derivedFrom: [...]` per
2013
+ * [`node.schema.json`](../../../spec/schemas/node.schema.json).
2014
+ */
2015
+ emitNode(node: IEmittedNode): void;
1842
2016
  /**
1843
2017
  * Merge canonical, kernel-curated properties onto the current node's
1844
2018
  * enrichment layer. The author-supplied frontmatter stays untouched
@@ -2004,6 +2178,18 @@ interface IAnalyzerContext {
2004
2178
  * absolute path when present.
2005
2179
  */
2006
2180
  cwd?: string;
2181
+ /**
2182
+ * Signals emitted by extractors during the scan, before the resolver
2183
+ * collapsed them into `links`. Populated when at least one extractor
2184
+ * opted into the Signal IR path (`ctx.emitSignal` in
2185
+ * `IExtractorCallbacks`). Empty / absent when every extractor used
2186
+ * `emitLink` directly (legacy and unambiguous paths). Treat as
2187
+ * read-only. Analyzers consume this for collision detection
2188
+ * (overlapping `range` from different extractors), fragmentation
2189
+ * detection, and conflict-visualisation; the resolved `links` remain
2190
+ * the source of truth for graph-level analyses.
2191
+ */
2192
+ signals?: readonly Signal[];
2007
2193
  /**
2008
2194
  * Emit a per-node view contribution declared in this analyzer's
2009
2195
  * manifest `viewContributions` map. Sync, void return; the
@@ -2489,6 +2675,8 @@ declare function runExtractorsForNode(opts: {
2489
2675
  externalLinks: Link[];
2490
2676
  enrichments: IEnrichmentRecord[];
2491
2677
  contributions: IContributionRecord[];
2678
+ signals: Signal[];
2679
+ virtualNodes: Node[];
2492
2680
  }>;
2493
2681
 
2494
2682
  /**
@@ -2508,7 +2696,14 @@ declare function runExtractorsForNode(opts: {
2508
2696
  interface RenameOp {
2509
2697
  from: string;
2510
2698
  to: string;
2511
- confidence: 'high' | 'medium';
2699
+ /**
2700
+ * Rename-heuristic confidence as a numeric tier. Body-hash matches
2701
+ * use `ConfidenceTier.HIGH` (`0.9`); frontmatter-hash matches use
2702
+ * `ConfidenceTier.MEDIUM` (`0.6`). Consumers that surface the tier
2703
+ * as a string (e.g. issue analyzerId `auto-rename-<tier>`) call
2704
+ * `renameTierLabel(confidence)` to recover the legacy label.
2705
+ */
2706
+ confidence: number;
2512
2707
  }
2513
2708
  /**
2514
2709
  * Pure rename / orphan classification per `spec/db-schema.md` §Rename
@@ -2524,7 +2719,7 @@ interface RenameOp {
2524
2719
  * 2. **Medium-confidence (1:1)**: of the remaining deletions, pair
2525
2720
  * each with the *unique* unclaimed `newPath` that shares its
2526
2721
  * `frontmatterHash`. Emits `auto-rename-medium` (severity warn)
2527
- * with `data: { from, to, confidence: 'medium' }`.
2722
+ * with `data: { from, to, confidence: ConfidenceTier.MEDIUM }`.
2528
2723
  * 3. **Ambiguous (N:1)**: when a single `newPath` has more than one
2529
2724
  * remaining frontmatter-matching candidate, emit ONE
2530
2725
  * `auto-rename-ambiguous` issue per `newPath`, listing all
@@ -100,7 +100,7 @@ import cl100k_base from "js-tiktoken/ranks/cl100k_base";
100
100
  // package.json
101
101
  var package_default = {
102
102
  name: "@skill-map/cli",
103
- version: "0.31.0",
103
+ version: "0.33.0",
104
104
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
105
105
  license: "MIT",
106
106
  type: "module",
@@ -181,6 +181,7 @@ var package_default = {
181
181
  "js-yaml": "4.1.1",
182
182
  kysely: "0.28.17",
183
183
  semver: "7.7.4",
184
+ "smol-toml": "1.6.1",
184
185
  typanion: "3.14.0",
185
186
  ws: "8.20.0"
186
187
  },
@@ -692,12 +693,22 @@ var ORCHESTRATOR_TEXTS = {
692
693
  runScanRootMissing: "runScan: root path '{{root}}' does not exist or is not a directory"
693
694
  };
694
695
 
696
+ // kernel/types.ts
697
+ var ConfidenceTier = Object.freeze({
698
+ HIGH: 0.9,
699
+ MEDIUM: 0.6,
700
+ LOW: 0.3
701
+ });
702
+
695
703
  // kernel/orchestrator/extractors.ts
696
704
  async function runExtractorsForNode(opts) {
697
705
  const internalLinks = [];
698
706
  const externalLinks = [];
699
707
  const enrichmentBuffer = /* @__PURE__ */ new Map();
700
708
  const contributions = [];
709
+ const signals = [];
710
+ const virtualNodes = [];
711
+ const virtualNodePaths = /* @__PURE__ */ new Set();
701
712
  const validators = loadSchemaValidators();
702
713
  for (const extractor of opts.extractors) {
703
714
  const qualifiedId = qualifiedExtensionId(extractor.pluginId, extractor.id);
@@ -769,6 +780,18 @@ async function runExtractorsForNode(opts) {
769
780
  emittedAt: Date.now()
770
781
  });
771
782
  };
783
+ const emitSignal = (signal) => {
784
+ const validated = validateSignal(extractor, signal, opts.emitter);
785
+ if (!validated) return;
786
+ signals.push(validated);
787
+ };
788
+ const emitNode = (emitted) => {
789
+ if (virtualNodePaths.has(emitted.path)) return;
790
+ const node = buildVirtualNode(extractor, emitted, opts.emitter);
791
+ if (!node) return;
792
+ virtualNodePaths.add(node.path);
793
+ virtualNodes.push(node);
794
+ };
772
795
  const store = opts.pluginStores?.get(extractor.pluginId);
773
796
  const ctx = buildExtractorContext(
774
797
  extractor,
@@ -778,6 +801,8 @@ async function runExtractorsForNode(opts) {
778
801
  emitLink,
779
802
  enrichNode,
780
803
  emitContribution,
804
+ emitSignal,
805
+ emitNode,
781
806
  store
782
807
  );
783
808
  await extractor.extract(ctx);
@@ -786,7 +811,9 @@ async function runExtractorsForNode(opts) {
786
811
  internalLinks,
787
812
  externalLinks,
788
813
  enrichments: Array.from(enrichmentBuffer.values()),
789
- contributions
814
+ contributions,
815
+ signals,
816
+ virtualNodes
790
817
  };
791
818
  }
792
819
  function readDeclaredContributions(extension) {
@@ -811,7 +838,7 @@ function emitExtensionError(emitter, qualifiedId, nodePath, data) {
811
838
  })
812
839
  );
813
840
  }
814
- function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, emitContribution, store) {
841
+ function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, emitContribution, emitSignal, emitNode, store) {
815
842
  const scope = extractor.scope ?? "both";
816
843
  const settings = extractor.resolvedSettings ?? {};
817
844
  return {
@@ -822,9 +849,62 @@ function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enr
822
849
  emitLink,
823
850
  enrichNode,
824
851
  emitContribution,
852
+ emitSignal,
853
+ emitNode,
825
854
  ...store !== void 0 ? { store } : {}
826
855
  };
827
856
  }
857
+ var VIRTUAL_NODE_PLACEHOLDER_HASH = "0".repeat(64);
858
+ function buildVirtualNode(extractor, emitted, emitter) {
859
+ const qualifiedId = qualifiedExtensionId(extractor.pluginId, extractor.id);
860
+ if (typeof emitted.path !== "string" || emitted.path.length === 0) {
861
+ emitter.emit(
862
+ makeEvent("extension.error", {
863
+ kind: "virtual-node-missing-path",
864
+ extensionId: qualifiedId,
865
+ message: `Extractor ${qualifiedId} emitted a virtual node with no path; dropped.`
866
+ })
867
+ );
868
+ return null;
869
+ }
870
+ if (typeof emitted.kind !== "string" || emitted.kind.length === 0) {
871
+ emitter.emit(
872
+ makeEvent("extension.error", {
873
+ kind: "virtual-node-missing-kind",
874
+ extensionId: qualifiedId,
875
+ virtualPath: emitted.path,
876
+ message: `Extractor ${qualifiedId} emitted a virtual node at '${emitted.path}' with no kind; dropped.`
877
+ })
878
+ );
879
+ return null;
880
+ }
881
+ if (!Array.isArray(emitted.derivedFrom) || emitted.derivedFrom.length === 0) {
882
+ emitter.emit(
883
+ makeEvent("extension.error", {
884
+ kind: "virtual-node-missing-derived-from",
885
+ extensionId: qualifiedId,
886
+ virtualPath: emitted.path,
887
+ message: `Extractor ${qualifiedId} emitted a virtual node at '${emitted.path}' with empty derivedFrom; dropped.`
888
+ })
889
+ );
890
+ return null;
891
+ }
892
+ const node = {
893
+ path: emitted.path,
894
+ kind: emitted.kind,
895
+ provider: emitted.provider,
896
+ bodyHash: VIRTUAL_NODE_PLACEHOLDER_HASH,
897
+ frontmatterHash: VIRTUAL_NODE_PLACEHOLDER_HASH,
898
+ bytes: { frontmatter: 0, body: 0, total: 0 },
899
+ linksOutCount: 0,
900
+ linksInCount: 0,
901
+ externalRefsCount: 0,
902
+ virtual: true,
903
+ derivedFrom: [...emitted.derivedFrom]
904
+ };
905
+ if (emitted.frontmatter) node.frontmatter = emitted.frontmatter;
906
+ return node;
907
+ }
828
908
  function validateLink(extractor, link, emitter) {
829
909
  const knownKinds = ["invokes", "references", "mentions", "supersedes"];
830
910
  if (!knownKinds.includes(link.kind)) {
@@ -845,9 +925,68 @@ function validateLink(extractor, link, emitter) {
845
925
  );
846
926
  return null;
847
927
  }
848
- const confidence = link.confidence ?? "medium";
928
+ const c = link.confidence;
929
+ if (c !== void 0 && (typeof c !== "number" || !Number.isFinite(c) || c < 0 || c > 1)) {
930
+ const qualifiedId = `${extractor.pluginId}/${extractor.id}`;
931
+ emitter.emit(
932
+ makeEvent("extension.error", {
933
+ kind: "link-confidence-out-of-range",
934
+ extensionId: qualifiedId,
935
+ confidence: c,
936
+ message: `Extractor ${qualifiedId} emitted a Link with confidence ${String(c)} outside [0..1]; dropped.`
937
+ })
938
+ );
939
+ return null;
940
+ }
941
+ const confidence = c ?? ConfidenceTier.MEDIUM;
849
942
  return { ...link, confidence };
850
943
  }
944
+ var KNOWN_LINK_KINDS = ["invokes", "references", "mentions", "supersedes"];
945
+ function validateSignal(extractor, signal, emitter) {
946
+ const qualifiedId = qualifiedExtensionId(extractor.pluginId, extractor.id);
947
+ if (!Array.isArray(signal.candidates) || signal.candidates.length === 0) {
948
+ emitter.emit(
949
+ makeEvent("extension.error", {
950
+ kind: "signal-no-candidates",
951
+ extensionId: qualifiedId,
952
+ signal: { source: signal.source, scope: signal.scope },
953
+ message: `Extractor ${qualifiedId} emitted a Signal with no candidates; dropped.`
954
+ })
955
+ );
956
+ return null;
957
+ }
958
+ for (const candidate of signal.candidates) {
959
+ if (!isValidSignalCandidate(qualifiedId, candidate, emitter)) return null;
960
+ }
961
+ return signal;
962
+ }
963
+ function isValidSignalCandidate(qualifiedId, candidate, emitter) {
964
+ if (!KNOWN_LINK_KINDS.includes(candidate.kind)) {
965
+ emitter.emit(
966
+ makeEvent("extension.error", {
967
+ kind: "signal-candidate-kind-not-declared",
968
+ extensionId: qualifiedId,
969
+ candidateKind: candidate.kind,
970
+ declaredKinds: KNOWN_LINK_KINDS,
971
+ message: `Extractor ${qualifiedId} emitted a Signal candidate with off-enum kind '${String(candidate.kind)}'; dropped.`
972
+ })
973
+ );
974
+ return false;
975
+ }
976
+ const c = candidate.confidence;
977
+ if (typeof c !== "number" || !Number.isFinite(c) || c < 0 || c > 1) {
978
+ emitter.emit(
979
+ makeEvent("extension.error", {
980
+ kind: "signal-candidate-confidence-out-of-range",
981
+ extensionId: qualifiedId,
982
+ confidence: candidate.confidence,
983
+ message: `Extractor ${qualifiedId} emitted a Signal candidate with confidence ${String(c)} outside [0..1]; dropped.`
984
+ })
985
+ );
986
+ return false;
987
+ }
988
+ return true;
989
+ }
851
990
  function dedupeLinks(links) {
852
991
  const out = /* @__PURE__ */ new Map();
853
992
  for (const link of links) {
@@ -894,7 +1033,9 @@ function recomputeExternalRefsCount(nodes, externalLinks, cachedPaths) {
894
1033
  }
895
1034
  }
896
1035
  var EXTERNAL_URL_SCHEME_RE = /^[a-z][a-z0-9+\-.]+:/i;
1036
+ var VIRTUAL_NODE_SCHEME_RE = /^mcp:\/\//i;
897
1037
  function isExternalUrlLink(link) {
1038
+ if (VIRTUAL_NODE_SCHEME_RE.test(link.target)) return false;
898
1039
  return EXTERNAL_URL_SCHEME_RE.test(link.target);
899
1040
  }
900
1041
 
@@ -1053,13 +1194,9 @@ function originatingNodeOf(link, priorNodePaths) {
1053
1194
  }
1054
1195
  function computeCacheDecision(opts) {
1055
1196
  const applicableExtractors = opts.extractors.filter((ex) => {
1056
- const kinds = ex.precondition?.kind;
1057
- if (!kinds || kinds.length === 0) return true;
1058
- return kinds.some((qualified) => {
1059
- const slashIdx = qualified.indexOf("/");
1060
- const kindOnly = slashIdx === -1 ? qualified : qualified.slice(slashIdx + 1);
1061
- return kindOnly === opts.kind;
1062
- });
1197
+ if (!matchesKindPrecondition(ex, opts.kind)) return false;
1198
+ if (!matchesProviderPrecondition(ex, opts.provider)) return false;
1199
+ return true;
1063
1200
  });
1064
1201
  const applicableQualifiedIds = new Set(
1065
1202
  applicableExtractors.map((ex) => qualifiedExtensionId(ex.pluginId, ex.id))
@@ -1073,6 +1210,20 @@ function computeCacheDecision(opts) {
1073
1210
  fullCacheHit: opts.nodeHashCacheEligible && split.missingExtractors.length === 0
1074
1211
  };
1075
1212
  }
1213
+ function matchesKindPrecondition(ex, kind) {
1214
+ const kinds = ex.precondition?.kind;
1215
+ if (!kinds || kinds.length === 0) return true;
1216
+ return kinds.some((qualified) => {
1217
+ const slashIdx = qualified.indexOf("/");
1218
+ const kindOnly = slashIdx === -1 ? qualified : qualified.slice(slashIdx + 1);
1219
+ return kindOnly === kind;
1220
+ });
1221
+ }
1222
+ function matchesProviderPrecondition(ex, provider) {
1223
+ const providers = ex.precondition?.provider;
1224
+ if (!providers || providers.length === 0) return true;
1225
+ return providers.includes(provider);
1226
+ }
1076
1227
  function splitLegacy(applicableExtractors, applicableQualifiedIds, nodeHashCacheEligible) {
1077
1228
  const cachedQualifiedIds = /* @__PURE__ */ new Set();
1078
1229
  const missingExtractors = [];
@@ -1182,7 +1333,7 @@ function findHighConfidenceRenames(opts) {
1182
1333
  if (opts.claimedNew.has(toPath)) continue;
1183
1334
  const toNode = opts.currentByPath.get(toPath);
1184
1335
  if (toNode.bodyHash === fromNode.bodyHash) {
1185
- ops.push({ from: fromPath, to: toPath, confidence: "high" });
1336
+ ops.push({ from: fromPath, to: toPath, confidence: ConfidenceTier.HIGH });
1186
1337
  opts.claimedDeleted.add(fromPath);
1187
1338
  opts.claimedNew.add(toPath);
1188
1339
  break;
@@ -1217,13 +1368,13 @@ function claimSingletonRenames(opts) {
1217
1368
  const remaining = candidates.filter((p) => !opts.claimedDeleted.has(p));
1218
1369
  if (remaining.length === 1) {
1219
1370
  const fromPath = remaining[0];
1220
- ops.push({ from: fromPath, to: toPath, confidence: "medium" });
1371
+ ops.push({ from: fromPath, to: toPath, confidence: ConfidenceTier.MEDIUM });
1221
1372
  opts.issues.push({
1222
1373
  analyzerId: "auto-rename-medium",
1223
1374
  severity: "warn",
1224
1375
  nodeIds: [toPath],
1225
1376
  message: `Auto-rename (medium confidence): ${fromPath} \u2192 ${toPath}`,
1226
- data: { from: fromPath, to: toPath, confidence: "medium" }
1377
+ data: { from: fromPath, to: toPath, confidence: ConfidenceTier.MEDIUM }
1227
1378
  });
1228
1379
  opts.claimedDeleted.add(fromPath);
1229
1380
  opts.claimedNew.add(toPath);
@@ -1427,10 +1578,45 @@ var plainParser = {
1427
1578
  }
1428
1579
  };
1429
1580
 
1581
+ // plugins/core/parsers/toml/index.ts
1582
+ import { parse as parseToml } from "smol-toml";
1583
+ var tomlParser = {
1584
+ id: "toml",
1585
+ parse(raw, _path) {
1586
+ let parsed = {};
1587
+ const issues = [];
1588
+ try {
1589
+ const doc = parseToml(raw);
1590
+ if (doc && typeof doc === "object" && !Array.isArray(doc)) {
1591
+ parsed = stripPrototypePollution(doc);
1592
+ }
1593
+ } catch (err) {
1594
+ issues.push({
1595
+ code: "frontmatter-parse-error",
1596
+ message: sanitiseParseErrorMessage2(err)
1597
+ });
1598
+ }
1599
+ const out = {
1600
+ frontmatterRaw: raw,
1601
+ frontmatter: parsed,
1602
+ body: ""
1603
+ };
1604
+ if (issues.length > 0) {
1605
+ return { ...out, issues };
1606
+ }
1607
+ return out;
1608
+ }
1609
+ };
1610
+ function sanitiseParseErrorMessage2(err) {
1611
+ const raw = err instanceof Error ? err.message : String(err);
1612
+ return raw.replace(/[-]+/g, " ").replace(/\s+/g, " ").trim();
1613
+ }
1614
+
1430
1615
  // kernel/scan/parsers/index.ts
1431
1616
  var REGISTRY = /* @__PURE__ */ new Map([
1432
1617
  [frontmatterYamlParser.id, frontmatterYamlParser],
1433
- [plainParser.id, plainParser]
1618
+ [plainParser.id, plainParser],
1619
+ [tomlParser.id, tomlParser]
1434
1620
  ]);
1435
1621
  var FROZEN_IDS = new Set(REGISTRY.keys());
1436
1622
  function getParser(id) {
@@ -2043,6 +2229,7 @@ async function processRawNode(raw, provider, wctx, accum, claimedPaths, nextInde
2043
2229
  const cacheDecision = computeCacheDecision({
2044
2230
  extractors: wctx.opts.extractors,
2045
2231
  kind,
2232
+ provider: provider.id,
2046
2233
  nodePath: raw.path,
2047
2234
  bodyHash,
2048
2235
  sidecarAnnotationsHash,
@@ -2145,6 +2332,10 @@ function mergeExtractResult(extractResult, accum) {
2145
2332
  accum.enrichmentBuffer.set(`${enr.nodePath}\0${enr.extractorId}`, enr);
2146
2333
  }
2147
2334
  for (const c of extractResult.contributions) accum.contributionsBuffer.push(c);
2335
+ for (const vn of extractResult.virtualNodes) {
2336
+ if (accum.nodes.some((n) => n.path === vn.path)) continue;
2337
+ accum.nodes.push(vn);
2338
+ }
2148
2339
  }
2149
2340
  function isPartialCacheHit(ctx) {
2150
2341
  return ctx.nodeHashCacheEligible && ctx.cacheDecision.cachedQualifiedIds.size > 0 && ctx.priorNode !== void 0;