@skill-map/cli 0.56.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // cli/entry.ts
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="49fdc2af-2694-5cd0-ac19-33f8d1dffa9e")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="7e4fdbab-f037-5030-b67a-732bb7ee46fd")}catch(e){}}();
4
4
  import { existsSync as existsSync33 } from "fs";
5
5
  import { Builtins, Cli as Cli2 } from "clipanion";
6
6
 
@@ -250,7 +250,7 @@ function bucketByKind(kind, instance, bag) {
250
250
  // package.json
251
251
  var package_default = {
252
252
  name: "@skill-map/cli",
253
- version: "0.56.0",
253
+ version: "0.58.0",
254
254
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
255
255
  license: "MIT",
256
256
  type: "module",
@@ -1442,71 +1442,12 @@ var coreMarkdownProvider = {
1442
1442
  }
1443
1443
  };
1444
1444
 
1445
- // plugins/core/extractors/annotations/index.ts
1446
- var ID4 = "annotations";
1447
- var annotationsExtractor = {
1448
- id: ID4,
1449
- pluginId: CORE_PLUGIN_ID,
1450
- kind: "extractor",
1451
- description: "Turns the `supersedes` and `supersededBy` entries from a node's `.sm` sidecar into arrows between nodes in the graph. Example: `supersededBy: v1-skill.md` in a `.sm` sidecar draws an arrow to `v1-skill.md`.",
1452
- scope: "frontmatter",
1453
- extract(ctx) {
1454
- const sourcePath = ctx.node.path;
1455
- const seen = /* @__PURE__ */ new Set();
1456
- function emit(source, target, fieldPath) {
1457
- const key = `${source} ${target}`;
1458
- if (seen.has(key)) return;
1459
- seen.add(key);
1460
- ctx.emitSignal({
1461
- source,
1462
- scope: "sidecar",
1463
- fieldPath,
1464
- raw: target,
1465
- candidates: [
1466
- {
1467
- extractorId: ID4,
1468
- kind: "supersedes",
1469
- target,
1470
- confidence: 1,
1471
- rationale: "structured sidecar annotation"
1472
- }
1473
- ]
1474
- });
1475
- }
1476
- const ann = pickAnnotations(ctx.node);
1477
- if (ann) processBlock(ann, sourcePath, emit);
1478
- }
1479
- };
1480
- function processBlock(block, sourcePath, emit) {
1481
- const supersedes = stringArray(block["supersedes"]);
1482
- for (let i = 0; i < supersedes.length; i += 1) {
1483
- emit(sourcePath, supersedes[i], ["annotations", "supersedes", String(i)]);
1484
- }
1485
- const supersededBy = block["supersededBy"];
1486
- if (typeof supersededBy === "string" && supersededBy.length > 0) {
1487
- emit(supersededBy, sourcePath, ["annotations", "supersededBy"]);
1488
- }
1489
- }
1490
- function pickAnnotations(node) {
1491
- const sidecar = node.sidecar;
1492
- if (!sidecar || sidecar.present !== true) return null;
1493
- const ann = sidecar.annotations;
1494
- if (ann && typeof ann === "object" && !Array.isArray(ann)) {
1495
- return ann;
1496
- }
1497
- return null;
1498
- }
1499
- function stringArray(value) {
1500
- if (!Array.isArray(value)) return [];
1501
- return value.filter((v) => typeof v === "string" && v.length > 0);
1502
- }
1503
-
1504
1445
  // plugins/core/extractors/backtick-path/index.ts
1505
1446
  import { posix as pathPosix2 } from "path";
1506
- var ID5 = "backtick-path";
1447
+ var ID4 = "backtick-path";
1507
1448
  var PATH_RE = /(?<![\w/:.-])(?:\.{1,2}\/)?[\w][\w.-]*(?:\/[\w.-]+)*\.md\b(?![\w/])/g;
1508
1449
  var backtickPathExtractor = {
1509
- id: ID5,
1450
+ id: ID4,
1510
1451
  pluginId: CORE_PLUGIN_ID,
1511
1452
  kind: "extractor",
1512
1453
  description: "Turns relative .md paths written inside code spans and fenced blocks into arrows between nodes in the graph. Example: a backticked `references/rules.md` path draws an arrow to that file.",
@@ -1531,7 +1472,7 @@ var backtickPathExtractor = {
1531
1472
  raw: original,
1532
1473
  candidates: [
1533
1474
  {
1534
- extractorId: ID5,
1475
+ extractorId: ID4,
1535
1476
  kind: "points",
1536
1477
  target: resolved,
1537
1478
  // 0.85: a strong file signal with one degree of inference,
@@ -1560,7 +1501,7 @@ function resolveTarget(sourceDir, raw) {
1560
1501
  }
1561
1502
 
1562
1503
  // plugins/core/extractors/external-url-counter/index.ts
1563
- var ID6 = "external-url-counter";
1504
+ var ID5 = "external-url-counter";
1564
1505
  var count2 = {
1565
1506
  slot: "card.footer.left",
1566
1507
  icon: "pi-link",
@@ -1580,7 +1521,7 @@ var settings = {
1580
1521
  var URL_RE = /https?:\/\/[^\s<>"'`)\]]+/g;
1581
1522
  var TRAILING_PUNCT = /[.,;:!?]+$/;
1582
1523
  var externalUrlCounterExtractor = {
1583
- id: ID6,
1524
+ id: ID5,
1584
1525
  pluginId: CORE_PLUGIN_ID,
1585
1526
  kind: "extractor",
1586
1527
  description: "Counts the distinct external URLs in a node's body and shows the count on the card. Example: a body linking `https://example.com` and `https://docs.rs` shows a count of 2.",
@@ -1632,7 +1573,7 @@ var externalUrlCounterExtractor = {
1632
1573
  raw: original,
1633
1574
  candidates: [
1634
1575
  {
1635
- extractorId: ID6,
1576
+ extractorId: ID5,
1636
1577
  kind: "references",
1637
1578
  target: normalized.href,
1638
1579
  confidence: 0.3,
@@ -1674,11 +1615,11 @@ function normalizeUrl(raw) {
1674
1615
 
1675
1616
  // plugins/core/extractors/markdown-link/index.ts
1676
1617
  import { posix as pathPosix3 } from "path";
1677
- var ID7 = "markdown-link";
1618
+ var ID6 = "markdown-link";
1678
1619
  var LINK_RE = /(?<!!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
1679
1620
  var URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
1680
1621
  var markdownLinkExtractor = {
1681
- id: ID7,
1622
+ id: ID6,
1682
1623
  pluginId: CORE_PLUGIN_ID,
1683
1624
  kind: "extractor",
1684
1625
  description: "Turns markdown links (`[text](path)`) in a node's body into arrows between nodes in the graph. Example: `[the guide](docs/guide.md)` draws an arrow to `docs/guide.md`.",
@@ -1703,7 +1644,7 @@ var markdownLinkExtractor = {
1703
1644
  raw: match[0],
1704
1645
  candidates: [
1705
1646
  {
1706
- extractorId: ID7,
1647
+ extractorId: ID6,
1707
1648
  kind: "references",
1708
1649
  target: resolved,
1709
1650
  // 0.95: the `[text](path)` syntax is unambiguous (the spec's
@@ -1738,10 +1679,10 @@ function resolveTarget2(sourceDir, raw) {
1738
1679
  }
1739
1680
 
1740
1681
  // plugins/core/extractors/mcp-tools/index.ts
1741
- var ID8 = "mcp-tools";
1682
+ var ID7 = "mcp-tools";
1742
1683
  var MCP_PATTERN = /^mcp__([a-z0-9][a-z0-9_-]*)__[a-z0-9_-]+$/i;
1743
1684
  var mcpToolsExtractor = {
1744
- id: ID8,
1685
+ id: ID7,
1745
1686
  pluginId: CORE_PLUGIN_ID,
1746
1687
  kind: "extractor",
1747
1688
  description: "Turns `tools: [mcp__<server>__<tool>]` entries in a node's frontmatter into an MCP node per unique server and an arrow from the source to each one. Example: `tools: [mcp__github__create_pr]` adds an `mcp://github` node and an arrow to it.",
@@ -1772,7 +1713,7 @@ var mcpToolsExtractor = {
1772
1713
  raw: `mcp__${server}__*`,
1773
1714
  candidates: [
1774
1715
  {
1775
- extractorId: ID8,
1716
+ extractorId: ID7,
1776
1717
  kind: "references",
1777
1718
  target: mcpPath,
1778
1719
  confidence: 0.85,
@@ -1815,16 +1756,25 @@ function applyAjvFormats(ajv) {
1815
1756
  addFormats(ajv);
1816
1757
  }
1817
1758
 
1759
+ // kernel/util/finding-format.ts
1760
+ function formatFinding(parts) {
1761
+ const head = parts.subject ? `\`${parts.subject}\`:
1762
+ ` : "";
1763
+ const loc = parts.lines && parts.lines.length > 0 ? `L${parts.lines.join(", ")}: ` : "";
1764
+ return `${head}${loc}${parts.body}`;
1765
+ }
1766
+
1818
1767
  // plugins/core/analyzers/annotation-field-unknown/text.ts
1819
1768
  var ANNOTATION_FIELD_UNKNOWN_TEXTS = {
1820
- // Compact finding grammar: the affected node is the finding's own
1821
- // node, so its path never appears in the message.
1769
+ // Diagnosis bodies (`<what>; <why>`). The shared `formatFinding` helper
1770
+ // owns the subject (the offending key, built before the call); the
1771
+ // affected node is the finding's own node, so its path never appears.
1822
1772
  /** Key inside `annotations:` is not in the curated catalog. */
1823
- unknownAnnotationKey: "Unknown sidecar key '{{key}}'; not in the annotations catalog.",
1773
+ unknownAnnotationKey: "Unknown sidecar key; not in the annotations catalog",
1824
1774
  /** Top-level key is neither reserved, nor a registered plugin namespace, nor a registered root key. */
1825
- unknownRootKey: "Unknown sidecar top-level key '{{key}}'; not a reserved block, a plugin namespace, or a root contribution.",
1775
+ unknownRootKey: "Unknown top-level sidecar key; not a reserved block, plugin namespace, or root contribution",
1826
1776
  /** Value under a registered plugin namespace fails the contributed schema. */
1827
- pluginNamespaceInvalid: "Sidecar block '{{pluginId}}.{{key}}' fails the schema from plugin '{{pluginId}}': {{errors}}.",
1777
+ pluginNamespaceInvalid: "Sidecar block fails the plugin schema; {{errors}}",
1828
1778
  // Tooltips for the per-node view-contribution badges. Singular vs
1829
1779
  // plural keeps the count grammar correct without a sub-template.
1830
1780
  alertTooltipSingle: "This node has 1 unknown field in its sidecar. Open the inspector for details.",
@@ -1832,10 +1782,10 @@ var ANNOTATION_FIELD_UNKNOWN_TEXTS = {
1832
1782
  };
1833
1783
 
1834
1784
  // plugins/core/analyzers/annotation-field-unknown/index.ts
1835
- var ID9 = "annotation-field-unknown";
1785
+ var ID8 = "annotation-field-unknown";
1836
1786
  var RESERVED_ROOT_BLOCKS = /* @__PURE__ */ new Set(["identity", "annotations", "settings", "audit"]);
1837
1787
  var annotationFieldUnknownAnalyzer = {
1838
- id: ID9,
1788
+ id: ID8,
1839
1789
  pluginId: CORE_PLUGIN_ID,
1840
1790
  kind: "analyzer",
1841
1791
  description: "Flags typos or unrecognized keys in sidecars (`.sm`).",
@@ -1874,12 +1824,12 @@ var annotationFieldUnknownAnalyzer = {
1874
1824
  for (const key of Object.keys(annotations)) {
1875
1825
  if (!knownAnnotationKeys.has(key)) {
1876
1826
  issues.push({
1877
- analyzerId: ID9,
1827
+ analyzerId: ID8,
1878
1828
  severity: "warn",
1879
1829
  nodeIds: [node.path],
1880
- message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.unknownAnnotationKey, {
1881
- path: node.path,
1882
- key
1830
+ message: formatFinding({
1831
+ subject: key,
1832
+ body: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.unknownAnnotationKey)
1883
1833
  }),
1884
1834
  data: { surface: "annotations", key }
1885
1835
  });
@@ -1901,14 +1851,14 @@ var annotationFieldUnknownAnalyzer = {
1901
1851
  if (validator(value)) continue;
1902
1852
  const errors = (validator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
1903
1853
  issues.push({
1904
- analyzerId: ID9,
1854
+ analyzerId: ID8,
1905
1855
  severity: "warn",
1906
1856
  nodeIds: [node.path],
1907
- message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.pluginNamespaceInvalid, {
1908
- path: node.path,
1909
- pluginId: key,
1910
- key: contribKey,
1911
- errors
1857
+ message: formatFinding({
1858
+ subject: `${key}.${contribKey}`,
1859
+ body: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.pluginNamespaceInvalid, {
1860
+ errors
1861
+ })
1912
1862
  }),
1913
1863
  data: { surface: "plugin-namespace", pluginId: key, key: contribKey }
1914
1864
  });
@@ -1917,12 +1867,12 @@ var annotationFieldUnknownAnalyzer = {
1917
1867
  continue;
1918
1868
  }
1919
1869
  issues.push({
1920
- analyzerId: ID9,
1870
+ analyzerId: ID8,
1921
1871
  severity: "warn",
1922
1872
  nodeIds: [node.path],
1923
- message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.unknownRootKey, {
1924
- path: node.path,
1925
- key
1873
+ message: formatFinding({
1874
+ subject: key,
1875
+ body: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.unknownRootKey)
1926
1876
  }),
1927
1877
  data: { surface: "root", key }
1928
1878
  });
@@ -1979,18 +1929,21 @@ function collectPluginIds(contributions) {
1979
1929
  // plugins/core/analyzers/annotation-orphan/text.ts
1980
1930
  var ANNOTATION_ORPHAN_TEXTS = {
1981
1931
  /**
1982
- * Compact finding grammar: line 1 = the orphan sidecar file, line 2
1983
- * = the diagnosis. The expected markdown path IS the finding's
1984
- * `nodeIds[0]` (the issue files under the path the sidecar points
1985
- * at), so it never appears in the message.
1932
+ * Diagnosis body (`<what>; <why>`). The shared `formatFinding` helper
1933
+ * wraps it with the backtick subject (the orphan sidecar file); the
1934
+ * expected markdown path IS the finding's `nodeIds[0]`, so it never
1935
+ * appears in the message. The remediation hint moves to
1936
+ * `Issue.fix.summary` below.
1986
1937
  */
1987
- message: "{{sidecarPath}}:\nOrphan sidecar; no matching markdown node."
1938
+ message: "Orphan sidecar; no matching markdown node",
1939
+ /** Remediation hint surfaced via `Issue.fix.summary`. */
1940
+ fixSummary: "Run `sm sidecar prune` to remove orphan sidecars."
1988
1941
  };
1989
1942
 
1990
1943
  // plugins/core/analyzers/annotation-orphan/index.ts
1991
- var ID10 = "annotation-orphan";
1944
+ var ID9 = "annotation-orphan";
1992
1945
  var annotationOrphanAnalyzer = {
1993
- id: ID10,
1946
+ id: ID9,
1994
1947
  pluginId: CORE_PLUGIN_ID,
1995
1948
  kind: "analyzer",
1996
1949
  description: "Flags sidecars (`.sm`) whose `.md` file no longer exists.",
@@ -2002,13 +1955,14 @@ var annotationOrphanAnalyzer = {
2002
1955
  for (const orphan of orphans) {
2003
1956
  const expectedMdRelative = orphan.relativePath.endsWith(".sm") ? `${orphan.relativePath.slice(0, -".sm".length)}.md` : `${orphan.relativePath}.md`;
2004
1957
  issues.push({
2005
- analyzerId: ID10,
1958
+ analyzerId: ID9,
2006
1959
  severity: "warn",
2007
1960
  nodeIds: [expectedMdRelative],
2008
- message: tx(ANNOTATION_ORPHAN_TEXTS.message, {
2009
- sidecarPath: orphan.relativePath,
2010
- expectedMdPath: orphan.expectedMdPath
1961
+ message: formatFinding({
1962
+ subject: orphan.relativePath,
1963
+ body: tx(ANNOTATION_ORPHAN_TEXTS.message)
2011
1964
  }),
1965
+ fix: { summary: tx(ANNOTATION_ORPHAN_TEXTS.fixSummary) },
2012
1966
  data: {
2013
1967
  sidecarPath: orphan.relativePath,
2014
1968
  expectedMdPath: orphan.expectedMdPath
@@ -2021,14 +1975,17 @@ var annotationOrphanAnalyzer = {
2021
1975
 
2022
1976
  // plugins/core/analyzers/annotation-stale/text.ts
2023
1977
  var ANNOTATION_STALE_TEXTS = {
2024
- // Compact finding grammar: the affected node is the finding's own
2025
- // node, so its path never appears in the message.
1978
+ // Diagnosis bodies (`<what>; <why>`). The shared `formatFinding` helper
1979
+ // emits no subject (the affected node IS the finding's own node); the
1980
+ // remediation hint moves to `Issue.fix.summary` below.
2026
1981
  /** body changed since last bump */
2027
- bodyDrift: "Sidecar `.sm` is stale: body changed since last bump.",
1982
+ bodyDrift: "Sidecar stale; body changed since last bump",
2028
1983
  /** frontmatter changed since last bump */
2029
- frontmatterDrift: "Sidecar `.sm` is stale: frontmatter changed since last bump.",
1984
+ frontmatterDrift: "Sidecar stale; frontmatter changed since last bump",
2030
1985
  /** both body and frontmatter changed */
2031
- bothDrift: "Sidecar `.sm` is stale: body and frontmatter changed since last bump.",
1986
+ bothDrift: "Sidecar stale; body and frontmatter changed since last bump",
1987
+ /** Remediation hint surfaced via `Issue.fix.summary`. */
1988
+ fixSummary: "Run `sm bump <path>` to refresh the sidecar.",
2032
1989
  // Tooltips for the `card.footer.right` clock chip emitted alongside
2033
1990
  // the issue. Lists only the drifted face(s), in-sync faces are
2034
1991
  // omitted so the operator immediately sees what's modified without
@@ -2041,7 +1998,7 @@ var ANNOTATION_STALE_TEXTS = {
2041
1998
  };
2042
1999
 
2043
2000
  // plugins/core/analyzers/annotation-stale/index.ts
2044
- var ID11 = "annotation-stale";
2001
+ var ID10 = "annotation-stale";
2045
2002
  var staleIcon = {
2046
2003
  slot: "card.footer.right",
2047
2004
  icon: "pi-clock",
@@ -2054,7 +2011,7 @@ var staleBadge = {
2054
2011
  priority: 20
2055
2012
  };
2056
2013
  var annotationStaleAnalyzer = {
2057
- id: ID11,
2014
+ id: ID10,
2058
2015
  pluginId: CORE_PLUGIN_ID,
2059
2016
  kind: "analyzer",
2060
2017
  description: "Marks sidecars (`.sm`) that are out of date with their `.md`.",
@@ -2070,10 +2027,11 @@ var annotationStaleAnalyzer = {
2070
2027
  const status = staleStatus(node.sidecar);
2071
2028
  if (status === null) continue;
2072
2029
  issues.push({
2073
- analyzerId: ID11,
2030
+ analyzerId: ID10,
2074
2031
  severity: "info",
2075
2032
  nodeIds: [node.path],
2076
- message: messageFor(status, node.path),
2033
+ message: formatFinding({ body: messageFor(status) }),
2034
+ fix: { summary: tx(ANNOTATION_STALE_TEXTS.fixSummary) },
2077
2035
  data: { status }
2078
2036
  });
2079
2037
  ctx.emitContribution(node.path, staleIcon, {
@@ -2093,14 +2051,14 @@ function staleStatus(overlay) {
2093
2051
  if (status === void 0 || status === null || status === "fresh") return null;
2094
2052
  return status;
2095
2053
  }
2096
- function messageFor(status, path) {
2054
+ function messageFor(status) {
2097
2055
  switch (status) {
2098
2056
  case "stale-body":
2099
- return tx(ANNOTATION_STALE_TEXTS.bodyDrift, { path });
2057
+ return tx(ANNOTATION_STALE_TEXTS.bodyDrift);
2100
2058
  case "stale-frontmatter":
2101
- return tx(ANNOTATION_STALE_TEXTS.frontmatterDrift, { path });
2059
+ return tx(ANNOTATION_STALE_TEXTS.frontmatterDrift);
2102
2060
  case "stale-both":
2103
- return tx(ANNOTATION_STALE_TEXTS.bothDrift, { path });
2061
+ return tx(ANNOTATION_STALE_TEXTS.bothDrift);
2104
2062
  }
2105
2063
  }
2106
2064
  function tooltipFor(status) {
@@ -2115,9 +2073,9 @@ function tooltipFor(status) {
2115
2073
  }
2116
2074
 
2117
2075
  // plugins/core/analyzers/contribution-orphan/index.ts
2118
- var ID12 = "contribution-orphan";
2076
+ var ID11 = "contribution-orphan";
2119
2077
  var contributionOrphanAnalyzer = {
2120
- id: ID12,
2078
+ id: ID11,
2121
2079
  pluginId: CORE_PLUGIN_ID,
2122
2080
  kind: "analyzer",
2123
2081
  description: "Warns about plugin data referencing nodes renamed or deleted in the latest scan.",
@@ -2127,6 +2085,92 @@ var contributionOrphanAnalyzer = {
2127
2085
  }
2128
2086
  };
2129
2087
 
2088
+ // plugins/core/analyzers/extractor-collision/text.ts
2089
+ var EXTRACTOR_COLLISION_TEXTS = {
2090
+ /**
2091
+ * Per-Signal warn issue: two extractors detected something at
2092
+ * overlapping byte ranges within the same node and the resolver
2093
+ * dropped the loser. Surfaces WHO lost, WHO won, and the tiebreak
2094
+ * reason so the operator can understand why a candidate edge did NOT
2095
+ * become a Link (e.g. a `[link](path)` with `@path` inside the bracket
2096
+ * text: markdown-link wins and the at-directive silently disappears
2097
+ * without this warning).
2098
+ */
2099
+ message: "Overlap collision; {{loserExtractor}} (at {{loserRange}}) lost to {{winnerExtractor}} (at {{winnerRange}}) by {{reason}}, only the winning edge persists",
2100
+ /**
2101
+ * Remediation hint for the range-overlap rejection, surfaced via
2102
+ * `Issue.fix.summary`. Not autofixable: the rule cannot tell which
2103
+ * detection the author meant, so it offers the two resolutions
2104
+ * (rephrase one token, or accept the winner).
2105
+ */
2106
+ rejectedFixSummary: "Rephrase one of the overlapping tokens, or accept the winner."
2107
+ };
2108
+
2109
+ // plugins/core/analyzers/extractor-collision/index.ts
2110
+ var ID12 = "extractor-collision";
2111
+ function signalLines(signal) {
2112
+ return signal.range && typeof signal.range.line === "number" ? [signal.range.line] : void 0;
2113
+ }
2114
+ var extractorCollisionAnalyzer = {
2115
+ id: ID12,
2116
+ pluginId: CORE_PLUGIN_ID,
2117
+ kind: "analyzer",
2118
+ description: "Reports when two extractors detect something at the same span of body text and the resolver drops one.",
2119
+ mode: "deterministic",
2120
+ evaluate(ctx) {
2121
+ const signals = ctx.signals;
2122
+ if (!signals || signals.length === 0) return [];
2123
+ const issues = [];
2124
+ for (const signal of signals) {
2125
+ const issue = makeIssue(signal);
2126
+ if (issue) issues.push(issue);
2127
+ }
2128
+ return issues;
2129
+ }
2130
+ };
2131
+ function makeIssue(signal) {
2132
+ const resolution = signal.resolution;
2133
+ if (!resolution || resolution.outcome !== "rejected" || !resolution.rejectedBy) return null;
2134
+ const winner = resolution.rejectedBy;
2135
+ const loserCandidate = signal.candidates[resolution.winnerIndex ?? 0];
2136
+ const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
2137
+ const winnerRange = `${winner.range.start}-${winner.range.end}`;
2138
+ return {
2139
+ analyzerId: ID12,
2140
+ severity: "warn",
2141
+ nodeIds: [signal.source],
2142
+ message: formatFinding({
2143
+ subject: signal.raw,
2144
+ lines: signalLines(signal),
2145
+ body: tx(EXTRACTOR_COLLISION_TEXTS.message, {
2146
+ loserExtractor: loserCandidate.extractorId,
2147
+ loserRange,
2148
+ winnerExtractor: winner.extractorId,
2149
+ winnerRange,
2150
+ reason: winner.reason
2151
+ })
2152
+ }),
2153
+ fix: { summary: tx(EXTRACTOR_COLLISION_TEXTS.rejectedFixSummary) },
2154
+ data: {
2155
+ loser: {
2156
+ extractorId: loserCandidate.extractorId,
2157
+ raw: signal.raw,
2158
+ range: signal.range ?? null,
2159
+ candidate: {
2160
+ kind: loserCandidate.kind,
2161
+ target: loserCandidate.target,
2162
+ confidence: loserCandidate.confidence
2163
+ }
2164
+ },
2165
+ winner: {
2166
+ extractorId: winner.extractorId,
2167
+ range: winner.range
2168
+ },
2169
+ reason: winner.reason
2170
+ }
2171
+ };
2172
+ }
2173
+
2130
2174
  // plugins/core/analyzers/issue-counter/text.ts
2131
2175
  var ISSUE_COUNTER_TEXTS = {
2132
2176
  errorTooltipSingle: "1 error",
@@ -2203,55 +2247,122 @@ var issueCounterAnalyzer = {
2203
2247
  }
2204
2248
  };
2205
2249
 
2206
- // plugins/core/analyzers/job-file-orphan/text.ts
2207
- var JOB_FILE_ORPHAN_TEXTS = {
2208
- /**
2209
- * `<path>.md` lives under `.skill-map/jobs/` but no `state_jobs.filePath`
2210
- * row references it. Compact finding grammar: the file IS the
2211
- * finding's own node, so its path never appears in the message.
2212
- */
2213
- message: "Orphan job file; not referenced by any job. Run `sm job prune --orphan-files` to remove it."
2250
+ // kernel/util/link-lines.ts
2251
+ function isSelfLoop(link) {
2252
+ if (link.source === link.target) return true;
2253
+ if (link.resolvedTarget && link.source === link.resolvedTarget) return true;
2254
+ return false;
2255
+ }
2256
+ function linkLines(link) {
2257
+ const lines = /* @__PURE__ */ new Set();
2258
+ for (const occ of link.occurrences ?? []) {
2259
+ const line = occ.location?.line;
2260
+ if (typeof line === "number") lines.add(line);
2261
+ }
2262
+ if (lines.size === 0) {
2263
+ const line = link.location?.line;
2264
+ if (typeof line === "number") lines.add(line);
2265
+ }
2266
+ return [...lines].sort((a, b) => a - b);
2267
+ }
2268
+
2269
+ // plugins/core/analyzers/link-counter/text.ts
2270
+ var LINK_COUNTER_TEXTS = {
2271
+ /** Accessible label for the incoming-links chip. */
2272
+ linksInLabel: "incoming links",
2273
+ /** Accessible label for the outgoing-links chip. */
2274
+ linksOutLabel: "outgoing links",
2275
+ /** Tooltip header for the incoming breakdown (first line). */
2276
+ directionIn: "in",
2277
+ /** Tooltip header for the outgoing breakdown (first line). */
2278
+ directionOut: "out"
2214
2279
  };
2215
2280
 
2216
- // plugins/core/analyzers/job-file-orphan/index.ts
2217
- var ID14 = "job-file-orphan";
2218
- var jobFileOrphanAnalyzer = {
2281
+ // plugins/core/analyzers/link-counter/index.ts
2282
+ var ID14 = "link-counter";
2283
+ var linksIn = {
2284
+ slot: "card.footer.left",
2285
+ icon: "pi-download",
2286
+ label: LINK_COUNTER_TEXTS.linksInLabel,
2287
+ emitWhenEmpty: false,
2288
+ priority: 10
2289
+ };
2290
+ var linksOut = {
2291
+ slot: "card.footer.left",
2292
+ icon: "pi-upload",
2293
+ label: LINK_COUNTER_TEXTS.linksOutLabel,
2294
+ emitWhenEmpty: false,
2295
+ priority: 20
2296
+ };
2297
+ var linkCounterAnalyzer = {
2219
2298
  id: ID14,
2220
2299
  pluginId: CORE_PLUGIN_ID,
2221
2300
  kind: "analyzer",
2222
- description: "Flags leftover job result files (no live job references them). Clean up via `sm job prune --orphan-files`.",
2301
+ description: "Counts incoming and outgoing links per node.",
2223
2302
  mode: "deterministic",
2303
+ ui: { linksIn, linksOut },
2224
2304
  evaluate(ctx) {
2225
- const orphans = ctx.orphanJobFiles;
2226
- if (!orphans || orphans.length === 0) return [];
2227
- const issues = [];
2228
- for (const filePath of orphans) {
2229
- issues.push({
2230
- analyzerId: ID14,
2231
- severity: "warn",
2232
- nodeIds: [filePath],
2233
- message: tx(JOB_FILE_ORPHAN_TEXTS.message, { filePath }),
2234
- data: { filePath }
2235
- });
2305
+ const perTarget = /* @__PURE__ */ new Map();
2306
+ const perSource = /* @__PURE__ */ new Map();
2307
+ for (const link of ctx.links) {
2308
+ if (isSelfLoop(link)) continue;
2309
+ const target = link.resolvedTarget ?? link.target;
2310
+ bump(perTarget, target, link.kind);
2311
+ bump(perSource, link.source, link.kind);
2236
2312
  }
2237
- return issues;
2313
+ for (const node of ctx.nodes) {
2314
+ emitChip(ctx, node.path, linksIn, "in", perTarget.get(node.path));
2315
+ emitChip(ctx, node.path, linksOut, "out", perSource.get(node.path));
2316
+ }
2317
+ return [];
2238
2318
  }
2239
2319
  };
2320
+ function bump(map, key, kind) {
2321
+ let byKind = map.get(key);
2322
+ if (!byKind) {
2323
+ byKind = /* @__PURE__ */ new Map();
2324
+ map.set(key, byKind);
2325
+ }
2326
+ byKind.set(kind, (byKind.get(kind) ?? 0) + 1);
2327
+ }
2328
+ function emitChip(ctx, nodePath, ref, direction, byKind) {
2329
+ if (!byKind) return;
2330
+ let total = 0;
2331
+ for (const n of byKind.values()) total += n;
2332
+ if (total === 0) return;
2333
+ const capped = Math.min(total, 99);
2334
+ ctx.emitContribution(nodePath, ref, {
2335
+ value: capped,
2336
+ tooltip: formatBreakdown(byKind, direction)
2337
+ });
2338
+ }
2339
+ function formatBreakdown(byKind, direction) {
2340
+ const lines = [...byKind.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([kind, n]) => `${kind}: ${n}`);
2341
+ const dirLabel = direction === "in" ? LINK_COUNTER_TEXTS.directionIn : LINK_COUNTER_TEXTS.directionOut;
2342
+ return [dirLabel, ...lines].join("\n");
2343
+ }
2240
2344
 
2241
- // plugins/core/analyzers/link-conflict/text.ts
2242
- var LINK_CONFLICT_TEXTS = {
2345
+ // plugins/core/analyzers/link-kind-conflict/text.ts
2346
+ var LINK_KIND_CONFLICT_TEXTS = {
2347
+ /**
2348
+ * Diagnosis body (`<what>; <why>`). The shared `formatFinding` helper
2349
+ * wraps it with the backtick subject (the disputed target); the source
2350
+ * is the finding's own node, so it never appears in the message.
2351
+ */
2352
+ message: "Conflicting link kind; detectors disagree ({{kindList}})",
2243
2353
  /**
2244
- * Compact finding grammar: line 1 = the disputed target, line 2 =
2245
- * the short diagnosis. The source is the finding's own node, so it
2246
- * never appears in the message.
2354
+ * Remediation hint surfaced via `Issue.fix.summary`. Not autofixable:
2355
+ * the rule cannot tell which kind the author meant, so it offers the
2356
+ * two valid resolutions (drop one source, or accept the overlap on
2357
+ * purpose). Mirrors the `warn`-severity `link-self-loop` hint shape.
2247
2358
  */
2248
- message: "{{target}}:\nDetectors disagree on link kind ({{kindList}})."
2359
+ fixSummary: "Remove one of the conflicting sources to settle on a single kind, or ignore the conflict deliberately."
2249
2360
  };
2250
2361
 
2251
- // plugins/core/analyzers/link-conflict/index.ts
2252
- var ID15 = "link-conflict";
2362
+ // plugins/core/analyzers/link-kind-conflict/index.ts
2363
+ var ID15 = "link-kind-conflict";
2253
2364
  var NON_CONFLICTING_KINDS = /* @__PURE__ */ new Set(["points"]);
2254
- var linkConflictAnalyzer = {
2365
+ var linkKindConflictAnalyzer = {
2255
2366
  id: ID15,
2256
2367
  pluginId: CORE_PLUGIN_ID,
2257
2368
  kind: "analyzer",
@@ -2303,11 +2414,13 @@ var linkConflictAnalyzer = {
2303
2414
  analyzerId: ID15,
2304
2415
  severity: "warn",
2305
2416
  nodeIds: [source, target],
2306
- message: tx(LINK_CONFLICT_TEXTS.message, {
2307
- source,
2308
- target,
2309
- kindList
2417
+ message: formatFinding({
2418
+ subject: target,
2419
+ body: tx(LINK_KIND_CONFLICT_TEXTS.message, {
2420
+ kindList
2421
+ })
2310
2422
  }),
2423
+ fix: { summary: tx(LINK_KIND_CONFLICT_TEXTS.fixSummary) },
2311
2424
  data: { source, target, variants }
2312
2425
  });
2313
2426
  }
@@ -2318,137 +2431,46 @@ function rankConfidence(c) {
2318
2431
  return c;
2319
2432
  }
2320
2433
 
2321
- // kernel/util/link-lines.ts
2322
- function isSelfLoop(link) {
2323
- if (link.source === link.target) return true;
2324
- if (link.resolvedTarget && link.source === link.resolvedTarget) return true;
2325
- return false;
2326
- }
2327
- function linkLines(link) {
2328
- const lines = /* @__PURE__ */ new Set();
2329
- for (const occ of link.occurrences ?? []) {
2330
- const line = occ.location?.line;
2331
- if (typeof line === "number") lines.add(line);
2332
- }
2333
- if (lines.size === 0) {
2334
- const line = link.location?.line;
2335
- if (typeof line === "number") lines.add(line);
2336
- }
2337
- return [...lines].sort((a, b) => a - b);
2338
- }
2339
- function linkWhere(link, texts) {
2340
- const lines = linkLines(link);
2341
- if (lines.length === 0) return "";
2342
- return tx(lines.length === 1 ? texts.single : texts.plural, {
2343
- lines: lines.join(", ")
2344
- });
2345
- }
2346
-
2347
- // plugins/core/analyzers/link-counter/index.ts
2348
- var ID16 = "link-counter";
2349
- var linksIn = {
2350
- slot: "card.footer.left",
2351
- icon: "pi-download",
2352
- label: "incoming links",
2353
- emitWhenEmpty: false,
2354
- priority: 10
2355
- };
2356
- var linksOut = {
2357
- slot: "card.footer.left",
2358
- icon: "pi-upload",
2359
- label: "outgoing links",
2360
- emitWhenEmpty: false,
2361
- priority: 20
2362
- };
2363
- var linkCounterAnalyzer = {
2364
- id: ID16,
2365
- pluginId: CORE_PLUGIN_ID,
2366
- kind: "analyzer",
2367
- description: "Counts incoming and outgoing links per node.",
2368
- mode: "deterministic",
2369
- ui: { linksIn, linksOut },
2370
- evaluate(ctx) {
2371
- const perTarget = /* @__PURE__ */ new Map();
2372
- const perSource = /* @__PURE__ */ new Map();
2373
- for (const link of ctx.links) {
2374
- if (isSelfLoop(link)) continue;
2375
- const target = link.resolvedTarget ?? link.target;
2376
- bump(perTarget, target, link.kind);
2377
- bump(perSource, link.source, link.kind);
2378
- }
2379
- for (const node of ctx.nodes) {
2380
- emitChip(ctx, node.path, linksIn, "in", perTarget.get(node.path));
2381
- emitChip(ctx, node.path, linksOut, "out", perSource.get(node.path));
2382
- }
2383
- return [];
2384
- }
2385
- };
2386
- function bump(map, key, kind) {
2387
- let byKind = map.get(key);
2388
- if (!byKind) {
2389
- byKind = /* @__PURE__ */ new Map();
2390
- map.set(key, byKind);
2391
- }
2392
- byKind.set(kind, (byKind.get(kind) ?? 0) + 1);
2393
- }
2394
- function emitChip(ctx, nodePath, ref, direction, byKind) {
2395
- if (!byKind) return;
2396
- let total = 0;
2397
- for (const n of byKind.values()) total += n;
2398
- if (total === 0) return;
2399
- const capped = Math.min(total, 99);
2400
- ctx.emitContribution(nodePath, ref, {
2401
- value: capped,
2402
- tooltip: formatBreakdown(byKind, direction)
2403
- });
2404
- }
2405
- function formatBreakdown(byKind, direction) {
2406
- const lines = [...byKind.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([kind, n]) => `${kind}: ${n}`);
2407
- return [direction, ...lines].join("\n");
2408
- }
2409
-
2410
2434
  // plugins/core/analyzers/link-self-loop/text.ts
2411
2435
  var LINK_SELF_LOOP_TEXTS = {
2412
2436
  /**
2413
- * Per-edge warn: a node body references itself via the slash /
2437
+ * Per-edge warn body: a node body references itself via the slash /
2414
2438
  * at-directive / markdown-link surface (most commonly because the
2415
2439
  * file's heading IS the invocation token, e.g. `# /deploy` inside
2416
2440
  * `commands/deploy.md`). The link is structurally valid but rarely
2417
2441
  * the operator's intent; UI consumers MAY hide it by default and
2418
2442
  * surface a count.
2419
2443
  */
2420
- message: "`{{trigger}}`:\nSelf-reference ({{kind}}{{where}}); typically the file's own heading or label. Remove the token or ignore deliberately.",
2421
- /** Location suffix inside the kind parens, one detection site. */
2422
- whereSingle: ", line {{lines}}",
2423
- /** Location suffix inside the kind parens, several detection sites. */
2424
- wherePlural: ", lines {{lines}}"
2444
+ message: "Self-reference; the skill/command invokes itself, potential loop",
2445
+ /**
2446
+ * Remediation hint surfaced via `Issue.fix.summary` (the Inspector
2447
+ * renders it under the finding, separate from the diagnosis body).
2448
+ */
2449
+ fixSummary: "Remove the token or ignore the self-reference deliberately."
2425
2450
  };
2426
2451
 
2427
2452
  // plugins/core/analyzers/link-self-loop/index.ts
2428
- var ID17 = "link-self-loop";
2453
+ var ID16 = "link-self-loop";
2429
2454
  var linkSelfLoopAnalyzer = {
2430
- id: ID17,
2455
+ id: ID16,
2431
2456
  pluginId: CORE_PLUGIN_ID,
2432
2457
  kind: "analyzer",
2433
2458
  description: "Flags links whose source is also their own resolved target (e.g. a body heading like `# /deploy` inside the file that defines `/deploy`).",
2434
2459
  mode: "deterministic",
2435
2460
  evaluate(ctx) {
2436
- if (ctx.links.length === 0) return [];
2437
2461
  const issues = [];
2438
2462
  for (const link of ctx.links) {
2439
2463
  if (!isSelfLoop(link)) continue;
2440
2464
  issues.push({
2441
- analyzerId: ID17,
2465
+ analyzerId: ID16,
2442
2466
  severity: "warn",
2443
2467
  nodeIds: [link.source],
2444
- message: tx(LINK_SELF_LOOP_TEXTS.message, {
2445
- trigger: link.trigger?.originalTrigger ?? link.target,
2446
- kind: link.kind,
2447
- where: linkWhere(link, {
2448
- single: LINK_SELF_LOOP_TEXTS.whereSingle,
2449
- plural: LINK_SELF_LOOP_TEXTS.wherePlural
2450
- })
2468
+ message: formatFinding({
2469
+ subject: link.trigger?.originalTrigger ?? link.target,
2470
+ lines: linkLines(link),
2471
+ body: tx(LINK_SELF_LOOP_TEXTS.message)
2451
2472
  }),
2473
+ fix: { summary: tx(LINK_SELF_LOOP_TEXTS.fixSummary) },
2452
2474
  data: {
2453
2475
  target: link.target,
2454
2476
  resolvedTarget: link.resolvedTarget ?? link.target,
@@ -2460,152 +2482,81 @@ var linkSelfLoopAnalyzer = {
2460
2482
  }
2461
2483
  };
2462
2484
 
2463
- // kernel/orchestrator/node-identifiers.ts
2464
- import { posix as pathPosix4 } from "path";
2465
- function deriveNodeIdentifiers(node, kindDescriptor) {
2466
- const sources = kindDescriptor?.identifiers;
2467
- if (!sources || sources.length === 0) return [];
2468
- const out = [];
2469
- for (const source of sources) {
2470
- const raw = readIdentifier(source, node);
2471
- if (!raw) continue;
2472
- const normalised = normalizeTrigger(raw);
2473
- if (normalised) out.push(normalised);
2474
- }
2475
- return out;
2476
- }
2477
- function readIdentifier(source, node) {
2478
- if (source === "frontmatter.name") return readFrontmatterName(node);
2479
- if (source === "filename-basename") return readFilenameBasename(node);
2480
- return readDirname(node);
2481
- }
2482
- function readFrontmatterName(node) {
2483
- const raw = node.frontmatter?.["name"];
2484
- if (typeof raw !== "string") return null;
2485
- return raw.length > 0 ? raw : null;
2486
- }
2487
- function readFilenameBasename(node) {
2488
- const base = pathPosix4.basename(node.path);
2489
- if (!base) return null;
2490
- const ext = pathPosix4.extname(base);
2491
- const stem = ext ? base.slice(0, -ext.length) : base;
2492
- return stem.length > 0 ? stem : null;
2493
- }
2494
- function readDirname(node) {
2495
- const dir = pathPosix4.dirname(node.path);
2496
- if (!dir || dir === "." || dir === "/") return null;
2497
- const base = pathPosix4.basename(dir);
2498
- return base.length > 0 ? base : null;
2499
- }
2485
+ // plugins/core/analyzers/name-collision/text.ts
2486
+ var NAME_COLLISION_TEXTS = {
2487
+ /**
2488
+ * Diagnosis body (`<what>; <why>: <evidence>`). The shared
2489
+ * `formatFinding` helper wraps it with the backtick subject (the
2490
+ * normalised name claimed by two or more nodes). The evidence is the
2491
+ * competing node paths.
2492
+ */
2493
+ message: "Name collision; {{count}} nodes declare the same name: {{paths}}"
2494
+ };
2500
2495
 
2501
- // kernel/orchestrator/lift-resolved-link-confidence.ts
2502
- var RESERVED_TARGET_CONFIDENCE = 0.1;
2503
- var BROKEN_TARGET_CONFIDENCE = 0.5;
2504
- function liftResolvedLinkConfidence(links, nodes, ctx) {
2505
- if (!links.some((l) => l.confidence < 1)) return;
2506
- const indexes = buildIndexes(nodes, ctx);
2507
- for (const link of links) {
2508
- if (link.confidence < 1) applyResolution(link, indexes, ctx);
2509
- }
2510
- }
2511
- function collectBrokenLinks(links, nodes, ctx) {
2512
- const broken = /* @__PURE__ */ new Set();
2513
- if (links.length === 0) return broken;
2514
- const indexes = buildIndexes(nodes, ctx);
2515
- for (const link of links) {
2516
- if (isGenuinelyBroken(link, indexes)) broken.add(link);
2517
- }
2518
- return broken;
2519
- }
2520
- function applyResolution(link, indexes, ctx) {
2521
- const resolution = resolve2(link, indexes, ctx);
2522
- if (resolution === "none") {
2523
- if (isGenuinelyBroken(link, indexes)) {
2524
- link.confidence = Math.min(link.confidence, BROKEN_TARGET_CONFIDENCE);
2525
- }
2526
- return;
2527
- }
2528
- link.resolvedTarget = resolution;
2529
- if (indexes.nodeByPath.get(resolution)?.virtual) return;
2530
- link.confidence = ctx.reservedNodePaths.has(resolution) ? RESERVED_TARGET_CONFIDENCE : 1;
2531
- }
2532
- function buildIndexes(nodes, ctx) {
2533
- const byPath3 = /* @__PURE__ */ new Set();
2534
- const byName = /* @__PURE__ */ new Map();
2535
- const nodeByPath = /* @__PURE__ */ new Map();
2536
- for (const node of nodes) {
2537
- byPath3.add(node.path);
2538
- nodeByPath.set(node.path, node);
2539
- indexNode(node, ctx, byName);
2540
- }
2541
- return { byPath: byPath3, byName, nodeByPath };
2542
- }
2543
- function resolve2(link, indexes, ctx) {
2544
- if (indexes.byPath.has(link.target)) return link.target;
2545
- return resolveByName(link, indexes, ctx);
2546
- }
2547
- function isGenuinelyBroken(link, indexes) {
2548
- if (indexes.byPath.has(link.target)) return false;
2549
- const stripped = stripTriggerSigil(link.trigger?.normalizedTrigger);
2550
- if (stripped !== null && indexes.byName.has(stripped)) return false;
2551
- return true;
2552
- }
2553
- function resolveByName(link, indexes, ctx) {
2554
- const stripped = stripTriggerSigil(link.trigger?.normalizedTrigger);
2555
- if (stripped === null) return "none";
2556
- const candidates = indexes.byName.get(stripped);
2557
- if (!candidates?.length) return "none";
2558
- const allowedKinds = lookupAllowedKinds(link, indexes, ctx);
2559
- if (!allowedKinds?.length) return "none";
2560
- const winner = candidates.find((c) => allowedKinds.includes(c.kind));
2561
- return winner ? winner.path : "none";
2562
- }
2563
- function lookupAllowedKinds(link, _indexes, ctx) {
2564
- if (ctx.activeProvider === null) return void 0;
2565
- return ctx.providerResolution.get(ctx.activeProvider)?.[link.kind];
2566
- }
2567
- function stripTriggerSigil(normalized) {
2568
- if (!normalized) return null;
2569
- const trimmed = normalized.replace(/^[/@]/, "").trim();
2570
- return trimmed.length === 0 ? null : trimmed;
2571
- }
2572
- function indexNode(node, ctx, byName) {
2573
- const kindDescriptor = ctx.kindRegistry.get(kindKey(node));
2574
- const normalised = deriveNodeIdentifiers(node, kindDescriptor);
2575
- for (const name of normalised) {
2576
- const entry = { kind: node.kind, path: node.path };
2577
- const bucket = byName.get(name);
2578
- if (bucket) {
2579
- bucket.push(entry);
2580
- } else {
2581
- byName.set(name, [entry]);
2496
+ // plugins/core/analyzers/name-collision/index.ts
2497
+ var ID17 = "name-collision";
2498
+ var nameCollisionAnalyzer = {
2499
+ id: ID17,
2500
+ pluginId: CORE_PLUGIN_ID,
2501
+ kind: "analyzer",
2502
+ mode: "deterministic",
2503
+ description: "Flags two or more nodes that declare the same resolvable `name`.",
2504
+ // Pure projector of `ctx.nameCollisions` (computed once by the
2505
+ // orchestrator from the kind registry). One `error` per colliding name.
2506
+ evaluate(ctx) {
2507
+ const collisions = ctx.nameCollisions;
2508
+ if (!collisions || collisions.size === 0) return [];
2509
+ const issues = [];
2510
+ for (const [name, claims] of collisions) {
2511
+ const paths = claims.map((c) => c.path);
2512
+ issues.push({
2513
+ analyzerId: ID17,
2514
+ severity: "error",
2515
+ nodeIds: paths,
2516
+ message: formatFinding({
2517
+ subject: name,
2518
+ body: tx(NAME_COLLISION_TEXTS.message, {
2519
+ count: paths.length,
2520
+ paths: paths.join(", ")
2521
+ })
2522
+ }),
2523
+ data: {
2524
+ name,
2525
+ claims: claims.map((c) => ({ path: c.path, kind: c.kind }))
2526
+ }
2527
+ });
2582
2528
  }
2529
+ return issues;
2583
2530
  }
2584
- }
2585
- function kindKey(node) {
2586
- return `${node.provider}/${node.kind}`;
2587
- }
2531
+ };
2532
+
2533
+ // kernel/orchestrator/confidence-constants.ts
2534
+ var RESERVED_PENALTY = 0.9;
2535
+ var BROKEN_PENALTY = 0.5;
2588
2536
 
2589
2537
  // plugins/core/analyzers/name-reserved/text.ts
2590
2538
  var NAME_RESERVED_TEXTS = {
2591
2539
  /**
2592
- * Target-side message: emitted on the user file that collides with
2593
- * a runtime built-in. Same wording skill-map shipped before the
2594
- * source-side link finding landed.
2540
+ * Target-side body (`<what>; <why>`): emitted on the user file that
2541
+ * collides with a runtime built-in. The shared `formatFinding` helper
2542
+ * adds no subject (the offending node IS the finding's own node); the
2543
+ * remediation hint moves to `Issue.fix.summary` below.
2595
2544
  */
2596
- message: "Name collision: this {{kind}} name is already used by the {{provider}} runtime built-in, which shadows this file. Rename the file or its `frontmatter.name`.",
2545
+ message: "Reserved name; this {{kind}} name is shadowed by the {{provider}} runtime built-in",
2546
+ /** Remediation hint for the target-side finding. */
2547
+ fixSummary: "Rename the file or its frontmatter.name.",
2597
2548
  /**
2598
- * Source-side message: emitted on the node that AUTHORED a link
2599
- * whose target resolves to a reserved name. Explains WHY the link's
2600
- * confidence dropped to `RESERVED_TARGET_CONFIDENCE` (today `0.1`):
2601
- * the kernel saw the target match a runtime built-in and downgraded
2602
- * the edge so the operator notices.
2549
+ * Source-side body (`<what>; <why>`): emitted on the node that
2550
+ * AUTHORED a link whose target resolves to a reserved name. Reports the
2551
+ * fact (the runtime built-in shadows this edge); it deliberately does
2552
+ * NOT assert a confidence number, since the value is owned by the
2553
+ * `score`-phase scorers and may vary or be absent. The shared
2554
+ * `formatFinding` helper wraps it with the backtick target subject and
2555
+ * the `L<line>:` location prefix.
2603
2556
  */
2604
- linkMessage: "{{target}}:\nName collision: resolves to a {{provider}} built-in ({{reservedKind}} `{{reservedPath}}`){{where}}; the built-in wins, so this edge drops to confidence {{confidence}}. Rename the target file or its `frontmatter.name`.",
2605
- /** Location suffix after the built-in parens, one detection site. */
2606
- whereSingle: " (line {{lines}})",
2607
- /** Location suffix after the built-in parens, several detection sites. */
2608
- wherePlural: " (lines {{lines}})"
2557
+ linkMessage: "Reserved name; resolves to the {{provider}} built-in ({{reservedKind}} `{{reservedPath}}`), the built-in shadows this edge",
2558
+ /** Remediation hint for the source-side finding. */
2559
+ linkFixSummary: "Rename the target file or its frontmatter.name."
2609
2560
  };
2610
2561
 
2611
2562
  // plugins/core/analyzers/name-reserved/index.ts
@@ -2616,10 +2567,12 @@ var nameReservedAnalyzer = {
2616
2567
  kind: "analyzer",
2617
2568
  description: "Flags two kinds of reserved-name collision: a file whose name shadows a built-in command of the active runtime, and a link that resolves to one of those reserved names.",
2618
2569
  mode: "deterministic",
2570
+ phase: "score",
2619
2571
  // eslint-disable-next-line complexity
2620
2572
  evaluate(ctx) {
2621
2573
  const reserved = ctx.reservedNodePaths;
2622
2574
  if (!reserved || reserved.size === 0) return [];
2575
+ const adjust = ctx.adjustConfidence;
2623
2576
  const byPath3 = /* @__PURE__ */ new Map();
2624
2577
  for (const node of ctx.nodes) byPath3.set(node.path, node);
2625
2578
  const issues = [];
@@ -2630,32 +2583,38 @@ var nameReservedAnalyzer = {
2630
2583
  analyzerId: ID18,
2631
2584
  severity: "warn",
2632
2585
  nodeIds: [node.path],
2633
- message: tx(NAME_RESERVED_TEXTS.message, {
2634
- path: node.path,
2635
- provider: node.provider,
2636
- kind: node.kind
2586
+ message: formatFinding({
2587
+ body: tx(NAME_RESERVED_TEXTS.message, {
2588
+ provider: node.provider,
2589
+ kind: node.kind
2590
+ })
2637
2591
  }),
2592
+ fix: { summary: tx(NAME_RESERVED_TEXTS.fixSummary) },
2638
2593
  data: { provider: node.provider, kind: node.kind, surface: "target" }
2639
2594
  });
2640
2595
  }
2641
2596
  for (const link of ctx.links) {
2642
- if (link.confidence !== RESERVED_TARGET_CONFIDENCE) continue;
2643
2597
  const reservedPath = link.resolvedTarget;
2644
2598
  if (!reservedPath || !reserved.has(reservedPath)) continue;
2645
2599
  const reservedNode = byPath3.get(reservedPath);
2646
2600
  if (!reservedNode) continue;
2601
+ if (adjust) {
2602
+ adjust(link, { kind: "delta", value: -RESERVED_PENALTY });
2603
+ }
2647
2604
  issues.push({
2648
2605
  analyzerId: ID18,
2649
2606
  severity: "warn",
2650
2607
  nodeIds: [link.source],
2651
- message: tx(NAME_RESERVED_TEXTS.linkMessage, {
2652
- target: link.target,
2653
- provider: reservedNode.provider,
2654
- reservedKind: reservedNode.kind,
2655
- reservedPath: reservedNode.path,
2656
- confidence: RESERVED_TARGET_CONFIDENCE.toFixed(2),
2657
- where: linkWhereSuffix(link)
2608
+ message: formatFinding({
2609
+ subject: link.target,
2610
+ lines: linkLines(link),
2611
+ body: tx(NAME_RESERVED_TEXTS.linkMessage, {
2612
+ provider: reservedNode.provider,
2613
+ reservedKind: reservedNode.kind,
2614
+ reservedPath: reservedNode.path
2615
+ })
2658
2616
  }),
2617
+ fix: { summary: tx(NAME_RESERVED_TEXTS.linkFixSummary) },
2659
2618
  data: {
2660
2619
  target: link.target,
2661
2620
  kind: link.kind,
@@ -2669,25 +2628,29 @@ var nameReservedAnalyzer = {
2669
2628
  return issues;
2670
2629
  }
2671
2630
  };
2672
- function linkWhereSuffix(link) {
2673
- return linkWhere(link, {
2674
- single: NAME_RESERVED_TEXTS.whereSingle,
2675
- plural: NAME_RESERVED_TEXTS.wherePlural
2676
- });
2631
+
2632
+ // plugins/core/stability.ts
2633
+ var STABILITY_VALUES = ["experimental", "stable", "deprecated"];
2634
+ function isStability(value) {
2635
+ return value === "experimental" || value === "stable" || value === "deprecated";
2636
+ }
2637
+ function readEffectiveStability(node) {
2638
+ const fromAnn = node.sidecar?.annotations?.["stability"];
2639
+ if (isStability(fromAnn)) return fromAnn;
2640
+ const legacy = readLegacyMetadataStability(node.frontmatter);
2641
+ return isStability(legacy) ? legacy : null;
2642
+ }
2643
+ function readLegacyMetadataStability(fm) {
2644
+ if (!fm) return void 0;
2645
+ const meta = fm["metadata"];
2646
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0;
2647
+ return meta["stability"];
2677
2648
  }
2678
2649
 
2679
2650
  // plugins/core/analyzers/node-stability/text.ts
2680
2651
  var NODE_STABILITY_TEXTS = {
2681
- /** Label of the inspector action button that sets the lifecycle stage. */
2682
- setLabel: "Set stability",
2683
- /** Prompt label for the enum-pick stability input. */
2684
- promptLabel: "Stability",
2685
- /** Prompt option label for the `experimental` stage. */
2686
- optionExperimental: "Experimental",
2687
- /** Prompt option label for the `stable` stage. */
2688
- optionStable: "Stable",
2689
- /** Prompt option label for the `deprecated` stage. */
2690
- optionDeprecated: "Deprecated"
2652
+ /** Issue body (`<what>; <why>`) for a deprecated-marked node. */
2653
+ deprecated: "Marked deprecated; avoid using it"
2691
2654
  };
2692
2655
 
2693
2656
  // plugins/core/analyzers/node-stability/index.ts
@@ -2708,159 +2671,63 @@ var deprecated = {
2708
2671
  emitWhenEmpty: false,
2709
2672
  priority: 10
2710
2673
  };
2711
- var setStabilityButton = {
2712
- slot: "inspector.action.button",
2713
- priority: 15
2714
- };
2715
2674
  var nodeStabilityAnalyzer = {
2716
2675
  id: ID19,
2717
2676
  pluginId: CORE_PLUGIN_ID,
2718
2677
  kind: "analyzer",
2719
- description: "Reports a node's stability stage (`experimental`, `deprecated`) on the card.",
2678
+ description: "Surfaces a node's stability stage on the card: `deprecated` as a chip plus a finding, `experimental` as a chip only; `stable` and unset stay silent.",
2720
2679
  mode: "deterministic",
2721
- ui: { experimental, deprecated, setStabilityButton },
2680
+ ui: { experimental, deprecated },
2722
2681
  evaluate(ctx) {
2723
2682
  const issues = [];
2724
2683
  for (const node of ctx.nodes) {
2725
- const stability = readStability(node);
2726
- if (node.sidecar?.present === true) {
2727
- emitSetStabilityButton(ctx, node.path, stability ?? "stable");
2728
- }
2684
+ const stability = readEffectiveStability(node);
2729
2685
  if (stability === "experimental") {
2730
2686
  ctx.emitContribution(node.path, experimental, {
2731
2687
  value: 0,
2732
2688
  tooltip: EXPERIMENTAL_TOOLTIP
2733
2689
  });
2734
- issues.push({
2735
- analyzerId: ID19,
2736
- severity: "info",
2737
- nodeIds: [node.path],
2738
- message: `Node '${node.path}' is marked experimental: API may change.`,
2739
- data: { stability }
2740
- });
2741
- } else if (stability === "deprecated") {
2742
- ctx.emitContribution(node.path, deprecated, {
2743
- value: 0,
2744
- tooltip: DEPRECATED_TOOLTIP,
2745
- severity: "warn"
2746
- });
2747
- issues.push({
2748
- analyzerId: ID19,
2749
- severity: "warn",
2750
- nodeIds: [node.path],
2751
- message: `Node '${node.path}' is marked deprecated: avoid in new code.`,
2752
- data: { stability }
2753
- });
2754
- }
2755
- }
2756
- return issues;
2757
- }
2758
- };
2759
- function readStability(node) {
2760
- const fromAnn = node.sidecar?.annotations?.["stability"];
2761
- if (isStability(fromAnn)) return fromAnn;
2762
- const legacy = readLegacyMetadataStability(node.frontmatter);
2763
- return isStability(legacy) ? legacy : null;
2764
- }
2765
- function readLegacyMetadataStability(fm) {
2766
- if (!fm) return void 0;
2767
- const meta = fm["metadata"];
2768
- if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0;
2769
- return meta["stability"];
2770
- }
2771
- function isStability(value) {
2772
- return value === "experimental" || value === "deprecated" || value === "stable";
2773
- }
2774
- function emitSetStabilityButton(ctx, nodePath, current) {
2775
- ctx.emitContribution(nodePath, setStabilityButton, {
2776
- actionId: "core/node-set-stability",
2777
- label: NODE_STABILITY_TEXTS.setLabel,
2778
- icon: "pi-flag",
2779
- enabled: true,
2780
- prompt: {
2781
- inputType: "enum-pick",
2782
- paramKey: "stability",
2783
- label: NODE_STABILITY_TEXTS.promptLabel,
2784
- options: [
2785
- { value: "experimental", label: NODE_STABILITY_TEXTS.optionExperimental },
2786
- { value: "stable", label: NODE_STABILITY_TEXTS.optionStable },
2787
- { value: "deprecated", label: NODE_STABILITY_TEXTS.optionDeprecated }
2788
- ],
2789
- defaultValue: current
2790
- }
2791
- });
2792
- }
2793
-
2794
- // plugins/core/analyzers/node-superseded/text.ts
2795
- var NODE_SUPERSEDED_TEXTS = {
2796
- /**
2797
- * Compact finding grammar: line 1 = the superseding artifact, line
2798
- * 2 = what it means. The superseded node is the finding's own node,
2799
- * so its path never appears in the message.
2800
- */
2801
- message: "{{supersededBy}}:\nSupersedes this node."
2802
- };
2803
-
2804
- // plugins/core/analyzers/node-superseded/index.ts
2805
- var ID20 = "node-superseded";
2806
- var nodeSupersededAnalyzer = {
2807
- id: ID20,
2808
- pluginId: CORE_PLUGIN_ID,
2809
- kind: "analyzer",
2810
- description: "Marks nodes replaced by a newer one via `supersededBy`.",
2811
- // Part of the experimental supersession feature: ships disabled by
2812
- // default alongside the declarer (`core/supersede` button +
2813
- // `core/node-supersede` action). With the declarer off by default a
2814
- // user rarely produces `supersededBy` data, so surfacing it stays
2815
- // experimental too; the operator opts the whole family in together.
2816
- stability: "experimental",
2817
- mode: "deterministic",
2818
- evaluate(ctx) {
2819
- const issues = [];
2820
- for (const node of ctx.nodes) {
2821
- const supersededBy = pickSupersededBy(node);
2822
- if (supersededBy === null) continue;
2823
- issues.push({
2824
- analyzerId: ID20,
2825
- severity: "info",
2826
- nodeIds: [node.path],
2827
- message: tx(NODE_SUPERSEDED_TEXTS.message, {
2828
- path: node.path,
2829
- supersededBy
2830
- }),
2831
- data: { supersededBy }
2832
- });
2690
+ } else if (stability === "deprecated") {
2691
+ ctx.emitContribution(node.path, deprecated, {
2692
+ value: 0,
2693
+ tooltip: DEPRECATED_TOOLTIP,
2694
+ severity: "warn"
2695
+ });
2696
+ issues.push({
2697
+ analyzerId: ID19,
2698
+ severity: "warn",
2699
+ nodeIds: [node.path],
2700
+ message: formatFinding({ body: tx(NODE_STABILITY_TEXTS.deprecated) }),
2701
+ data: { stability }
2702
+ });
2703
+ }
2833
2704
  }
2834
2705
  return issues;
2835
2706
  }
2836
2707
  };
2837
- function pickSupersededBy(node) {
2838
- const sidecar = node.sidecar;
2839
- if (!sidecar || sidecar.present !== true) return null;
2840
- const ann = sidecar.annotations;
2841
- if (!ann || typeof ann !== "object" || Array.isArray(ann)) return null;
2842
- const value = ann["supersededBy"];
2843
- if (typeof value !== "string" || value.length === 0) return null;
2844
- return value;
2845
- }
2846
2708
 
2847
2709
  // plugins/core/analyzers/reference-broken/index.ts
2848
- import { resolve as resolve3 } from "path";
2710
+ import { resolve as resolve2 } from "path";
2849
2711
 
2850
2712
  // plugins/core/analyzers/reference-broken/text.ts
2851
2713
  var REFERENCE_BROKEN_TEXTS = {
2852
2714
  /**
2853
- * Compact finding grammar: line 1 = the unresolved target, line 2 =
2854
- * the short diagnosis plus WHERE the reference sits (`{{where}}` is
2855
- * the pre-rendered location suffix below, or empty when the link
2856
- * carries no line info). The source is the finding's own node, so it
2857
- * never appears in the message.
2715
+ * Diagnosis body (`<what>; <why>`). The shared `formatFinding` helper
2716
+ * wraps it with the backtick subject (the unresolved target) and the
2717
+ * `L<line>:` location prefix; the source is the finding's own node, so
2718
+ * it never appears in the message.
2719
+ */
2720
+ message: "Broken {{kindLabel}}; target not found in the graph or on disk",
2721
+ /**
2722
+ * Remediation hint surfaced via `Issue.fix.summary`. Not autofixable:
2723
+ * the rule cannot tell which resolution the author wants. The folder
2724
+ * option maps to `scan.referencePaths` ("Folders for link validation"
2725
+ * in Settings), the rule's own escape hatch: it clears only PATH-style
2726
+ * breaks (the file exists on disk outside the indexed graph). A
2727
+ * trigger-style `/cmd` / `@agent` break is settled by the path/name or
2728
+ * removal options instead.
2858
2729
  */
2859
- message: "{{target}}:\nBroken {{kindLabel}}{{where}}.",
2860
- /** Location suffix, one detection site. */
2861
- whereSingle: " (line {{lines}})",
2862
- /** Location suffix, several detection sites. */
2863
- wherePlural: " (lines {{lines}})",
2730
+ fixSummary: "Fix the path or name, remove the broken link, or add its folder under Folders for link validation.",
2864
2731
  /**
2865
2732
  * Human noun per link kind for the message above. Fallback for an
2866
2733
  * off-catalog kind: `<kind> link` (composed in the analyzer).
@@ -2869,7 +2736,6 @@ var REFERENCE_BROKEN_TEXTS = {
2869
2736
  references: "reference",
2870
2737
  mentions: "mention",
2871
2738
  invokes: "invocation",
2872
- supersedes: "supersession",
2873
2739
  points: "pointer"
2874
2740
  },
2875
2741
  kindLabelFallback: "{{kind}} link",
@@ -2880,13 +2746,14 @@ var REFERENCE_BROKEN_TEXTS = {
2880
2746
  };
2881
2747
 
2882
2748
  // plugins/core/analyzers/reference-broken/index.ts
2883
- var ID21 = "reference-broken";
2749
+ var ID20 = "reference-broken";
2884
2750
  var referenceBrokenAnalyzer = {
2885
- id: ID21,
2751
+ id: ID20,
2886
2752
  pluginId: CORE_PLUGIN_ID,
2887
2753
  kind: "analyzer",
2888
2754
  description: "Flags arrows pointing at a node not part of the current scan.",
2889
2755
  mode: "deterministic",
2756
+ phase: "score",
2890
2757
  // No `ui` declaration: this analyzer used to emit a per-finding
2891
2758
  // counter chip on `card.footer.right`, but that chip duplicated the
2892
2759
  // aggregate severity counters now owned by `core/issue-counter`. The
@@ -2902,10 +2769,12 @@ var referenceBrokenAnalyzer = {
2902
2769
  const broken = ctx.brokenLinks;
2903
2770
  if (!broken || broken.size === 0) return [];
2904
2771
  const refIndex = buildReferenceIndex(ctx);
2772
+ const adjust = ctx.adjustConfidence;
2905
2773
  const issues = [];
2906
2774
  for (const link of ctx.links) {
2907
2775
  if (!broken.has(link)) continue;
2908
2776
  if (refIndex && resolvesViaReferencePaths(link, refIndex)) continue;
2777
+ penalizeBrokenConfidence(adjust, link);
2909
2778
  issues.push(buildIssue(link));
2910
2779
  }
2911
2780
  return issues;
@@ -2915,9 +2784,14 @@ function buildReferenceIndex(ctx) {
2915
2784
  if (!ctx.referenceablePaths || ctx.referenceablePaths.size === 0 || !ctx.cwd) return null;
2916
2785
  return { paths: ctx.referenceablePaths, cwd: ctx.cwd };
2917
2786
  }
2787
+ function penalizeBrokenConfidence(adjust, link) {
2788
+ if (adjust) {
2789
+ adjust(link, { kind: "delta", value: -BROKEN_PENALTY });
2790
+ }
2791
+ }
2918
2792
  function buildIssue(link) {
2919
2793
  return {
2920
- analyzerId: ID21,
2794
+ analyzerId: ID20,
2921
2795
  // `error`, not `warn`: a link whose target is not in the scan is a
2922
2796
  // structural defect the operator must notice, and the card chip
2923
2797
  // paints `danger` (red) to match. Per the chip-vs-issue policy in
@@ -2926,14 +2800,14 @@ function buildIssue(link) {
2926
2800
  // and the global error count on the card.
2927
2801
  severity: "error",
2928
2802
  nodeIds: [link.source],
2929
- message: tx(REFERENCE_BROKEN_TEXTS.message, {
2930
- target: link.target,
2931
- kindLabel: REFERENCE_BROKEN_TEXTS.kindLabels[link.kind] ?? tx(REFERENCE_BROKEN_TEXTS.kindLabelFallback, { kind: link.kind }),
2932
- where: linkWhere(link, {
2933
- single: REFERENCE_BROKEN_TEXTS.whereSingle,
2934
- plural: REFERENCE_BROKEN_TEXTS.wherePlural
2803
+ message: formatFinding({
2804
+ subject: link.target,
2805
+ lines: linkLines(link),
2806
+ body: tx(REFERENCE_BROKEN_TEXTS.message, {
2807
+ kindLabel: REFERENCE_BROKEN_TEXTS.kindLabels[link.kind] ?? tx(REFERENCE_BROKEN_TEXTS.kindLabelFallback, { kind: link.kind })
2935
2808
  })
2936
2809
  }),
2810
+ fix: { summary: tx(REFERENCE_BROKEN_TEXTS.fixSummary) },
2937
2811
  data: {
2938
2812
  target: link.target,
2939
2813
  kind: link.kind,
@@ -2943,7 +2817,7 @@ function buildIssue(link) {
2943
2817
  }
2944
2818
  function resolvesViaReferencePaths(link, refIndex) {
2945
2819
  if (!isPathStyleLink(link)) return false;
2946
- return refIndex.paths.has(resolve3(refIndex.cwd, link.target));
2820
+ return refIndex.paths.has(resolve2(refIndex.cwd, link.target));
2947
2821
  }
2948
2822
  function isPathStyleLink(link) {
2949
2823
  const sigil = link.trigger?.normalizedTrigger?.charAt(0);
@@ -2954,29 +2828,36 @@ function isPathStyleLink(link) {
2954
2828
  // plugins/core/analyzers/reference-redundant/text.ts
2955
2829
  var REFERENCE_REDUNDANT_TEXTS = {
2956
2830
  /**
2957
- * Compact finding grammar (subject first, `\n` renders as a line
2958
- * break in the inspector and flattens to a space in `sm check`):
2959
- *
2960
- * <resolvedTarget>:
2961
- * Duplicate reference (2): `references/x.md` (124, 145).
2962
- *
2831
+ * Diagnosis body (`<what>; <why>`). Kind-agnostic wording ("links", not
2832
+ * "reference"): the redundancy can span different link kinds (e.g.
2833
+ * `invokes` + `references` to one node), so the message never names a
2834
+ * single kind. The shared `formatFinding` helper wraps it with the
2835
+ * backtick subject (the resolved target); no `L<line>:` prefix because
2836
+ * the per-occurrence line numbers stay inline in the rendered
2837
+ * occurrence list. Remediation lives in `fix.summary`, not the message.
2963
2838
  * Occurrences are grouped BY TRIGGER: each distinct trigger text
2964
- * appears once with its line numbers collapsed into one paren list.
2965
- * The source node is the finding's own node, so it never appears.
2839
+ * appears once with its line numbers collapsed into one paren list. The
2840
+ * source node is the finding's own node, so it never appears.
2966
2841
  */
2967
- message: "{{resolvedTarget}}:\nDuplicate reference ({{count}}): {{occurrences}}.",
2842
+ message: "Redundant links; the target is reached {{count}} times: {{occurrences}}",
2843
+ /**
2844
+ * Remediation hint surfaced via `Issue.fix.summary`. Phrased as
2845
+ * optional: severity is `info` and keeping multiple forms can be
2846
+ * deliberate, so the hint offers consolidation OR keeping the overlap.
2847
+ */
2848
+ fixSummary: "Consolidate the links into one, or keep the overlap deliberately.",
2968
2849
  /** Inline separator between trigger groups in the message. */
2969
2850
  occurrenceSeparator: ", ",
2970
2851
  /** Per-trigger formatting: the trigger once, its lines grouped. */
2971
- occurrence: "`{{trigger}}` ({{lines}})",
2852
+ occurrence: "`{{trigger}}` (L{{lines}})",
2972
2853
  /** Placeholder for an occurrence whose extractor recorded no line. */
2973
2854
  lineUnknown: "?"
2974
2855
  };
2975
2856
 
2976
2857
  // plugins/core/analyzers/reference-redundant/index.ts
2977
- var ID22 = "reference-redundant";
2858
+ var ID21 = "reference-redundant";
2978
2859
  var referenceRedundantAnalyzer = {
2979
- id: ID22,
2860
+ id: ID21,
2980
2861
  pluginId: CORE_PLUGIN_ID,
2981
2862
  kind: "analyzer",
2982
2863
  description: "Flags when one node references the same target through two or more different links (e.g. a markdown link plus a `references:` entry).",
@@ -2999,14 +2880,17 @@ var referenceRedundantAnalyzer = {
2999
2880
  const [source, resolvedTarget] = key.split("\0");
3000
2881
  const flat = flattenOccurrences(links);
3001
2882
  issues.push({
3002
- analyzerId: ID22,
2883
+ analyzerId: ID21,
3003
2884
  severity: "info",
3004
2885
  nodeIds: [source],
3005
- message: tx(REFERENCE_REDUNDANT_TEXTS.message, {
3006
- resolvedTarget,
3007
- count: flat.length,
3008
- occurrences: formatGroupedOccurrences(flat)
2886
+ message: formatFinding({
2887
+ subject: resolvedTarget,
2888
+ body: tx(REFERENCE_REDUNDANT_TEXTS.message, {
2889
+ count: flat.length,
2890
+ occurrences: formatGroupedOccurrences(flat)
2891
+ })
3009
2892
  }),
2893
+ fix: { summary: tx(REFERENCE_REDUNDANT_TEXTS.fixSummary) },
3010
2894
  data: {
3011
2895
  target: resolvedTarget,
3012
2896
  resolvedTarget,
@@ -3069,7 +2953,7 @@ function formatGroupedOccurrences(occurrences) {
3069
2953
 
3070
2954
  // kernel/adapters/schema-validators.ts
3071
2955
  import { readFileSync as readFileSync2 } from "fs";
3072
- import { dirname as dirname2, resolve as resolve4 } from "path";
2956
+ import { dirname as dirname2, resolve as resolve3 } from "path";
3073
2957
  import { createRequire as createRequire2 } from "module";
3074
2958
  import { Ajv2020 as Ajv20202 } from "ajv/dist/2020.js";
3075
2959
 
@@ -3137,14 +3021,14 @@ function buildSchemaValidators() {
3137
3021
  });
3138
3022
  applyAjvFormats(ajv);
3139
3023
  for (const rel of SUPPORTING_SCHEMAS) {
3140
- const file = resolve4(specRoot, rel);
3024
+ const file = resolve3(specRoot, rel);
3141
3025
  if (!existsSyncSafe(file)) continue;
3142
3026
  const schema = JSON.parse(readFileSync2(file, "utf8"));
3143
3027
  ajv.addSchema(schema);
3144
3028
  }
3145
3029
  const validators = /* @__PURE__ */ new Map();
3146
3030
  for (const [name, rel] of Object.entries(SCHEMA_FILES)) {
3147
- const file = resolve4(specRoot, rel);
3031
+ const file = resolve3(specRoot, rel);
3148
3032
  const schema = JSON.parse(readFileSync2(file, "utf8"));
3149
3033
  const byId = typeof schema.$id === "string" ? ajv.getSchema(schema.$id) : void 0;
3150
3034
  validators.set(name, byId ?? ajv.compile(schema));
@@ -3216,7 +3100,7 @@ function buildProviderFrontmatterValidator(providers) {
3216
3100
  allowUnionTypes: true
3217
3101
  });
3218
3102
  applyAjvFormats(ajv);
3219
- const baseFile = resolve4(specRoot, "schemas/frontmatter/base.schema.json");
3103
+ const baseFile = resolve3(specRoot, "schemas/frontmatter/base.schema.json");
3220
3104
  const baseSchema = JSON.parse(readFileSync2(baseFile, "utf8"));
3221
3105
  ajv.addSchema(baseSchema);
3222
3106
  registerProviderAuxiliarySchemas(ajv, providers);
@@ -3315,16 +3199,25 @@ function existsSyncSafe(path) {
3315
3199
  }
3316
3200
  }
3317
3201
 
3202
+ // kernel/orchestrator/frontmatter-issue-ids.ts
3203
+ var FRONTMATTER_ISSUE_ANALYZERS = /* @__PURE__ */ new Set([
3204
+ "frontmatter-invalid",
3205
+ "frontmatter-malformed",
3206
+ "frontmatter-parse-error"
3207
+ ]);
3208
+
3318
3209
  // plugins/core/analyzers/schema-violation/text.ts
3319
3210
  var SCHEMA_VIOLATION_TEXTS = {
3320
- // Compact finding grammar: the affected node (or the link's source)
3321
- // is the finding's own node, so its path never appears.
3322
- /** `Schema validation failed: <errors>` */
3323
- nodeFailure: "Schema validation failed: {{errors}}",
3324
- /** `<target>:\nLink failed schema validation: <errors>` */
3325
- linkFailure: "{{target}}:\nLink failed schema validation: {{errors}}",
3326
- /** `Missing required frontmatter: <missing>.` */
3327
- frontmatterBaseFailure: "Missing required frontmatter: {{missing}}.",
3211
+ // Diagnosis bodies (`<what>; <why>`). The shared `formatFinding` helper
3212
+ // owns the subject / location chrome: the node + frontmatter findings
3213
+ // carry no subject (the affected node IS the finding's own node), the
3214
+ // link finding uses the link target as its subject.
3215
+ /** `Schema validation failed; <errors>` */
3216
+ nodeFailure: "Schema validation failed; {{errors}}",
3217
+ /** `<target>` subject + `Link failed schema validation; <errors>` */
3218
+ linkFailure: "Link failed schema validation; {{errors}}",
3219
+ /** `Missing required frontmatter; <missing>` */
3220
+ frontmatterBaseFailure: "Missing required frontmatter; {{missing}}",
3328
3221
  /** Singular tooltip on the alert / chip when a node has exactly one validation failure. */
3329
3222
  alertTooltipSingle: "Frontmatter or schema validation failed.",
3330
3223
  /** Plural tooltip; `{{count}}` capped at 99 in the chip badge but the tooltip text shows the raw count. */
@@ -3332,9 +3225,9 @@ var SCHEMA_VIOLATION_TEXTS = {
3332
3225
  };
3333
3226
 
3334
3227
  // plugins/core/analyzers/schema-violation/index.ts
3335
- var ID23 = "schema-violation";
3228
+ var ID22 = "schema-violation";
3336
3229
  var schemaViolationAnalyzer = {
3337
- id: ID23,
3230
+ id: ID22,
3338
3231
  pluginId: CORE_PLUGIN_ID,
3339
3232
  kind: "analyzer",
3340
3233
  description: "Flags nodes or links that violate the project schemas.",
@@ -3355,10 +3248,11 @@ var schemaViolationAnalyzer = {
3355
3248
  const validators = loadSchemaValidators();
3356
3249
  const findings = [];
3357
3250
  const perNode = /* @__PURE__ */ new Map();
3251
+ const kernelFlaggedNodes = collectKernelFlaggedNodes(ctx.accumulatedIssues);
3358
3252
  for (const node of ctx.nodes) {
3359
3253
  const before = findings.length;
3360
3254
  collectNodeFindings(validators, node, findings);
3361
- collectFrontmatterBaseFindings(node, findings);
3255
+ collectFrontmatterBaseFindings(node, findings, kernelFlaggedNodes);
3362
3256
  if (findings.length > before) {
3363
3257
  let worst = "warn";
3364
3258
  for (let i = before; i < findings.length; i++) {
@@ -3385,17 +3279,27 @@ function collectNodeFindings(v, node, out) {
3385
3279
  const result = v.validate("node", toNodeForSchema(node));
3386
3280
  if (result.ok) return;
3387
3281
  out.push({
3388
- analyzerId: ID23,
3282
+ analyzerId: ID22,
3389
3283
  severity: "error",
3390
3284
  nodeIds: [node.path],
3391
- message: tx(SCHEMA_VIOLATION_TEXTS.nodeFailure, {
3392
- path: node.path,
3393
- errors: result.errors
3285
+ message: formatFinding({
3286
+ body: tx(SCHEMA_VIOLATION_TEXTS.nodeFailure, {
3287
+ errors: result.errors
3288
+ })
3394
3289
  }),
3395
3290
  data: { target: "node", path: node.path }
3396
3291
  });
3397
3292
  }
3398
- function collectFrontmatterBaseFindings(node, out) {
3293
+ function collectKernelFlaggedNodes(accumulated) {
3294
+ const flagged = /* @__PURE__ */ new Set();
3295
+ for (const issue of accumulated ?? []) {
3296
+ if (!FRONTMATTER_ISSUE_ANALYZERS.has(issue.analyzerId)) continue;
3297
+ for (const id of issue.nodeIds) flagged.add(id);
3298
+ }
3299
+ return flagged;
3300
+ }
3301
+ function collectFrontmatterBaseFindings(node, out, kernelFlagged) {
3302
+ if (kernelFlagged.has(node.path)) return;
3399
3303
  if (node.provider === "markdown") return;
3400
3304
  if (node.bytes.frontmatter === 0) return;
3401
3305
  const fm = node.frontmatter ?? {};
@@ -3404,16 +3308,17 @@ function collectFrontmatterBaseFindings(node, out) {
3404
3308
  if (isMissingStringField(fm, "description")) missing.push("description");
3405
3309
  if (missing.length === 0) return;
3406
3310
  out.push({
3407
- analyzerId: ID23,
3311
+ analyzerId: ID22,
3408
3312
  // `warn` (not `error`) so the default `sm scan` exit code stays
3409
3313
  // 0 even when nodes are missing frontmatter base fields. Strict
3410
3314
  // mode (`sm scan --strict`) still escalates to exit 1. Matches
3411
3315
  // the `frontmatter-invalid` severity policy of the orchestrator.
3412
3316
  severity: "warn",
3413
3317
  nodeIds: [node.path],
3414
- message: tx(SCHEMA_VIOLATION_TEXTS.frontmatterBaseFailure, {
3415
- path: node.path,
3416
- missing: missing.join(", ")
3318
+ message: formatFinding({
3319
+ body: tx(SCHEMA_VIOLATION_TEXTS.frontmatterBaseFailure, {
3320
+ missing: missing.join(", ")
3321
+ })
3417
3322
  }),
3418
3323
  data: { target: "frontmatter", path: node.path, missing }
3419
3324
  });
@@ -3426,13 +3331,14 @@ function collectLinkFindings(v, link, out) {
3426
3331
  const result = v.validate("link", toLinkForSchema(link));
3427
3332
  if (result.ok) return;
3428
3333
  out.push({
3429
- analyzerId: ID23,
3334
+ analyzerId: ID22,
3430
3335
  severity: "error",
3431
3336
  nodeIds: [link.source],
3432
- message: tx(SCHEMA_VIOLATION_TEXTS.linkFailure, {
3433
- source: link.source,
3434
- target: link.target,
3435
- errors: result.errors
3337
+ message: formatFinding({
3338
+ subject: link.target,
3339
+ body: tx(SCHEMA_VIOLATION_TEXTS.linkFailure, {
3340
+ errors: result.errors
3341
+ })
3436
3342
  }),
3437
3343
  data: { target: "link", source: link.source, to: link.target }
3438
3344
  });
@@ -3466,282 +3372,6 @@ function toLinkForSchema(link) {
3466
3372
  };
3467
3373
  }
3468
3374
 
3469
- // plugins/core/analyzers/signal-collision/text.ts
3470
- var SIGNAL_COLLISION_TEXTS = {
3471
- /**
3472
- * Per-Signal warn issue: two extractors detected something at
3473
- * overlapping byte ranges within the same node and the resolver
3474
- * dropped the loser. Surfaces WHO lost, WHO won, and the tiebreak
3475
- * reason so the operator can understand why a candidate edge did NOT
3476
- * become a Link.
3477
- *
3478
- * Placeholders are deliberately verbose because this is one of the
3479
- * few diagnostic surfaces where the operator may need to disambiguate
3480
- * a confusing graph (e.g. a `[link](path)` followed by `@path` inside
3481
- * the same paragraph, the markdown-link wins and the at-directive
3482
- * silently disappears without this warning).
3483
- */
3484
- message: "`{{loserRaw}}`:\nOverlap collision: {{loserExtractor}} (at {{loserRange}}) lost to {{winnerExtractor}} (at {{winnerRange}}) by {{reason}}; only the winning edge persists.",
3485
- /**
3486
- * Same warn but for the rare case the resolver rejected a Signal
3487
- * because the operator disabled its extractor via
3488
- * `plugins.<id>.extensions.<extId>.enabled`. Phase 4+ stub: today the
3489
- * filter is not wired so this template is unreachable from the
3490
- * resolver; documented now so the analyzer stays forward-compatible
3491
- * with the upcoming filter pass.
3492
- */
3493
- messageExtractorDisabled: "`{{loserRaw}}`:\nDropped: extension `{{extractorId}}` is disabled. Re-enable it in Settings or via `sm plugins enable`.",
3494
- /**
3495
- * Same warn but for the future confidence floor case. Phase 4+ stub:
3496
- * today the resolver materialises every winning candidate regardless
3497
- * of confidence, so this template is unreachable; documented for
3498
- * forward compatibility.
3499
- */
3500
- messageBelowFloor: "`{{loserRaw}}`:\nDropped: confidence {{confidence}} is below the threshold {{threshold}}."
3501
- };
3502
-
3503
- // plugins/core/analyzers/signal-collision/index.ts
3504
- var ID24 = "signal-collision";
3505
- var signalCollisionAnalyzer = {
3506
- id: ID24,
3507
- pluginId: CORE_PLUGIN_ID,
3508
- kind: "analyzer",
3509
- description: "Reports when two extractors fight over the same span of body text, or when a candidate link is dropped (extractor disabled, confidence too low) before it reaches the graph.",
3510
- mode: "deterministic",
3511
- evaluate(ctx) {
3512
- const signals = ctx.signals;
3513
- if (!signals || signals.length === 0) return [];
3514
- const issues = [];
3515
- for (const signal of signals) {
3516
- const issue = makeIssue(signal);
3517
- if (issue) issues.push(issue);
3518
- }
3519
- return issues;
3520
- }
3521
- };
3522
- function makeIssue(signal) {
3523
- const resolution = signal.resolution;
3524
- if (!resolution || resolution.outcome !== "rejected") return null;
3525
- if (resolution.rejectedBy) {
3526
- const winner = resolution.rejectedBy;
3527
- const winnerCandidate = signal.candidates[resolution.winnerIndex ?? 0];
3528
- const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
3529
- const winnerRange = `${winner.range.start}-${winner.range.end}`;
3530
- return {
3531
- analyzerId: ID24,
3532
- severity: "warn",
3533
- nodeIds: [signal.source],
3534
- message: tx(SIGNAL_COLLISION_TEXTS.message, {
3535
- loserExtractor: winnerCandidate.extractorId,
3536
- loserRaw: signal.raw,
3537
- loserRange,
3538
- winnerExtractor: winner.extractorId,
3539
- winnerRange,
3540
- reason: winner.reason
3541
- }),
3542
- data: {
3543
- loser: {
3544
- extractorId: winnerCandidate.extractorId,
3545
- raw: signal.raw,
3546
- range: signal.range ?? null,
3547
- candidate: {
3548
- kind: winnerCandidate.kind,
3549
- target: winnerCandidate.target,
3550
- confidence: winnerCandidate.confidence
3551
- }
3552
- },
3553
- winner: {
3554
- extractorId: winner.extractorId,
3555
- range: winner.range
3556
- },
3557
- reason: winner.reason
3558
- }
3559
- };
3560
- }
3561
- if (resolution.extractorDisabled) {
3562
- const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
3563
- return {
3564
- analyzerId: ID24,
3565
- severity: "warn",
3566
- nodeIds: [signal.source],
3567
- message: tx(SIGNAL_COLLISION_TEXTS.messageExtractorDisabled, {
3568
- extractorId: resolution.extractorDisabled.extractorId,
3569
- loserRaw: signal.raw,
3570
- loserRange
3571
- }),
3572
- data: {
3573
- extractorDisabled: resolution.extractorDisabled,
3574
- raw: signal.raw,
3575
- range: signal.range ?? null
3576
- }
3577
- };
3578
- }
3579
- if (resolution.belowFloor) {
3580
- const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
3581
- const topCandidate = signal.candidates[0];
3582
- return {
3583
- analyzerId: ID24,
3584
- severity: "warn",
3585
- nodeIds: [signal.source],
3586
- message: tx(SIGNAL_COLLISION_TEXTS.messageBelowFloor, {
3587
- loserRaw: signal.raw,
3588
- loserRange,
3589
- confidence: topCandidate.confidence,
3590
- threshold: resolution.belowFloor.threshold
3591
- }),
3592
- data: {
3593
- belowFloor: resolution.belowFloor,
3594
- raw: signal.raw,
3595
- range: signal.range ?? null
3596
- }
3597
- };
3598
- }
3599
- return null;
3600
- }
3601
-
3602
- // plugins/core/analyzers/trigger-collision/text.ts
3603
- var TRIGGER_COLLISION_TEXTS = {
3604
- /**
3605
- * Top-level message when `analyzeTriggerBucket` accumulated exactly one
3606
- * cause part. Used for the advertiser-ambiguous-only, invocation-
3607
- * ambiguous-only, and cross-kind-only branches.
3608
- */
3609
- messageOnePart: '"{{normalized}}":\nTrigger collision: {{part}}.',
3610
- /**
3611
- * Top-level message when `analyzeTriggerBucket` accumulated two cause
3612
- * parts (advertiser-ambiguous AND invocation-ambiguous fire together).
3613
- * The joiner lives inside the template so future locales can adapt it
3614
- * (e.g. `'; y '` in Spanish) without touching the rule code.
3615
- */
3616
- messageTwoParts: '"{{normalized}}":\nTrigger collision: {{first}}; and {{second}}.',
3617
- /** `<n> advertisers: <list>` part, fires on the advertiser-ambiguous branch. */
3618
- partAdvertisers: "{{count}} advertisers: {{paths}}",
3619
- /** `<n> invocation forms: <list>` part, fires on the invocation-ambiguous branch. */
3620
- partInvocations: "{{count}} invocation forms: {{forms}}",
3621
- /** Singular cross-kind cause: `non-canonical invocation <form> against advertiser <path>`. */
3622
- partNonCanonicalSingular: "non-canonical invocation {{forms}} against advertiser {{advertiser}}",
3623
- /** Plural cross-kind cause: `non-canonical invocations <forms> against advertiser <path>`. */
3624
- partNonCanonicalPlural: "non-canonical invocations {{forms}} against advertiser {{advertiser}}"
3625
- };
3626
-
3627
- // plugins/core/analyzers/trigger-collision/index.ts
3628
- var ID25 = "trigger-collision";
3629
- var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
3630
- "command",
3631
- "skill",
3632
- "agent"
3633
- ]);
3634
- var triggerCollisionAnalyzer = {
3635
- id: ID25,
3636
- pluginId: CORE_PLUGIN_ID,
3637
- kind: "analyzer",
3638
- mode: "deterministic",
3639
- description: "Flags two or more nodes that claim the same `/command` or `@agent` name.",
3640
- // Two claim-collection passes (advertisement + invocation) feeding
3641
- // the bucket map. Per-bucket analysis lives in `analyzeTriggerBucket`.
3642
- // eslint-disable-next-line complexity
3643
- evaluate(ctx) {
3644
- const buckets = /* @__PURE__ */ new Map();
3645
- const push = (key, claim) => {
3646
- const bucket = buckets.get(key) ?? [];
3647
- bucket.push(claim);
3648
- buckets.set(key, bucket);
3649
- };
3650
- for (const node of ctx.nodes) {
3651
- if (!ADVERTISING_KINDS.has(node.kind)) continue;
3652
- const raw = node.frontmatter?.["name"];
3653
- if (typeof raw !== "string" || raw.length === 0) continue;
3654
- const normalized = `/${normalizeTrigger(raw)}`;
3655
- if (normalized === "/") continue;
3656
- push(normalized, {
3657
- kind: "advertiser",
3658
- token: node.path,
3659
- nodeId: node.path,
3660
- canonicalForm: `/${raw}`
3661
- });
3662
- }
3663
- for (const link of ctx.links) {
3664
- const normalized = link.trigger?.normalizedTrigger;
3665
- if (!normalized) continue;
3666
- push(normalized, {
3667
- kind: "invocation",
3668
- token: link.target,
3669
- nodeId: link.source
3670
- });
3671
- }
3672
- const issues = [];
3673
- for (const [normalized, claims] of buckets) {
3674
- const issue = analyzeTriggerBucket(normalized, claims);
3675
- if (issue) issues.push(issue);
3676
- }
3677
- return issues;
3678
- }
3679
- };
3680
- function analyzeTriggerBucket(normalized, claims) {
3681
- const advertiserPaths = [
3682
- ...new Set(claims.filter((c) => c.kind === "advertiser").map((c) => c.token))
3683
- ].sort();
3684
- const invocationTargets = [
3685
- ...new Set(claims.filter((c) => c.kind === "invocation").map((c) => c.token))
3686
- ].sort();
3687
- const advertisers = claims.filter(
3688
- (c) => c.kind === "advertiser"
3689
- );
3690
- const advertiserAmbiguous = advertiserPaths.length >= 2;
3691
- const invocationAmbiguous = invocationTargets.length >= 2;
3692
- const canonicalForms = new Set(advertisers.map((a) => a.canonicalForm));
3693
- const nonCanonicalInvocations = invocationTargets.filter((t) => !canonicalForms.has(t));
3694
- const crossKindAmbiguous = advertiserPaths.length === 1 && nonCanonicalInvocations.length >= 1;
3695
- if (!advertiserAmbiguous && !invocationAmbiguous && !crossKindAmbiguous) {
3696
- return null;
3697
- }
3698
- const nodeIds = [...new Set(claims.map((c) => c.nodeId))].sort();
3699
- const parts = [];
3700
- if (advertiserAmbiguous) {
3701
- parts.push(
3702
- tx(TRIGGER_COLLISION_TEXTS.partAdvertisers, {
3703
- count: advertiserPaths.length,
3704
- paths: advertiserPaths.join(", ")
3705
- })
3706
- );
3707
- }
3708
- if (invocationAmbiguous) {
3709
- parts.push(
3710
- tx(TRIGGER_COLLISION_TEXTS.partInvocations, {
3711
- count: invocationTargets.length,
3712
- forms: invocationTargets.join(", ")
3713
- })
3714
- );
3715
- } else if (crossKindAmbiguous) {
3716
- const template = nonCanonicalInvocations.length > 1 ? TRIGGER_COLLISION_TEXTS.partNonCanonicalPlural : TRIGGER_COLLISION_TEXTS.partNonCanonicalSingular;
3717
- parts.push(
3718
- tx(template, {
3719
- forms: nonCanonicalInvocations.join(", "),
3720
- advertiser: advertiserPaths[0]
3721
- })
3722
- );
3723
- }
3724
- const message = parts.length === 2 ? tx(TRIGGER_COLLISION_TEXTS.messageTwoParts, {
3725
- normalized,
3726
- first: parts[0],
3727
- second: parts[1]
3728
- }) : tx(TRIGGER_COLLISION_TEXTS.messageOnePart, {
3729
- normalized,
3730
- part: parts[0]
3731
- });
3732
- return {
3733
- analyzerId: ID25,
3734
- severity: "error",
3735
- nodeIds,
3736
- message,
3737
- data: {
3738
- normalizedTrigger: normalized,
3739
- invocationTargets,
3740
- advertiserPaths
3741
- }
3742
- };
3743
- }
3744
-
3745
3375
  // kernel/util/safe-text.ts
3746
3376
  var ANSI_ESCAPE_RE = /[›][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
3747
3377
  var C0_CONTROL_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
@@ -3770,13 +3400,13 @@ var ASCII_FORMATTER_TEXTS = {
3770
3400
  };
3771
3401
 
3772
3402
  // plugins/core/formatters/ascii/index.ts
3773
- var ID26 = "ascii";
3403
+ var ID23 = "ascii";
3774
3404
  var KIND_ORDER = ["agent", "command", "skill", "markdown"];
3775
3405
  var asciiFormatter = {
3776
- id: ID26,
3406
+ id: ID23,
3777
3407
  pluginId: CORE_PLUGIN_ID,
3778
3408
  kind: "formatter",
3779
- formatId: ID26,
3409
+ formatId: ID23,
3780
3410
  description: "Renders the scan as plain text in three sections: nodes (grouped by kind), arrows, and issues. Used by `sm scan --format ascii`.",
3781
3411
  // ASCII tree formatter, header + per-kind sections + per-issue
3782
3412
  // section. Each section iterates and renders; splitting per section
@@ -3870,13 +3500,13 @@ function renderSection(out, kind, group) {
3870
3500
  }
3871
3501
 
3872
3502
  // plugins/core/formatters/json/index.ts
3873
- var ID27 = "json";
3503
+ var ID24 = "json";
3874
3504
  var jsonFormatter = {
3875
- id: ID27,
3505
+ id: ID24,
3876
3506
  pluginId: CORE_PLUGIN_ID,
3877
3507
  kind: "formatter",
3878
3508
  description: "Renders the persisted scan as JSON (conforms to `scan-result.schema.json`). Used by `sm graph --format json` and `GET /api/graph?format=json`.",
3879
- formatId: ID27,
3509
+ formatId: ID24,
3880
3510
  format(ctx) {
3881
3511
  if (ctx.scanResult !== void 0) {
3882
3512
  return JSON.stringify(ctx.scanResult);
@@ -3891,7 +3521,7 @@ var jsonFormatter = {
3891
3521
 
3892
3522
  // kernel/sidecar/parse.ts
3893
3523
  import { existsSync, readFileSync as readFileSync3 } from "fs";
3894
- import { dirname as dirname3, resolve as resolve5 } from "path";
3524
+ import { dirname as dirname3, resolve as resolve4 } from "path";
3895
3525
  import { createRequire as createRequire3 } from "module";
3896
3526
  import { Ajv2020 as Ajv20203 } from "ajv/dist/2020.js";
3897
3527
  import yaml from "js-yaml";
@@ -3993,10 +3623,10 @@ function getSidecarValidator() {
3993
3623
  applyAjvFormats(ajv);
3994
3624
  const specRoot = resolveSpecRoot2();
3995
3625
  const annotationsSchema = JSON.parse(
3996
- readFileSync3(resolve5(specRoot, "schemas/annotations.schema.json"), "utf8")
3626
+ readFileSync3(resolve4(specRoot, "schemas/annotations.schema.json"), "utf8")
3997
3627
  );
3998
3628
  const sidecarSchema = JSON.parse(
3999
- readFileSync3(resolve5(specRoot, "schemas/sidecar.schema.json"), "utf8")
3629
+ readFileSync3(resolve4(specRoot, "schemas/sidecar.schema.json"), "utf8")
4000
3630
  );
4001
3631
  ajv.addSchema(annotationsSchema);
4002
3632
  cachedSidecarValidator = ajv.compile(sidecarSchema);
@@ -4023,13 +3653,13 @@ var BUMP_TEXTS = {
4023
3653
  };
4024
3654
 
4025
3655
  // plugins/core/actions/node-bump/index.ts
4026
- var ID28 = "node-bump";
3656
+ var ID25 = "node-bump";
4027
3657
  var bumpButton = {
4028
3658
  slot: "inspector.action.button",
4029
3659
  priority: 10
4030
3660
  };
4031
3661
  var nodeBumpAction = {
4032
- id: ID28,
3662
+ id: ID25,
4033
3663
  pluginId: CORE_PLUGIN_ID,
4034
3664
  kind: "action",
4035
3665
  description: "Marks a node as updated: bumps `annotations.version`, refreshes sidecar hashes, and records the timestamp.",
@@ -4037,8 +3667,9 @@ var nodeBumpAction = {
4037
3667
  ui: { bumpButton },
4038
3668
  project(ctx) {
4039
3669
  for (const node of ctx.nodes) {
4040
- if (node.sidecar?.present !== true) continue;
4041
- emitBumpButton(ctx, node.path, staleStatus2(node.sidecar) !== null);
3670
+ if (node.virtual === true) continue;
3671
+ const enabled = node.sidecar?.present !== true || staleStatus2(node.sidecar) !== null;
3672
+ emitBumpButton(ctx, node.path, enabled);
4042
3673
  }
4043
3674
  },
4044
3675
  // The runtime contract uses generic <TInput, TReport>; bump narrows
@@ -4107,15 +3738,39 @@ function pickCurrentVersion(overlay) {
4107
3738
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
4108
3739
  }
4109
3740
 
3741
+ // plugins/core/actions/node-set-stability/text.ts
3742
+ var NODE_SET_STABILITY_TEXTS = {
3743
+ /** Label of the inspector action button that sets the lifecycle stage. */
3744
+ setLabel: "Set stability",
3745
+ /** Prompt label for the enum-pick stability input. */
3746
+ promptLabel: "Stability",
3747
+ /** Prompt option label for the `experimental` stage. */
3748
+ optionExperimental: "Experimental",
3749
+ /** Prompt option label for the `stable` stage. */
3750
+ optionStable: "Stable",
3751
+ /** Prompt option label for the `deprecated` stage. */
3752
+ optionDeprecated: "Deprecated"
3753
+ };
3754
+
4110
3755
  // plugins/core/actions/node-set-stability/index.ts
4111
- var STABILITY_VALUES = ["experimental", "stable", "deprecated"];
4112
- var ID29 = "node-set-stability";
3756
+ var ID26 = "node-set-stability";
3757
+ var setStabilityButton = {
3758
+ slot: "inspector.action.button",
3759
+ priority: 15
3760
+ };
4113
3761
  var nodeSetStabilityAction = {
4114
- id: ID29,
3762
+ id: ID26,
4115
3763
  pluginId: CORE_PLUGIN_ID,
4116
3764
  kind: "action",
4117
3765
  description: "Sets the lifecycle stage of the current node (writes `stability` to the sidecar).",
4118
3766
  mode: "deterministic",
3767
+ ui: { setStabilityButton },
3768
+ project(ctx) {
3769
+ for (const node of ctx.nodes) {
3770
+ if (node.virtual === true) continue;
3771
+ emitSetStabilityButton(ctx, node);
3772
+ }
3773
+ },
4119
3774
  // The runtime contract uses generic <TInput, TReport>; this narrows
4120
3775
  // both. The cast is the standard pattern for built-ins that want
4121
3776
  // typed local I/O while staying compatible with the open generic.
@@ -4124,6 +3779,27 @@ var nodeSetStabilityAction = {
4124
3779
  return invokeSetStability(input, ctx);
4125
3780
  }
4126
3781
  };
3782
+ function emitSetStabilityButton(ctx, node) {
3783
+ ctx.emitContribution(node.path, setStabilityButton, {
3784
+ actionId: "core/node-set-stability",
3785
+ label: NODE_SET_STABILITY_TEXTS.setLabel,
3786
+ icon: "pi-flag",
3787
+ enabled: true,
3788
+ prompt: {
3789
+ inputType: "enum-pick",
3790
+ paramKey: "stability",
3791
+ label: NODE_SET_STABILITY_TEXTS.promptLabel,
3792
+ options: [
3793
+ { value: "experimental", label: NODE_SET_STABILITY_TEXTS.optionExperimental },
3794
+ { value: "stable", label: NODE_SET_STABILITY_TEXTS.optionStable },
3795
+ { value: "deprecated", label: NODE_SET_STABILITY_TEXTS.optionDeprecated }
3796
+ ],
3797
+ // Pre-load the node's current stage so the picker opens on the active
3798
+ // value; `stable` when nothing is set yet.
3799
+ defaultValue: readEffectiveStability(node) ?? "stable"
3800
+ }
3801
+ });
3802
+ }
4127
3803
  function invokeSetStability(input, ctx) {
4128
3804
  const stability = input.stability;
4129
3805
  if (!STABILITY_VALUES.includes(stability)) {
@@ -4159,13 +3835,13 @@ var TAGS_TEXTS = {
4159
3835
  };
4160
3836
 
4161
3837
  // plugins/core/actions/node-set-tags/index.ts
4162
- var ID30 = "node-set-tags";
3838
+ var ID27 = "node-set-tags";
4163
3839
  var setTagsButton = {
4164
3840
  slot: "inspector.action.button",
4165
3841
  priority: 15
4166
3842
  };
4167
3843
  var nodeSetTagsAction = {
4168
- id: ID30,
3844
+ id: ID27,
4169
3845
  pluginId: CORE_PLUGIN_ID,
4170
3846
  kind: "action",
4171
3847
  description: "Sets the taxonomy tags of the current node (writes `tags` to the sidecar; whole-array replace).",
@@ -4173,7 +3849,7 @@ var nodeSetTagsAction = {
4173
3849
  ui: { setTagsButton },
4174
3850
  project(ctx) {
4175
3851
  for (const node of ctx.nodes) {
4176
- if (node.sidecar?.present !== true) continue;
3852
+ if (node.virtual === true) continue;
4177
3853
  emitSetTagsButton(ctx, node);
4178
3854
  }
4179
3855
  },
@@ -4186,129 +3862,28 @@ var nodeSetTagsAction = {
4186
3862
  }
4187
3863
  };
4188
3864
  function emitSetTagsButton(ctx, node) {
4189
- ctx.emitContribution(node.path, setTagsButton, {
4190
- actionId: "core/node-set-tags",
4191
- label: TAGS_TEXTS.editLabel,
4192
- icon: "pi-tags",
4193
- enabled: true,
4194
- prompt: {
4195
- inputType: "string-list",
4196
- paramKey: "tags",
4197
- label: TAGS_TEXTS.promptLabel,
4198
- defaultValue: currentTags(node)
4199
- }
4200
- });
4201
- }
4202
- function currentTags(node) {
4203
- const ann = node.sidecar?.annotations;
4204
- if (!ann || typeof ann !== "object" || Array.isArray(ann)) return [];
4205
- const value = ann["tags"];
4206
- if (!Array.isArray(value)) return [];
4207
- return value.filter((t) => typeof t === "string");
4208
- }
4209
- function invokeSetTags(input, ctx) {
4210
- const tags = Array.isArray(input.tags) ? input.tags : [];
4211
- const timestamp = ctx.now().toISOString();
4212
- const write = {
4213
- kind: "sidecar",
4214
- path: sidecarPathFor(ctx.nodeAbsolutePath),
4215
- changes: {
4216
- identity: {
4217
- path: ctx.node.path,
4218
- bodyHash: ctx.node.bodyHash,
4219
- frontmatterHash: ctx.node.frontmatterHash
4220
- },
4221
- annotations: { tags },
4222
- audit: {
4223
- lastBumpedAt: timestamp,
4224
- lastBumpedBy: ctx.invoker
4225
- }
4226
- }
4227
- };
4228
- const report = { ok: true, tags };
4229
- return { report, writes: [write] };
4230
- }
4231
-
4232
- // plugins/core/actions/node-supersede/text.ts
4233
- var SUPERSEDE_TEXTS = {
4234
- /** Label of the inspector action button that declares supersession. */
4235
- supersedeLabel: "Supersede",
4236
- /** Tooltip shown when the supersede button is disabled (already superseded). */
4237
- supersedeDisabledReason: "Already superseded.",
4238
- /** Tooltip shown when there is no other node to supersede this one. */
4239
- supersedeNoTargetsReason: "No other node to supersede this one.",
4240
- /** Prompt label for the target node-picker (enum-pick over the live node set). */
4241
- supersedePromptLabel: "Superseded by"
4242
- };
4243
-
4244
- // plugins/core/actions/node-supersede/index.ts
4245
- var ID31 = "node-supersede";
4246
- var supersedeButton = {
4247
- slot: "inspector.action.button",
4248
- priority: 10
4249
- };
4250
- var nodeSupersedeAction = {
4251
- id: ID31,
4252
- pluginId: CORE_PLUGIN_ID,
4253
- kind: "action",
4254
- description: "Declares the current node as superseded by another (writes `supersededBy` to the sidecar).",
4255
- // Ships disabled by default (the declarer feature is still settling its
4256
- // node-picker UX). The button self-projection gates as a unit with the
4257
- // invoke executor: an enabled button pointing at a disabled action
4258
- // would error on click, so the whole action stays experimental.
4259
- stability: "experimental",
4260
- mode: "deterministic",
4261
- ui: { supersedeButton },
4262
- project(ctx) {
4263
- const candidates = ctx.nodes.filter((n) => n.virtual !== true).map((n) => n.path);
4264
- for (const node of ctx.nodes) {
4265
- if (node.virtual === true) continue;
4266
- const options = candidates.filter((p) => p !== node.path).map((p) => ({ value: p, label: p }));
4267
- emitSupersedeButton(ctx, node, options);
4268
- }
4269
- },
4270
- // The runtime contract uses generic <TInput, TReport>; supersede
4271
- // narrows both. The cast is the standard pattern for built-ins that
4272
- // want typed local I/O while staying compatible with the open generic.
4273
- invoke(rawInput, ctx) {
4274
- const input = rawInput ?? {};
4275
- return invokeSupersede(input, ctx);
4276
- }
4277
- };
4278
- function emitSupersedeButton(ctx, node, options) {
4279
- const disabledReason = resolveDisabledReason(node, options.length);
4280
- ctx.emitContribution(node.path, supersedeButton, {
4281
- actionId: "core/node-supersede",
4282
- label: SUPERSEDE_TEXTS.supersedeLabel,
4283
- icon: "pi-arrow-right-arrow-left",
4284
- enabled: disabledReason === void 0,
4285
- ...disabledReason === void 0 ? {} : { disabledReason },
3865
+ ctx.emitContribution(node.path, setTagsButton, {
3866
+ actionId: "core/node-set-tags",
3867
+ label: TAGS_TEXTS.editLabel,
3868
+ icon: "pi-tags",
3869
+ enabled: true,
4286
3870
  prompt: {
4287
- inputType: "enum-pick",
4288
- paramKey: "supersededBy",
4289
- label: SUPERSEDE_TEXTS.supersedePromptLabel,
4290
- options
3871
+ inputType: "string-list",
3872
+ paramKey: "tags",
3873
+ label: TAGS_TEXTS.promptLabel,
3874
+ defaultValue: currentTags(node)
4291
3875
  }
4292
3876
  });
4293
3877
  }
4294
- function resolveDisabledReason(node, optionCount) {
4295
- if (alreadySuperseded(node)) return SUPERSEDE_TEXTS.supersedeDisabledReason;
4296
- if (optionCount === 0) return SUPERSEDE_TEXTS.supersedeNoTargetsReason;
4297
- return void 0;
4298
- }
4299
- function alreadySuperseded(node) {
4300
- const sidecar = node.sidecar;
4301
- if (!sidecar || sidecar.present !== true) return false;
4302
- const ann = sidecar.annotations;
4303
- if (!ann || typeof ann !== "object" || Array.isArray(ann)) return false;
4304
- const value = ann["supersededBy"];
4305
- return typeof value === "string" && value.length > 0;
3878
+ function currentTags(node) {
3879
+ const ann = node.sidecar?.annotations;
3880
+ if (!ann || typeof ann !== "object" || Array.isArray(ann)) return [];
3881
+ const value = ann["tags"];
3882
+ if (!Array.isArray(value)) return [];
3883
+ return value.filter((t) => typeof t === "string");
4306
3884
  }
4307
- function invokeSupersede(input, ctx) {
4308
- const supersededBy = input.supersededBy;
4309
- if (supersededBy === ctx.node.path) {
4310
- return { report: { ok: false, reason: "self" } };
4311
- }
3885
+ function invokeSetTags(input, ctx) {
3886
+ const tags = Array.isArray(input.tags) ? input.tags : [];
4312
3887
  const timestamp = ctx.now().toISOString();
4313
3888
  const write = {
4314
3889
  kind: "sidecar",
@@ -4319,14 +3894,14 @@ function invokeSupersede(input, ctx) {
4319
3894
  bodyHash: ctx.node.bodyHash,
4320
3895
  frontmatterHash: ctx.node.frontmatterHash
4321
3896
  },
4322
- annotations: { supersededBy },
3897
+ annotations: { tags },
4323
3898
  audit: {
4324
3899
  lastBumpedAt: timestamp,
4325
3900
  lastBumpedBy: ctx.invoker
4326
3901
  }
4327
3902
  }
4328
3903
  };
4329
- const report = { ok: true, supersededBy };
3904
+ const report = { ok: true, tags };
4330
3905
  return { report, writes: [write] };
4331
3906
  }
4332
3907
 
@@ -4536,7 +4111,7 @@ function writeJsonAtomic(path, content) {
4536
4111
  }
4537
4112
 
4538
4113
  // core/paths/db-path.ts
4539
- import { join as join2, resolve as resolve6 } from "path";
4114
+ import { join as join2, resolve as resolve5 } from "path";
4540
4115
 
4541
4116
  // kernel/util/skill-map-paths.ts
4542
4117
  import { join } from "path";
@@ -4564,17 +4139,17 @@ var GITIGNORE_ENTRIES = [
4564
4139
  `${SKILL_MAP_DIR}/${DB_FILENAME}`
4565
4140
  ];
4566
4141
  function resolveDbPath(options) {
4567
- if (options.db) return resolve6(options.db);
4568
- return resolve6(options.cwd, DEFAULT_DB_REL);
4142
+ if (options.db) return resolve5(options.db);
4143
+ return resolve5(options.cwd, DEFAULT_DB_REL);
4569
4144
  }
4570
4145
  function defaultProjectDbPath(ctx) {
4571
- return resolve6(ctx.cwd, DEFAULT_DB_REL);
4146
+ return resolve5(ctx.cwd, DEFAULT_DB_REL);
4572
4147
  }
4573
4148
  function defaultProjectJobsDir(ctx) {
4574
- return resolve6(ctx.cwd, SKILL_MAP_DIR, JOBS_DIRNAME);
4149
+ return resolve5(ctx.cwd, SKILL_MAP_DIR, JOBS_DIRNAME);
4575
4150
  }
4576
4151
  function defaultProjectPluginsDir(ctx) {
4577
- return resolve6(ctx.cwd, SKILL_MAP_DIR, PLUGINS_DIRNAME);
4152
+ return resolve5(ctx.cwd, SKILL_MAP_DIR, PLUGINS_DIRNAME);
4578
4153
  }
4579
4154
  function defaultDbPath(scopeRoot) {
4580
4155
  return join2(scopeRoot, SKILL_MAP_DIR, DB_FILENAME);
@@ -4814,7 +4389,6 @@ var antigravityProvider2 = { ...antigravityProvider, pluginId: "antigravity", ve
4814
4389
  var openaiProvider2 = { ...openaiProvider, pluginId: "openai", version: VERSION };
4815
4390
  var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills", version: VERSION };
4816
4391
  var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core", version: VERSION };
4817
- var annotationsExtractor2 = { ...annotationsExtractor, pluginId: "core", version: VERSION };
4818
4392
  var backtickPathExtractor2 = { ...backtickPathExtractor, pluginId: "core", version: VERSION };
4819
4393
  var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core", version: VERSION };
4820
4394
  var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core", version: VERSION };
@@ -4823,25 +4397,22 @@ var annotationFieldUnknownAnalyzer2 = { ...annotationFieldUnknownAnalyzer, plugi
4823
4397
  var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core", version: VERSION };
4824
4398
  var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core", version: VERSION };
4825
4399
  var contributionOrphanAnalyzer2 = { ...contributionOrphanAnalyzer, pluginId: "core", version: VERSION };
4400
+ var extractorCollisionAnalyzer2 = { ...extractorCollisionAnalyzer, pluginId: "core", version: VERSION };
4826
4401
  var issueCounterAnalyzer2 = { ...issueCounterAnalyzer, pluginId: "core", version: VERSION };
4827
- var jobFileOrphanAnalyzer2 = { ...jobFileOrphanAnalyzer, pluginId: "core", version: VERSION };
4828
- var linkConflictAnalyzer2 = { ...linkConflictAnalyzer, pluginId: "core", version: VERSION };
4829
4402
  var linkCounterAnalyzer2 = { ...linkCounterAnalyzer, pluginId: "core", version: VERSION };
4403
+ var linkKindConflictAnalyzer2 = { ...linkKindConflictAnalyzer, pluginId: "core", version: VERSION };
4830
4404
  var linkSelfLoopAnalyzer2 = { ...linkSelfLoopAnalyzer, pluginId: "core", version: VERSION };
4405
+ var nameCollisionAnalyzer2 = { ...nameCollisionAnalyzer, pluginId: "core", version: VERSION };
4831
4406
  var nameReservedAnalyzer2 = { ...nameReservedAnalyzer, pluginId: "core", version: VERSION };
4832
4407
  var nodeStabilityAnalyzer2 = { ...nodeStabilityAnalyzer, pluginId: "core", version: VERSION };
4833
- var nodeSupersededAnalyzer2 = { ...nodeSupersededAnalyzer, pluginId: "core", version: VERSION };
4834
4408
  var referenceBrokenAnalyzer2 = { ...referenceBrokenAnalyzer, pluginId: "core", version: VERSION };
4835
4409
  var referenceRedundantAnalyzer2 = { ...referenceRedundantAnalyzer, pluginId: "core", version: VERSION };
4836
4410
  var schemaViolationAnalyzer2 = { ...schemaViolationAnalyzer, pluginId: "core", version: VERSION };
4837
- var signalCollisionAnalyzer2 = { ...signalCollisionAnalyzer, pluginId: "core", version: VERSION };
4838
- var triggerCollisionAnalyzer2 = { ...triggerCollisionAnalyzer, pluginId: "core", version: VERSION };
4839
4411
  var asciiFormatter2 = { ...asciiFormatter, pluginId: "core", version: VERSION };
4840
4412
  var jsonFormatter2 = { ...jsonFormatter, pluginId: "core", version: VERSION };
4841
4413
  var nodeBumpAction2 = { ...nodeBumpAction, pluginId: "core", version: VERSION };
4842
4414
  var nodeSetStabilityAction2 = { ...nodeSetStabilityAction, pluginId: "core", version: VERSION };
4843
4415
  var nodeSetTagsAction2 = { ...nodeSetTagsAction, pluginId: "core", version: VERSION };
4844
- var nodeSupersedeAction2 = { ...nodeSupersedeAction, pluginId: "core", version: VERSION };
4845
4416
  var updateCheckHook2 = { ...updateCheckHook, pluginId: "core", version: VERSION };
4846
4417
  var builtInPlugins = [
4847
4418
  {
@@ -4880,7 +4451,6 @@ var builtInPlugins = [
4880
4451
  description: "Core extensions shared across providers: parsers, extractors, analyzers, actions, hooks, formatters, and the universal `.md` fallback provider.",
4881
4452
  extensions: [
4882
4453
  coreMarkdownProvider2,
4883
- annotationsExtractor2,
4884
4454
  backtickPathExtractor2,
4885
4455
  externalUrlCounterExtractor2,
4886
4456
  markdownLinkExtractor2,
@@ -4889,25 +4459,22 @@ var builtInPlugins = [
4889
4459
  annotationOrphanAnalyzer2,
4890
4460
  annotationStaleAnalyzer2,
4891
4461
  contributionOrphanAnalyzer2,
4462
+ extractorCollisionAnalyzer2,
4892
4463
  issueCounterAnalyzer2,
4893
- jobFileOrphanAnalyzer2,
4894
- linkConflictAnalyzer2,
4895
4464
  linkCounterAnalyzer2,
4465
+ linkKindConflictAnalyzer2,
4896
4466
  linkSelfLoopAnalyzer2,
4467
+ nameCollisionAnalyzer2,
4897
4468
  nameReservedAnalyzer2,
4898
4469
  nodeStabilityAnalyzer2,
4899
- nodeSupersededAnalyzer2,
4900
4470
  referenceBrokenAnalyzer2,
4901
4471
  referenceRedundantAnalyzer2,
4902
4472
  schemaViolationAnalyzer2,
4903
- signalCollisionAnalyzer2,
4904
- triggerCollisionAnalyzer2,
4905
4473
  asciiFormatter2,
4906
4474
  jsonFormatter2,
4907
4475
  nodeBumpAction2,
4908
4476
  nodeSetStabilityAction2,
4909
4477
  nodeSetTagsAction2,
4910
- nodeSupersedeAction2,
4911
4478
  updateCheckHook2
4912
4479
  ]
4913
4480
  }
@@ -5720,7 +5287,7 @@ import { Command as Command2, Option as Option2 } from "clipanion";
5720
5287
 
5721
5288
  // core/config/helper.ts
5722
5289
  import { homedir as osHomedir } from "os";
5723
- import { isAbsolute, join as join4, resolve as resolve7, sep } from "path";
5290
+ import { isAbsolute, join as join4, resolve as resolve6, sep } from "path";
5724
5291
 
5725
5292
  // kernel/config/loader.ts
5726
5293
  import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
@@ -6138,13 +5705,13 @@ function projectPathExposure(inputs) {
6138
5705
  return { expandsSurface: true, exposedPaths: exposed };
6139
5706
  }
6140
5707
  function resolveScanPathForExposure(raw, cwd) {
6141
- if (raw.startsWith("~/")) return resolve7(join4(osHomedir(), raw.slice(2)));
6142
- if (raw === "~") return resolve7(osHomedir());
6143
- if (isAbsolute(raw)) return resolve7(raw);
6144
- return resolve7(cwd, raw);
5708
+ if (raw.startsWith("~/")) return resolve6(join4(osHomedir(), raw.slice(2)));
5709
+ if (raw === "~") return resolve6(osHomedir());
5710
+ if (isAbsolute(raw)) return resolve6(raw);
5711
+ return resolve6(cwd, raw);
6145
5712
  }
6146
5713
  function isUnderProject(absPath, cwd) {
6147
- const projectRoot = resolve7(cwd);
5714
+ const projectRoot = resolve6(cwd);
6148
5715
  return absPath === projectRoot || absPath.startsWith(`${projectRoot}${sep}`);
6149
5716
  }
6150
5717
 
@@ -6185,7 +5752,7 @@ function ensureSidecarWritesAllowed(opts) {
6185
5752
 
6186
5753
  // kernel/sidecar/store.ts
6187
5754
  import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
6188
- import { dirname as dirname5, resolve as resolve8 } from "path";
5755
+ import { dirname as dirname5, resolve as resolve7 } from "path";
6189
5756
  import { createRequire as createRequire4 } from "module";
6190
5757
  import { Ajv2020 as Ajv20204 } from "ajv/dist/2020.js";
6191
5758
  import yaml2 from "js-yaml";
@@ -6293,10 +5860,10 @@ function getSidecarValidator2() {
6293
5860
  applyAjvFormats(ajv);
6294
5861
  const specRoot = resolveSpecRoot3();
6295
5862
  const annotationsSchema = JSON.parse(
6296
- readFileSync7(resolve8(specRoot, "schemas/annotations.schema.json"), "utf8")
5863
+ readFileSync7(resolve7(specRoot, "schemas/annotations.schema.json"), "utf8")
6297
5864
  );
6298
5865
  const sidecarSchema = JSON.parse(
6299
- readFileSync7(resolve8(specRoot, "schemas/sidecar.schema.json"), "utf8")
5866
+ readFileSync7(resolve7(specRoot, "schemas/sidecar.schema.json"), "utf8")
6300
5867
  );
6301
5868
  ajv.addSchema(annotationsSchema);
6302
5869
  cachedValidator = ajv.compile(sidecarSchema);
@@ -6416,11 +5983,11 @@ async function confirm(question, streams, opts) {
6416
5983
  // cli/util/git.ts
6417
5984
  import { spawnSync } from "child_process";
6418
5985
  import { existsSync as existsSync7 } from "fs";
6419
- import { dirname as dirname6, resolve as resolve9 } from "path";
5986
+ import { dirname as dirname6, resolve as resolve8 } from "path";
6420
5987
  function isInsideGitRepo(cwd) {
6421
5988
  let current = cwd;
6422
5989
  while (true) {
6423
- if (existsSync7(resolve9(current, ".git"))) return true;
5990
+ if (existsSync7(resolve8(current, ".git"))) return true;
6424
5991
  const parent = dirname6(current);
6425
5992
  if (parent === current) return false;
6426
5993
  current = parent;
@@ -6619,7 +6186,7 @@ import { existsSync as existsSync11 } from "fs";
6619
6186
 
6620
6187
  // kernel/adapters/sqlite/storage-adapter.ts
6621
6188
  import { mkdirSync as mkdirSync4 } from "fs";
6622
- import { dirname as dirname8, resolve as resolve12 } from "path";
6189
+ import { dirname as dirname8, resolve as resolve11 } from "path";
6623
6190
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
6624
6191
  import { CamelCasePlugin, Kysely, sql as sql3 } from "kysely";
6625
6192
 
@@ -7178,7 +6745,7 @@ async function migrateNodeFavorites(trx, fromPath, toPath, report) {
7178
6745
  }
7179
6746
 
7180
6747
  // kernel/adapters/sqlite/jobs.ts
7181
- import { resolve as resolve10 } from "path";
6748
+ import { resolve as resolve9 } from "path";
7182
6749
  async function pruneTerminalJobs(db, status, cutoffMs) {
7183
6750
  const rows = await db.selectFrom("state_jobs").select(["id", "filePath"]).where("status", "=", status).where("finishedAt", "is not", null).where("finishedAt", "<", cutoffMs).execute();
7184
6751
  if (rows.length === 0) {
@@ -7193,14 +6760,14 @@ async function selectReferencedJobFilePaths(db) {
7193
6760
  const rows = await db.selectFrom("state_jobs").select(["filePath"]).where("filePath", "is not", null).execute();
7194
6761
  const out = /* @__PURE__ */ new Set();
7195
6762
  for (const row of rows) {
7196
- if (row.filePath !== null) out.add(resolve10(row.filePath));
6763
+ if (row.filePath !== null) out.add(resolve9(row.filePath));
7197
6764
  }
7198
6765
  return out;
7199
6766
  }
7200
6767
 
7201
6768
  // kernel/adapters/sqlite/migrations.ts
7202
6769
  import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync8, readdirSync } from "fs";
7203
- import { dirname as dirname7, join as join5, resolve as resolve11 } from "path";
6770
+ import { dirname as dirname7, join as join5, resolve as resolve10 } from "path";
7204
6771
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
7205
6772
  import { fileURLToPath } from "url";
7206
6773
 
@@ -7215,9 +6782,9 @@ var MIGRATIONS_TEXTS = {
7215
6782
  var FILE_RE = /^(\d{3})_([a-z0-9_]+)\.sql$/;
7216
6783
  function defaultMigrationsDir() {
7217
6784
  const here = dirname7(fileURLToPath(import.meta.url));
7218
- const flatLayout = resolve11(here, "migrations");
6785
+ const flatLayout = resolve10(here, "migrations");
7219
6786
  if (existsSync8(flatLayout)) return flatLayout;
7220
- return resolve11(here, "..", "..", "..", "migrations");
6787
+ return resolve10(here, "..", "..", "..", "migrations");
7221
6788
  }
7222
6789
  function discoverMigrations(dir = defaultMigrationsDir()) {
7223
6790
  if (!existsSync8(dir)) return [];
@@ -7287,7 +6854,7 @@ function resolveMigrationTarget(to, files) {
7287
6854
  function writePreMigrateBackup(dbPath, target) {
7288
6855
  return writeBackup(
7289
6856
  dbPath,
7290
- join5(dirname7(resolve11(dbPath)), "backups", `skill-map-pre-migrate-v${target}.db`)
6857
+ join5(dirname7(resolve10(dbPath)), "backups", `skill-map-pre-migrate-v${target}.db`)
7291
6858
  );
7292
6859
  }
7293
6860
  function applyOneMigration(db, migration) {
@@ -7322,8 +6889,8 @@ function applyOneMigration(db, migration) {
7322
6889
  }
7323
6890
  function writeBackup(dbPath, destPath) {
7324
6891
  if (dbPath === ":memory:") return null;
7325
- const absoluteSource = resolve11(dbPath);
7326
- const absoluteDest = resolve11(destPath);
6892
+ const absoluteSource = resolve10(dbPath);
6893
+ const absoluteDest = resolve10(destPath);
7327
6894
  mkdirSync3(dirname7(absoluteDest), { recursive: true });
7328
6895
  const db = new DatabaseSync2(absoluteSource);
7329
6896
  try {
@@ -7810,7 +7377,6 @@ var LINK_KIND_VALUES = Object.freeze([
7810
7377
  "invokes",
7811
7378
  "references",
7812
7379
  "mentions",
7813
- "supersedes",
7814
7380
  "points"
7815
7381
  ]);
7816
7382
  var SEVERITY_VALUES = Object.freeze([
@@ -8284,6 +7850,36 @@ function rowToContribution(row) {
8284
7850
  };
8285
7851
  }
8286
7852
 
7853
+ // kernel/adapters/sqlite/link-scores.ts
7854
+ async function replaceAllScanLinkScores(trx, linkScores) {
7855
+ await trx.deleteFrom("scan_link_scores").execute();
7856
+ if (linkScores.length === 0) return;
7857
+ const CHUNK = 90;
7858
+ for (let i = 0; i < linkScores.length; i += CHUNK) {
7859
+ const slice = linkScores.slice(i, i + CHUNK);
7860
+ const rows = slice.map(adjustmentToRow);
7861
+ await trx.insertInto("scan_link_scores").values(rows).execute();
7862
+ }
7863
+ }
7864
+ function adjustmentToRow(adj) {
7865
+ return {
7866
+ pluginId: adj.pluginId,
7867
+ extensionId: adj.extensionId,
7868
+ sourcePath: adj.link.source,
7869
+ target: adj.link.target,
7870
+ kind: adj.link.kind,
7871
+ normalizedTrigger: adj.link.trigger?.normalizedTrigger ?? null,
7872
+ opKind: adj.op.kind,
7873
+ opValue: adj.op.value,
7874
+ // FOLDED final confidence: by the time this writer runs, the
7875
+ // orchestrator has already applied every buffered op into
7876
+ // `link.confidence` (see `applyConfidenceAdjustments`). Denormalised
7877
+ // per row so the audit read needs no join.
7878
+ resultConfidence: adj.link.confidence,
7879
+ emittedAt: Date.now()
7880
+ };
7881
+ }
7882
+
8287
7883
  // kernel/adapters/sqlite/schema-fingerprint.ts
8288
7884
  import { createHash } from "crypto";
8289
7885
  import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
@@ -8370,8 +7966,18 @@ async function findNodesByTag(db, tag) {
8370
7966
  }
8371
7967
 
8372
7968
  // kernel/adapters/sqlite/scan-persistence.ts
8373
- async function persistScanResult(db, result, renameOps = [], extractorRuns = [], enrichments = [], contributions = [], registeredContributionKeys = /* @__PURE__ */ new Set(), freshlyRunTuples = /* @__PURE__ */ new Set(), contributionErrors = []) {
7969
+ async function persistScanResult(db, result, inputs = {}) {
8374
7970
  const scannedAt = validateScannedAt(result.scannedAt);
7971
+ const {
7972
+ renameOps,
7973
+ extractorRuns,
7974
+ enrichments,
7975
+ contributions,
7976
+ registeredContributionKeys,
7977
+ freshlyRunTuples,
7978
+ contributionErrors,
7979
+ linkScores
7980
+ } = resolvePersistInputs(inputs);
8375
7981
  const renames = [];
8376
7982
  await db.transaction().execute(async (trx) => {
8377
7983
  await applyRenames(trx, renameOps, renames);
@@ -8386,6 +7992,7 @@ async function persistScanResult(db, result, renameOps = [], extractorRuns = [],
8386
7992
  freshlyRunTuples
8387
7993
  );
8388
7994
  await replaceAllScanContributionErrors(trx, contributionErrors);
7995
+ await replaceAllScanLinkScores(trx, linkScores);
8389
7996
  const tagRecords = nodesToTagRecords(result.nodes);
8390
7997
  await replaceAllScanTags(trx, tagRecords, livePathsForContrib);
8391
7998
  await upsertEnrichmentLayer(trx, result, renameOps, enrichments);
@@ -8394,6 +8001,19 @@ async function persistScanResult(db, result, renameOps = [], extractorRuns = [],
8394
8001
  await sql2`PRAGMA wal_checkpoint(TRUNCATE)`.execute(db);
8395
8002
  return { renames };
8396
8003
  }
8004
+ function resolvePersistInputs(inputs) {
8005
+ return {
8006
+ renameOps: [],
8007
+ extractorRuns: [],
8008
+ enrichments: [],
8009
+ contributions: [],
8010
+ registeredContributionKeys: /* @__PURE__ */ new Set(),
8011
+ freshlyRunTuples: /* @__PURE__ */ new Set(),
8012
+ contributionErrors: [],
8013
+ linkScores: [],
8014
+ ...inputs
8015
+ };
8016
+ }
8397
8017
  function validateScannedAt(scannedAt) {
8398
8018
  if (!Number.isInteger(scannedAt) || scannedAt < 0) {
8399
8019
  throw new Error(
@@ -8762,7 +8382,7 @@ var SqliteStorageAdapter = class {
8762
8382
  if (this.#db) return;
8763
8383
  const path = this.#options.databasePath;
8764
8384
  if (path !== ":memory:") {
8765
- const absolute = resolve12(path);
8385
+ const absolute = resolve11(path);
8766
8386
  mkdirSync4(dirname8(absolute), { recursive: true });
8767
8387
  }
8768
8388
  if (this.#options.autoMigrate !== false) {
@@ -8899,17 +8519,7 @@ var SqliteStorageAdapter = class {
8899
8519
  };
8900
8520
  async function persistScansThroughNonTx(db, result, opts) {
8901
8521
  const defaults = applyPersistDefaults(opts);
8902
- await persistScanResult(
8903
- db,
8904
- result,
8905
- defaults.renameOps,
8906
- defaults.extractorRuns,
8907
- defaults.enrichments,
8908
- defaults.contributions,
8909
- defaults.registeredContributionKeys,
8910
- defaults.freshlyRunTuples,
8911
- defaults.contributionErrors
8912
- );
8522
+ await persistScanResult(db, result, defaults);
8913
8523
  }
8914
8524
  function applyPersistDefaults(opts) {
8915
8525
  return {
@@ -8920,6 +8530,7 @@ function applyPersistDefaults(opts) {
8920
8530
  registeredContributionKeys: /* @__PURE__ */ new Set(),
8921
8531
  freshlyRunTuples: /* @__PURE__ */ new Set(),
8922
8532
  contributionErrors: [],
8533
+ linkScores: [],
8923
8534
  ...opts
8924
8535
  };
8925
8536
  }
@@ -9076,17 +8687,7 @@ function buildTxSubset(trx) {
9076
8687
  scans: {
9077
8688
  persist: (result, opts) => {
9078
8689
  const d = applyPersistDefaults(opts);
9079
- return persistScanResult(
9080
- trx,
9081
- result,
9082
- d.renameOps,
9083
- d.extractorRuns,
9084
- d.enrichments,
9085
- d.contributions,
9086
- d.registeredContributionKeys,
9087
- d.freshlyRunTuples,
9088
- d.contributionErrors
9089
- ).then(() => void 0);
8690
+ return persistScanResult(trx, result, d).then(() => void 0);
9090
8691
  }
9091
8692
  },
9092
8693
  issues: {
@@ -9380,16 +8981,16 @@ async function tryWithSqlite(options, fn) {
9380
8981
  }
9381
8982
 
9382
8983
  // cli/commands/bump-plan.ts
9383
- import { resolve as resolve14 } from "path";
8984
+ import { resolve as resolve13 } from "path";
9384
8985
 
9385
8986
  // core/paths/path-guard.ts
9386
8987
  import { lstatSync } from "fs";
9387
- import { isAbsolute as isAbsolute2, resolve as resolve13, sep as sep2 } from "path";
8988
+ import { isAbsolute as isAbsolute2, resolve as resolve12, sep as sep2 } from "path";
9388
8989
  function assertContained(cwd, rel) {
9389
8990
  if (isAbsolute2(rel)) {
9390
8991
  throw new Error(`node path is absolute, refusing to read: ${rel}`);
9391
8992
  }
9392
- const abs = resolve13(cwd, rel);
8993
+ const abs = resolve12(cwd, rel);
9393
8994
  if (abs !== cwd && !abs.startsWith(cwd + sep2)) {
9394
8995
  throw new Error(`node path escapes repo root: ${rel}`);
9395
8996
  }
@@ -9424,7 +9025,7 @@ function planOne(node, options, invoker) {
9424
9025
  let absPath;
9425
9026
  try {
9426
9027
  assertContained(options.cwd, node.path);
9427
- absPath = resolve14(options.cwd, node.path);
9028
+ absPath = resolve13(options.cwd, node.path);
9428
9029
  } catch (err) {
9429
9030
  return {
9430
9031
  nodePath: node.path,
@@ -10047,18 +9648,18 @@ var PLUGIN_LOADER_TEXTS = {
10047
9648
  // kernel/adapters/plugin-loader/index.ts
10048
9649
  import { createRequire as createRequire5 } from "module";
10049
9650
  import { existsSync as existsSync13, readFileSync as readFileSync13, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
10050
- import { join as join8, resolve as resolve17 } from "path";
9651
+ import { join as join8, resolve as resolve16 } from "path";
10051
9652
  import { pathToFileURL } from "url";
10052
9653
  import semver from "semver";
10053
9654
 
10054
9655
  // kernel/adapters/plugin-loader/id-utils.ts
10055
- import { isAbsolute as isAbsolute3, relative, resolve as resolve15 } from "path";
9656
+ import { isAbsolute as isAbsolute3, relative, resolve as resolve14 } from "path";
10056
9657
  function fail(path, id, status, reason) {
10057
9658
  return { path, id, status, reason };
10058
9659
  }
10059
9660
  function isInsidePlugin(pluginPath, relEntry) {
10060
9661
  if (isAbsolute3(relEntry)) return false;
10061
- const abs = resolve15(pluginPath, relEntry);
9662
+ const abs = resolve14(pluginPath, relEntry);
10062
9663
  const rel = relative(pluginPath, abs);
10063
9664
  if (rel === "") return true;
10064
9665
  if (rel.startsWith("..")) return false;
@@ -10399,7 +10000,7 @@ function isDirectorySafe(path, statSync12) {
10399
10000
 
10400
10001
  // kernel/adapters/plugin-loader/storage-schemas.ts
10401
10002
  import { readFileSync as readFileSync12 } from "fs";
10402
- import { resolve as resolve16 } from "path";
10003
+ import { resolve as resolve15 } from "path";
10403
10004
  import { Ajv2020 as Ajv20206 } from "ajv/dist/2020.js";
10404
10005
 
10405
10006
  // kernel/adapters/plugin-store.ts
@@ -10463,7 +10064,7 @@ function compilePluginSchema(pluginPath, relPath) {
10463
10064
  errDescription: tx(PLUGIN_LOADER_TEXTS.loadErrorSchemaPathEscapesPlugin, { relPath, pluginPath })
10464
10065
  };
10465
10066
  }
10466
- const abs = resolve16(pluginPath, relPath);
10067
+ const abs = resolve15(pluginPath, relPath);
10467
10068
  let raw;
10468
10069
  try {
10469
10070
  raw = JSON.parse(readFileSync12(abs, "utf8"));
@@ -10505,7 +10106,7 @@ var PluginLoader = class {
10505
10106
  if (!entry.isDirectory()) continue;
10506
10107
  const candidate = join8(root, entry.name);
10507
10108
  if (existsSync13(join8(candidate, "plugin.json"))) {
10508
- out.push(resolve17(candidate));
10109
+ out.push(resolve16(candidate));
10509
10110
  }
10510
10111
  }
10511
10112
  }
@@ -10664,7 +10265,7 @@ var PluginLoader = class {
10664
10265
  manifest
10665
10266
  } };
10666
10267
  }
10667
- const abs = resolve17(pluginPath, relEntry);
10268
+ const abs = resolve16(pluginPath, relEntry);
10668
10269
  if (!existsSync13(abs)) {
10669
10270
  return { ok: false, failure: {
10670
10271
  ...fail(
@@ -10868,7 +10469,7 @@ function discoverExtensionEntries(pluginPath) {
10868
10469
  return out;
10869
10470
  }
10870
10471
  function collectKindEntries(pluginPath, kindDir, out) {
10871
- const kindAbs = resolve17(pluginPath, kindDir);
10472
+ const kindAbs = resolve16(pluginPath, kindDir);
10872
10473
  if (!existsSync13(kindAbs)) return;
10873
10474
  let entries;
10874
10475
  try {
@@ -10879,7 +10480,7 @@ function collectKindEntries(pluginPath, kindDir, out) {
10879
10480
  entries.sort();
10880
10481
  for (const entry of entries) {
10881
10482
  if (entry.startsWith(".")) continue;
10882
- const entryAbs = resolve17(kindAbs, entry);
10483
+ const entryAbs = resolve16(kindAbs, entry);
10883
10484
  if (!isDirectorySafe2(entryAbs)) continue;
10884
10485
  const candidate = findIndexCandidate(entryAbs);
10885
10486
  if (candidate !== null) {
@@ -10896,14 +10497,14 @@ function isDirectorySafe2(path) {
10896
10497
  }
10897
10498
  function findIndexCandidate(entryAbs) {
10898
10499
  for (const candidate of INDEX_CANDIDATES) {
10899
- if (existsSync13(resolve17(entryAbs, candidate))) return candidate;
10500
+ if (existsSync13(resolve16(entryAbs, candidate))) return candidate;
10900
10501
  }
10901
10502
  return null;
10902
10503
  }
10903
10504
  function installedSpecVersion() {
10904
10505
  const require2 = createRequire5(import.meta.url);
10905
10506
  const indexPath = require2.resolve("@skill-map/spec/index.json");
10906
- const pkgPath = resolve17(indexPath, "..", "package.json");
10507
+ const pkgPath = resolve16(indexPath, "..", "package.json");
10907
10508
  const pkg = JSON.parse(readFileSync13(pkgPath, "utf8"));
10908
10509
  return pkg.version;
10909
10510
  }
@@ -10916,17 +10517,6 @@ var LOCKED_PLUGIN_IDS = /* @__PURE__ */ new Set([
10916
10517
  // silently invisible, a foot-gun the host product does not want to
10917
10518
  // expose. Lock it in the enabled state.
10918
10519
  "core/markdown",
10919
- // `core/annotations` turns the `supersedes` / `supersededBy` /
10920
- // `requires` / `related` / `conflictsWith` entries of the sidecar
10921
- // `annotations:` block into the arrows the graph draws between nodes.
10922
- // It does NOT own the rest of the block (`version`, `stability`,
10923
- // `tags`, `description`, those live on the node bundle directly and
10924
- // keep rendering with the plugin off). Disabling it produces a
10925
- // confusing "edges disappear but the sidecar metadata stays" split
10926
- // that no operator actually wants; the lock makes the asymmetry
10927
- // unreachable from CLI / BFF / UI. Re-evaluate if a third-party ever
10928
- // ships a competing supersession extractor.
10929
- "core/annotations",
10930
10520
  // `core/schema-violation` validates every scanned Node against
10931
10521
  // `node.schema.json` and every Link against `link.schema.json` (the
10932
10522
  // authoritative @skill-map/spec). Disabling it makes the system
@@ -11003,7 +10593,7 @@ import { join as join9, relative as relative2, sep as sep3 } from "path";
11003
10593
 
11004
10594
  // kernel/scan/ignore.ts
11005
10595
  import { existsSync as existsSync14, readFileSync as readFileSync14 } from "fs";
11006
- import { dirname as dirname10, resolve as resolve18 } from "path";
10596
+ import { dirname as dirname10, resolve as resolve17 } from "path";
11007
10597
  import { fileURLToPath as fileURLToPath2 } from "url";
11008
10598
  import ignoreFactory from "ignore";
11009
10599
  function buildIgnoreFilter(opts = {}) {
@@ -11032,7 +10622,7 @@ function loadBundledIgnoreText() {
11032
10622
  return loadDefaultsText();
11033
10623
  }
11034
10624
  function readIgnoreFileText(scopeRoot) {
11035
- const path = resolve18(scopeRoot, ".skillmapignore");
10625
+ const path = resolve17(scopeRoot, ".skillmapignore");
11036
10626
  if (!existsSync14(path)) return void 0;
11037
10627
  try {
11038
10628
  return readFileSync14(path, "utf8");
@@ -11061,11 +10651,11 @@ function loadDefaultsText() {
11061
10651
  function readDefaultsFromDisk() {
11062
10652
  const here = dirname10(fileURLToPath2(import.meta.url));
11063
10653
  const candidates = [
11064
- resolve18(here, "../../config/defaults/skillmapignore"),
10654
+ resolve17(here, "../../config/defaults/skillmapignore"),
11065
10655
  // src/kernel/scan/ → src/config/defaults/
11066
- resolve18(here, "../config/defaults/skillmapignore"),
10656
+ resolve17(here, "../config/defaults/skillmapignore"),
11067
10657
  // dist/cli.js → dist/config/defaults/ (siblings)
11068
- resolve18(here, "config/defaults/skillmapignore")
10658
+ resolve17(here, "config/defaults/skillmapignore")
11069
10659
  ];
11070
10660
  for (const candidate of candidates) {
11071
10661
  if (existsSync14(candidate)) {
@@ -11362,7 +10952,7 @@ function isExtensionInstance(v) {
11362
10952
  }
11363
10953
 
11364
10954
  // core/runtime/plugin-runtime/warnings.ts
11365
- import { resolve as resolve19 } from "path";
10955
+ import { resolve as resolve18 } from "path";
11366
10956
 
11367
10957
  // kernel/util/text.ts
11368
10958
  function truncateHead(s, max) {
@@ -11412,7 +11002,7 @@ function resolveRuntimeContext(opts) {
11412
11002
  return opts.runtimeContext ?? defaultRuntimeContext();
11413
11003
  }
11414
11004
  function resolveSearchPaths(opts, ctx) {
11415
- if (opts.pluginDir) return [resolve19(opts.pluginDir)];
11005
+ if (opts.pluginDir) return [resolve18(opts.pluginDir)];
11416
11006
  return [defaultProjectPluginsDir(ctx)];
11417
11007
  }
11418
11008
 
@@ -12679,7 +12269,7 @@ var CONFIG_COMMANDS = [
12679
12269
 
12680
12270
  // cli/commands/conformance.ts
12681
12271
  import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
12682
- import { dirname as dirname12, resolve as resolve22 } from "path";
12272
+ import { dirname as dirname12, resolve as resolve21 } from "path";
12683
12273
  import { fileURLToPath as fileURLToPath4 } from "url";
12684
12274
  import { Command as Command5, Option as Option5 } from "clipanion";
12685
12275
 
@@ -12687,7 +12277,7 @@ import { Command as Command5, Option as Option5 } from "clipanion";
12687
12277
  import { spawnSync as spawnSync2 } from "child_process";
12688
12278
  import { cpSync, existsSync as existsSync17, mkdtempSync, readdirSync as readdirSync5, readFileSync as readFileSync15, rmSync, statSync as statSync3 } from "fs";
12689
12279
  import { tmpdir } from "os";
12690
- import { isAbsolute as isAbsolute5, join as join11, relative as relative3, resolve as resolve20 } from "path";
12280
+ import { isAbsolute as isAbsolute5, join as join11, relative as relative3, resolve as resolve19 } from "path";
12691
12281
 
12692
12282
  // conformance/i18n/runner.texts.ts
12693
12283
  var CONFORMANCE_RUNNER_TEXTS = {
@@ -12854,7 +12444,7 @@ function assertContained2(root, rel, label) {
12854
12444
  tx(CONFORMANCE_RUNNER_TEXTS.pathMustBeRelative, { label, path: rel, anchor: root })
12855
12445
  );
12856
12446
  }
12857
- const abs = resolve20(root, rel);
12447
+ const abs = resolve19(root, rel);
12858
12448
  const r = relative3(root, abs);
12859
12449
  if (r.startsWith("..") || isAbsolute5(r)) {
12860
12450
  throw new Error(
@@ -12881,7 +12471,7 @@ function evaluateAssertion(a, ctx) {
12881
12471
  } catch (err) {
12882
12472
  return { ok: false, type: a.type, reason: formatErrorMessage(err) };
12883
12473
  }
12884
- const abs = resolve20(ctx.scope, a.path);
12474
+ const abs = resolve19(ctx.scope, a.path);
12885
12475
  return existsSync17(abs) ? { ok: true, type: a.type } : {
12886
12476
  ok: false,
12887
12477
  type: a.type,
@@ -12896,7 +12486,7 @@ function evaluateAssertion(a, ctx) {
12896
12486
  return { ok: false, type: a.type, reason: formatErrorMessage(err) };
12897
12487
  }
12898
12488
  const fixturePath = join11(ctx.fixturesRoot, a.fixture);
12899
- const targetPath = resolve20(ctx.scope, a.path);
12489
+ const targetPath = resolve19(ctx.scope, a.path);
12900
12490
  if (!existsSync17(targetPath)) {
12901
12491
  return {
12902
12492
  ok: false,
@@ -13079,7 +12669,7 @@ var CONFORMANCE_TEXTS = {
13079
12669
 
13080
12670
  // cli/util/conformance-scopes.ts
13081
12671
  import { existsSync as existsSync18, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
13082
- import { dirname as dirname11, resolve as resolve21 } from "path";
12672
+ import { dirname as dirname11, resolve as resolve20 } from "path";
13083
12673
  import { createRequire as createRequire6 } from "module";
13084
12674
  import { fileURLToPath as fileURLToPath3 } from "url";
13085
12675
  function resolveSpecRoot4() {
@@ -13097,7 +12687,7 @@ function resolveCliWorkspaceRoot() {
13097
12687
  const here = dirname11(fileURLToPath3(import.meta.url));
13098
12688
  let cursor = here;
13099
12689
  for (let depth = 0; depth < 6; depth += 1) {
13100
- const candidate = resolve21(cursor, "plugins");
12690
+ const candidate = resolve20(cursor, "plugins");
13101
12691
  if (existsSync18(candidate) && statSync4(candidate).isDirectory()) {
13102
12692
  return cursor;
13103
12693
  }
@@ -13117,12 +12707,12 @@ function collectProviderScopes(specRoot) {
13117
12707
  } catch {
13118
12708
  return out;
13119
12709
  }
13120
- const pluginsRoot = resolve21(workspaceRoot, "plugins");
12710
+ const pluginsRoot = resolve20(workspaceRoot, "plugins");
13121
12711
  if (!existsSync18(pluginsRoot)) return out;
13122
12712
  for (const pluginEntry of readdirSync6(pluginsRoot)) {
13123
- const pluginDir = resolve21(pluginsRoot, pluginEntry);
12713
+ const pluginDir = resolve20(pluginsRoot, pluginEntry);
13124
12714
  if (!isDir(pluginDir)) continue;
13125
- const providersRoot = resolve21(pluginDir, "providers");
12715
+ const providersRoot = resolve20(pluginDir, "providers");
13126
12716
  if (!isDir(providersRoot)) continue;
13127
12717
  collectPluginProviderScopes(providersRoot, specRoot, out);
13128
12718
  }
@@ -13137,12 +12727,12 @@ function isDir(path) {
13137
12727
  }
13138
12728
  function collectPluginProviderScopes(providersRoot, specRoot, out) {
13139
12729
  for (const entry of readdirSync6(providersRoot)) {
13140
- const providerDir = resolve21(providersRoot, entry);
12730
+ const providerDir = resolve20(providersRoot, entry);
13141
12731
  if (!isDir(providerDir)) continue;
13142
- const conformanceDir = resolve21(providerDir, "conformance");
12732
+ const conformanceDir = resolve20(providerDir, "conformance");
13143
12733
  if (!existsSync18(conformanceDir)) continue;
13144
- const casesDir = resolve21(conformanceDir, "cases");
13145
- const fixturesDir = resolve21(conformanceDir, "fixtures");
12734
+ const casesDir = resolve20(conformanceDir, "cases");
12735
+ const fixturesDir = resolve20(conformanceDir, "fixtures");
13146
12736
  if (!existsSync18(casesDir) || !existsSync18(fixturesDir)) continue;
13147
12737
  out.push({
13148
12738
  id: `provider:${entry}`,
@@ -13159,8 +12749,8 @@ function specScope(specRoot) {
13159
12749
  id: "spec",
13160
12750
  kind: "spec",
13161
12751
  label: "spec",
13162
- casesDir: resolve21(specRoot, "conformance", "cases"),
13163
- fixturesDir: resolve21(specRoot, "conformance", "fixtures"),
12752
+ casesDir: resolve20(specRoot, "conformance", "cases"),
12753
+ fixturesDir: resolve20(specRoot, "conformance", "fixtures"),
13164
12754
  specRoot
13165
12755
  };
13166
12756
  }
@@ -13182,7 +12772,7 @@ function selectConformanceScopes(scope) {
13182
12772
  }
13183
12773
  function listCaseFiles(scope) {
13184
12774
  if (!existsSync18(scope.casesDir)) return [];
13185
- return readdirSync6(scope.casesDir).filter((entry) => entry.endsWith(".json")).sort().map((entry) => resolve21(scope.casesDir, entry));
12775
+ return readdirSync6(scope.casesDir).filter((entry) => entry.endsWith(".json")).sort().map((entry) => resolve20(scope.casesDir, entry));
13186
12776
  }
13187
12777
 
13188
12778
  // cli/commands/conformance.ts
@@ -13199,13 +12789,13 @@ function resolveBinary() {
13199
12789
  const here = dirname12(fileURLToPath4(import.meta.url));
13200
12790
  let cursor = here;
13201
12791
  for (let depth = 0; depth < 6; depth += 1) {
13202
- const candidate = resolve22(cursor, "bin", "sm.js");
12792
+ const candidate = resolve21(cursor, "bin", "sm.js");
13203
12793
  if (existsSync19(candidate)) return candidate;
13204
12794
  const parent = dirname12(cursor);
13205
12795
  if (parent === cursor) break;
13206
12796
  cursor = parent;
13207
12797
  }
13208
- return resolve22(here, "..", "..", "bin", "sm.js");
12798
+ return resolve21(here, "..", "..", "bin", "sm.js");
13209
12799
  }
13210
12800
  var ConformanceRunCommand = class extends SmCommand {
13211
12801
  static paths = [["conformance", "run"]];
@@ -13458,7 +13048,7 @@ function writeStreamSnippet(stream, header, text) {
13458
13048
  var CONFORMANCE_COMMANDS = [ConformanceRunCommand];
13459
13049
 
13460
13050
  // cli/commands/db/backup.ts
13461
- import { dirname as dirname13, join as join12, resolve as resolve23 } from "path";
13051
+ import { dirname as dirname13, join as join12, resolve as resolve22 } from "path";
13462
13052
  import { Command as Command6, Option as Option6 } from "clipanion";
13463
13053
 
13464
13054
  // cli/i18n/db.texts.ts
@@ -13571,7 +13161,7 @@ var DbBackupCommand = class extends SmCommand {
13571
13161
  const exit = requireDbOrExit(path, this.context.stderr);
13572
13162
  if (exit !== null) return exit;
13573
13163
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
13574
- const outPath = this.out ? resolve23(this.out) : join12(dirname13(path), "backups", `${ts}.db`);
13164
+ const outPath = this.out ? resolve22(this.out) : join12(dirname13(path), "backups", `${ts}.db`);
13575
13165
  await withSqlite({ databasePath: path, autoMigrate: false }, async (storage) => {
13576
13166
  storage.migrations.writeBackup(outPath);
13577
13167
  });
@@ -13588,7 +13178,7 @@ var DbBackupCommand = class extends SmCommand {
13588
13178
 
13589
13179
  // cli/commands/db/restore.ts
13590
13180
  import { chmod, copyFile, mkdir, rm } from "fs/promises";
13591
- import { dirname as dirname14, resolve as resolve24 } from "path";
13181
+ import { dirname as dirname14, resolve as resolve23 } from "path";
13592
13182
  import { Command as Command7, Option as Option7 } from "clipanion";
13593
13183
 
13594
13184
  // cli/util/fs.ts
@@ -13638,7 +13228,7 @@ var DbRestoreCommand = class extends SmCommand {
13638
13228
  });
13639
13229
  async run() {
13640
13230
  const target = resolveDbPath({ db: this.db, ...defaultRuntimeContext() });
13641
- const sourcePath = resolve24(this.source);
13231
+ const sourcePath = resolve23(this.source);
13642
13232
  const stderrAnsi = this.ansiFor("stderr");
13643
13233
  const sourceStat = await statOrNull(sourcePath);
13644
13234
  if (!sourceStat) {
@@ -13879,7 +13469,7 @@ var DbShellCommand = class extends SmCommand {
13879
13469
 
13880
13470
  // cli/commands/db/browser.ts
13881
13471
  import { spawn, spawnSync as spawnSync4 } from "child_process";
13882
- import { resolve as resolve25 } from "path";
13472
+ import { resolve as resolve24 } from "path";
13883
13473
  import { Command as Command10, Option as Option9 } from "clipanion";
13884
13474
  var DbBrowserCommand = class extends SmCommand {
13885
13475
  static paths = [["db", "browser"]];
@@ -13912,7 +13502,7 @@ var DbBrowserCommand = class extends SmCommand {
13912
13502
  });
13913
13503
  positional = Option9.String({ required: false });
13914
13504
  async run() {
13915
- const path = this.positional ? resolve25(this.positional) : resolveDbPath({ db: this.db, ...defaultRuntimeContext() });
13505
+ const path = this.positional ? resolve24(this.positional) : resolveDbPath({ db: this.db, ...defaultRuntimeContext() });
13916
13506
  if (!assertDbExists(path, this.context.stderr)) {
13917
13507
  this.printer.error(DB_TEXTS.browserRunScanFirstHint);
13918
13508
  return ExitCode.NotFound;
@@ -14830,7 +14420,7 @@ var GraphCommand = class extends SmCommand {
14830
14420
  // cli/commands/help.ts
14831
14421
  import { readFileSync as readFileSync17 } from "fs";
14832
14422
  import { createRequire as createRequire7 } from "module";
14833
- import { resolve as resolve26 } from "path";
14423
+ import { resolve as resolve25 } from "path";
14834
14424
  import { Command as Command15, Option as Option14 } from "clipanion";
14835
14425
 
14836
14426
  // cli/i18n/help.texts.ts
@@ -15144,7 +14734,7 @@ function resolveSpecVersion() {
15144
14734
  try {
15145
14735
  const req = createRequire7(import.meta.url);
15146
14736
  const indexPath = req.resolve("@skill-map/spec/index.json");
15147
- const pkgPath = resolve26(indexPath, "..", "package.json");
14737
+ const pkgPath = resolve25(indexPath, "..", "package.json");
15148
14738
  const pkg = JSON.parse(readFileSync17(pkgPath, "utf8"));
15149
14739
  return pkg.version;
15150
14740
  } catch {
@@ -15452,7 +15042,7 @@ function registeredVerbPaths(cli2) {
15452
15042
 
15453
15043
  // cli/commands/hooks.ts
15454
15044
  import { chmod as chmod2, mkdir as mkdir3, readFile as readFile2, stat as stat2, writeFile } from "fs/promises";
15455
- import { dirname as dirname16, resolve as resolve27 } from "path";
15045
+ import { dirname as dirname16, resolve as resolve26 } from "path";
15456
15046
  import { Command as Command16, Option as Option15 } from "clipanion";
15457
15047
 
15458
15048
  // cli/i18n/hooks.texts.ts
@@ -15555,8 +15145,8 @@ var HooksInstallCommand = class extends SmCommand {
15555
15145
  );
15556
15146
  return ExitCode.NotFound;
15557
15147
  }
15558
- const hooksDir = resolve27(repoRoot, ".git", "hooks");
15559
- const hookPath = resolve27(hooksDir, "pre-commit");
15148
+ const hooksDir = resolve26(repoRoot, ".git", "hooks");
15149
+ const hookPath = resolve26(hooksDir, "pre-commit");
15560
15150
  const existing = await pathExists(hookPath) ? await readFile2(hookPath, "utf8") : null;
15561
15151
  const planned2 = computePlannedHookContent(existing);
15562
15152
  if (planned2.kind === "already-installed") {
@@ -15614,7 +15204,7 @@ var HooksInstallCommand = class extends SmCommand {
15614
15204
  async function findGitRepoRoot(cwd) {
15615
15205
  let current = cwd;
15616
15206
  while (true) {
15617
- if (await pathExists(resolve27(current, ".git"))) return current;
15207
+ if (await pathExists(resolve26(current, ".git"))) return current;
15618
15208
  const parent = dirname16(current);
15619
15209
  if (parent === current) return null;
15620
15210
  current = parent;
@@ -15636,7 +15226,7 @@ var HOOKS_COMMANDS = [HooksInstallCommand];
15636
15226
 
15637
15227
  // cli/commands/init.ts
15638
15228
  import { mkdir as mkdir4, readFile as readFile3, unlink, writeFile as writeFile2 } from "fs/promises";
15639
- import { join as join17 } from "path";
15229
+ import { join as join16 } from "path";
15640
15230
  import { Command as Command17, Option as Option16 } from "clipanion";
15641
15231
 
15642
15232
  // kernel/orchestrator/index.ts
@@ -15891,7 +15481,7 @@ function buildVirtualNode(extractor, emitted, emitter) {
15891
15481
  if (emitted.frontmatter) node.frontmatter = emitted.frontmatter;
15892
15482
  return node;
15893
15483
  }
15894
- var KNOWN_LINK_KINDS = ["invokes", "references", "mentions", "supersedes", "points"];
15484
+ var KNOWN_LINK_KINDS = ["invokes", "references", "mentions", "points"];
15895
15485
  function validateLink(extractor, link, emitter) {
15896
15486
  const knownKinds = KNOWN_LINK_KINDS;
15897
15487
  if (!knownKinds.includes(link.kind)) {
@@ -16129,8 +15719,49 @@ function runActionProjections(actions, nodes, links, emitter) {
16129
15719
  return { contributions, contributionErrors };
16130
15720
  }
16131
15721
 
15722
+ // kernel/orchestrator/confidence-score.ts
15723
+ function foldConfidence(base, ops) {
15724
+ let running = base;
15725
+ for (const op of ops) {
15726
+ if (op.kind === "set") running = op.value;
15727
+ }
15728
+ for (const op of ops) {
15729
+ if (op.kind === "delta") running += op.value;
15730
+ }
15731
+ for (const op of ops) {
15732
+ if (op.kind === "floor") running = Math.max(running, op.value);
15733
+ }
15734
+ for (const op of ops) {
15735
+ if (op.kind === "ceil") running = Math.min(running, op.value);
15736
+ }
15737
+ return clamp01(running);
15738
+ }
15739
+ function clamp01(n) {
15740
+ if (n < 0) return 0;
15741
+ if (n > 1) return 1;
15742
+ return n;
15743
+ }
15744
+ function applyConfidenceAdjustments(adjustments) {
15745
+ if (adjustments.length === 0) return;
15746
+ const byLink = /* @__PURE__ */ new Map();
15747
+ for (const adj of adjustments) {
15748
+ const bucket = byLink.get(adj.link);
15749
+ if (bucket) bucket.push(adj);
15750
+ else byLink.set(adj.link, [adj]);
15751
+ }
15752
+ for (const [link, adjs] of byLink) {
15753
+ const ops = [...adjs].sort(compareAdjustments).map((a) => a.op);
15754
+ link.confidence = foldConfidence(link.confidence, ops);
15755
+ }
15756
+ }
15757
+ function compareAdjustments(a, b) {
15758
+ if (a.pluginId !== b.pluginId) return a.pluginId < b.pluginId ? -1 : 1;
15759
+ if (a.extensionId !== b.extensionId) return a.extensionId < b.extensionId ? -1 : 1;
15760
+ return 0;
15761
+ }
15762
+
16132
15763
  // kernel/orchestrator/analyzers.ts
16133
- async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher, reservedNodePaths, brokenLinks, signals, seedIssues = []) {
15764
+ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher, reservedNodePaths, brokenLinks, nameCollisions, signals, seedIssues = []) {
16134
15765
  const issues = [...seedIssues];
16135
15766
  const contributions = [];
16136
15767
  const contributionErrors = [];
@@ -16141,7 +15772,16 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
16141
15772
  expectedMdPath: o.expectedMdPath
16142
15773
  }));
16143
15774
  const scheduled = orderAnalyzersByPhase(analyzers);
15775
+ const scoreAdjustments = [];
15776
+ const scorableLinks = new Set(internalLinks);
15777
+ let scoresFolded = false;
15778
+ const foldScores = () => {
15779
+ if (scoresFolded) return;
15780
+ scoresFolded = true;
15781
+ applyConfidenceAdjustments(scoreAdjustments);
15782
+ };
16144
15783
  for (const analyzer of scheduled) {
15784
+ if (analyzer.phase !== "score") foldScores();
16145
15785
  const qualifiedId2 = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
16146
15786
  const declaredContributions = readDeclaredContributionRefs(analyzer);
16147
15787
  const emitContribution = (nodePath, ref, payload) => {
@@ -16204,6 +15844,16 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
16204
15844
  emittedAt: Date.now()
16205
15845
  });
16206
15846
  };
15847
+ const adjustConfidence = analyzer.phase === "score" ? (link, op) => {
15848
+ if (scorableLinks.has(link)) {
15849
+ scoreAdjustments.push({
15850
+ link,
15851
+ pluginId: analyzer.pluginId,
15852
+ extensionId: analyzer.id,
15853
+ op
15854
+ });
15855
+ }
15856
+ } : void 0;
16207
15857
  const emitted = await analyzer.evaluate({
16208
15858
  nodes,
16209
15859
  links: internalLinks,
@@ -16215,7 +15865,6 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
16215
15865
  sidecarRoots,
16216
15866
  annotationContributions,
16217
15867
  viewContributions,
16218
- orphanJobFiles,
16219
15868
  // `issues` is the live accumulator, mutated by `issues.push(...)`
16220
15869
  // below as each analyzer's emission lands. Late-phase analyzers
16221
15870
  // (`core/issue-counter`) read it to compute cross-analyzer
@@ -16225,7 +15874,9 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
16225
15874
  ...cwd ? { cwd } : {},
16226
15875
  ...reservedNodePaths ? { reservedNodePaths } : {},
16227
15876
  ...brokenLinks ? { brokenLinks } : {},
15877
+ ...nameCollisions && nameCollisions.size > 0 ? { nameCollisions } : {},
16228
15878
  ...signals && signals.length > 0 ? { signals } : {},
15879
+ ...adjustConfidence ? { adjustConfidence } : {},
16229
15880
  emitContribution
16230
15881
  });
16231
15882
  for (const issue of emitted) {
@@ -16236,13 +15887,16 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
16236
15887
  emitter.emit(evt);
16237
15888
  await hookDispatcher.dispatch("analyzer.completed", evt);
16238
15889
  }
16239
- return { issues, contributions, contributionErrors };
15890
+ foldScores();
15891
+ return { issues, contributions, contributionErrors, linkScores: scoreAdjustments };
16240
15892
  }
16241
15893
  function orderAnalyzersByPhase(analyzers) {
16242
15894
  return analyzers.slice().sort((a, b) => phaseRank(a) - phaseRank(b));
16243
15895
  }
16244
15896
  function phaseRank(a) {
16245
- return a.phase === "aggregate" ? 1 : 0;
15897
+ if (a.phase === "score") return 0;
15898
+ if (a.phase === "aggregate") return 2;
15899
+ return 1;
16246
15900
  }
16247
15901
  function validateIssue(analyzer, issue, emitter) {
16248
15902
  const severity = issue.severity;
@@ -16268,42 +15922,29 @@ function validateIssue(analyzer, issue, emitter) {
16268
15922
  // kernel/orchestrator/cache.ts
16269
15923
  function indexPriorSnapshot(prior) {
16270
15924
  const priorNodesByPath = /* @__PURE__ */ new Map();
16271
- const priorNodePaths = /* @__PURE__ */ new Set();
16272
15925
  const priorLinksByOriginating = /* @__PURE__ */ new Map();
16273
15926
  const priorFrontmatterIssuesByNode = /* @__PURE__ */ new Map();
16274
15927
  if (!prior) {
16275
- return { priorNodesByPath, priorNodePaths, priorLinksByOriginating, priorFrontmatterIssuesByNode };
15928
+ return { priorNodesByPath, priorLinksByOriginating, priorFrontmatterIssuesByNode };
16276
15929
  }
16277
- indexPriorNodes(prior.nodes, priorNodesByPath, priorNodePaths);
16278
- indexPriorLinks(prior.links, priorNodePaths, priorLinksByOriginating);
15930
+ indexPriorNodes(prior.nodes, priorNodesByPath);
15931
+ indexPriorLinks(prior.links, priorLinksByOriginating);
16279
15932
  indexPriorFrontmatterIssues(prior.issues, priorFrontmatterIssuesByNode);
16280
- return { priorNodesByPath, priorNodePaths, priorLinksByOriginating, priorFrontmatterIssuesByNode };
15933
+ return { priorNodesByPath, priorLinksByOriginating, priorFrontmatterIssuesByNode };
16281
15934
  }
16282
- function indexPriorNodes(nodes, byPath3, paths) {
15935
+ function indexPriorNodes(nodes, byPath3) {
16283
15936
  for (const node of nodes) {
16284
15937
  byPath3.set(node.path, node);
16285
- paths.add(node.path);
16286
15938
  }
16287
15939
  }
16288
- function indexPriorLinks(links, priorNodePaths, byOriginating) {
15940
+ function indexPriorLinks(links, byOriginating) {
16289
15941
  for (const link of links) {
16290
- const key = originatingNodeOf(link, priorNodePaths);
15942
+ const key = link.source;
16291
15943
  const list = byOriginating.get(key);
16292
15944
  if (list) list.push(link);
16293
15945
  else byOriginating.set(key, [link]);
16294
15946
  }
16295
15947
  }
16296
- var FRONTMATTER_ISSUE_ANALYZERS = /* @__PURE__ */ new Set([
16297
- "frontmatter-invalid",
16298
- "frontmatter-malformed",
16299
- // Audit L1: parser parse-error is emitted by
16300
- // `buildFreshNodeAndValidateFrontmatter` from `raw.parseIssues`. The
16301
- // raw.parseIssues only flows through the non-cache path; a cached
16302
- // node skips the rebuild, so the prior issue MUST survive the
16303
- // incremental scan or the warning silently disappears on a clean
16304
- // re-scan of an unchanged file.
16305
- "frontmatter-parse-error"
16306
- ]);
16307
15948
  function indexPriorFrontmatterIssues(issues, byNode) {
16308
15949
  for (const issue of issues) {
16309
15950
  if (!FRONTMATTER_ISSUE_ANALYZERS.has(issue.analyzerId)) continue;
@@ -16314,12 +15955,6 @@ function indexPriorFrontmatterIssues(issues, byNode) {
16314
15955
  else byNode.set(path, [issue]);
16315
15956
  }
16316
15957
  }
16317
- function originatingNodeOf(link, priorNodePaths) {
16318
- if (link.kind === "supersedes" && !priorNodePaths.has(link.source)) {
16319
- return link.target;
16320
- }
16321
- return link.source;
16322
- }
16323
15958
  function computeCacheDecision(opts) {
16324
15959
  const applicableExtractors = opts.extractors.filter((ex) => {
16325
15960
  if (!matchesKindPrecondition(ex, opts.kind)) return false;
@@ -16452,6 +16087,157 @@ function classifyLinkSource(source, shortIdToQualified, cachedQualifiedIds, appl
16452
16087
  return "obsolete";
16453
16088
  }
16454
16089
 
16090
+ // kernel/orchestrator/node-identifiers.ts
16091
+ import { posix as pathPosix4 } from "path";
16092
+ function deriveNodeIdentifiers(node, kindDescriptor) {
16093
+ const sources = kindDescriptor?.identifiers;
16094
+ if (!sources || sources.length === 0) return [];
16095
+ const out = [];
16096
+ for (const source of sources) {
16097
+ const raw = readIdentifier(source, node);
16098
+ if (!raw) continue;
16099
+ const normalised = normalizeTrigger(raw);
16100
+ if (normalised) out.push(normalised);
16101
+ }
16102
+ return out;
16103
+ }
16104
+ function readIdentifier(source, node) {
16105
+ if (source === "frontmatter.name") return readFrontmatterName(node);
16106
+ if (source === "filename-basename") return readFilenameBasename(node);
16107
+ return readDirname(node);
16108
+ }
16109
+ function readFrontmatterName(node) {
16110
+ const raw = node.frontmatter?.["name"];
16111
+ if (typeof raw !== "string") return null;
16112
+ return raw.length > 0 ? raw : null;
16113
+ }
16114
+ function readFilenameBasename(node) {
16115
+ const base = pathPosix4.basename(node.path);
16116
+ if (!base) return null;
16117
+ const ext = pathPosix4.extname(base);
16118
+ const stem = ext ? base.slice(0, -ext.length) : base;
16119
+ return stem.length > 0 ? stem : null;
16120
+ }
16121
+ function readDirname(node) {
16122
+ const dir = pathPosix4.dirname(node.path);
16123
+ if (!dir || dir === "." || dir === "/") return null;
16124
+ const base = pathPosix4.basename(dir);
16125
+ return base.length > 0 ? base : null;
16126
+ }
16127
+ function collectNameCollisions(nodes, kindRegistry) {
16128
+ const byName = indexNameClaims(nodes, kindRegistry);
16129
+ const collisions = /* @__PURE__ */ new Map();
16130
+ for (const [name, claims] of byName) {
16131
+ const distinct = dedupeClaimsByPath(claims);
16132
+ if (distinct.length >= 2) collisions.set(name, distinct);
16133
+ }
16134
+ return collisions;
16135
+ }
16136
+ function indexNameClaims(nodes, kindRegistry) {
16137
+ const byName = /* @__PURE__ */ new Map();
16138
+ for (const node of nodes) {
16139
+ const name = resolvableName(node, kindRegistry);
16140
+ if (name === null) continue;
16141
+ const bucket = byName.get(name) ?? [];
16142
+ bucket.push({ path: node.path, kind: node.kind });
16143
+ byName.set(name, bucket);
16144
+ }
16145
+ return byName;
16146
+ }
16147
+ function resolvableName(node, kindRegistry) {
16148
+ const descriptor = kindRegistry.get(`${node.provider}/${node.kind}`);
16149
+ if (!descriptor?.identifiers?.includes("frontmatter.name")) return null;
16150
+ const raw = node.frontmatter?.["name"];
16151
+ if (typeof raw !== "string" || raw.length === 0) return null;
16152
+ const normalised = normalizeTrigger(raw);
16153
+ return normalised.length > 0 ? normalised : null;
16154
+ }
16155
+ function dedupeClaimsByPath(claims) {
16156
+ return [...new Map(claims.map((c) => [c.path, c])).values()].sort(
16157
+ (a, b) => a.path.localeCompare(b.path)
16158
+ );
16159
+ }
16160
+
16161
+ // kernel/orchestrator/lift-resolved-link-confidence.ts
16162
+ function liftResolvedLinkConfidence(links, nodes, ctx) {
16163
+ if (links.length === 0) return;
16164
+ const indexes = buildIndexes(nodes, ctx);
16165
+ for (const link of links) {
16166
+ link.confidence = 1;
16167
+ applyResolution(link, indexes, ctx);
16168
+ }
16169
+ }
16170
+ function collectBrokenLinks(links, nodes, ctx) {
16171
+ const broken = /* @__PURE__ */ new Set();
16172
+ if (links.length === 0) return broken;
16173
+ const indexes = buildIndexes(nodes, ctx);
16174
+ for (const link of links) {
16175
+ if (isGenuinelyBroken(link, indexes)) broken.add(link);
16176
+ }
16177
+ return broken;
16178
+ }
16179
+ function applyResolution(link, indexes, ctx) {
16180
+ const resolution = resolve27(link, indexes, ctx);
16181
+ if (resolution === "none") return;
16182
+ link.resolvedTarget = resolution;
16183
+ }
16184
+ function buildIndexes(nodes, ctx) {
16185
+ const byPath3 = /* @__PURE__ */ new Set();
16186
+ const byName = /* @__PURE__ */ new Map();
16187
+ const nodeByPath = /* @__PURE__ */ new Map();
16188
+ for (const node of nodes) {
16189
+ byPath3.add(node.path);
16190
+ nodeByPath.set(node.path, node);
16191
+ indexNode(node, ctx, byName);
16192
+ }
16193
+ return { byPath: byPath3, byName, nodeByPath };
16194
+ }
16195
+ function resolve27(link, indexes, ctx) {
16196
+ if (indexes.byPath.has(link.target)) return link.target;
16197
+ return resolveByName(link, indexes, ctx);
16198
+ }
16199
+ function isGenuinelyBroken(link, indexes) {
16200
+ if (indexes.byPath.has(link.target)) return false;
16201
+ const stripped = stripTriggerSigil(link.trigger?.normalizedTrigger);
16202
+ if (stripped !== null && indexes.byName.has(stripped)) return false;
16203
+ return true;
16204
+ }
16205
+ function resolveByName(link, indexes, ctx) {
16206
+ const stripped = stripTriggerSigil(link.trigger?.normalizedTrigger);
16207
+ if (stripped === null) return "none";
16208
+ const candidates = indexes.byName.get(stripped);
16209
+ if (!candidates?.length) return "none";
16210
+ const allowedKinds = lookupAllowedKinds(link, indexes, ctx);
16211
+ if (!allowedKinds?.length) return "none";
16212
+ const winner = candidates.find((c) => allowedKinds.includes(c.kind));
16213
+ return winner ? winner.path : "none";
16214
+ }
16215
+ function lookupAllowedKinds(link, _indexes, ctx) {
16216
+ if (ctx.activeProvider === null) return void 0;
16217
+ return ctx.providerResolution.get(ctx.activeProvider)?.[link.kind];
16218
+ }
16219
+ function stripTriggerSigil(normalized) {
16220
+ if (!normalized) return null;
16221
+ const trimmed = normalized.replace(/^[/@]/, "").trim();
16222
+ return trimmed.length === 0 ? null : trimmed;
16223
+ }
16224
+ function indexNode(node, ctx, byName) {
16225
+ const kindDescriptor = ctx.kindRegistry.get(kindKey(node));
16226
+ const normalised = deriveNodeIdentifiers(node, kindDescriptor);
16227
+ for (const name of normalised) {
16228
+ const entry = { kind: node.kind, path: node.path };
16229
+ const bucket = byName.get(name);
16230
+ if (bucket) {
16231
+ bucket.push(entry);
16232
+ } else {
16233
+ byName.set(name, [entry]);
16234
+ }
16235
+ }
16236
+ }
16237
+ function kindKey(node) {
16238
+ return `${node.provider}/${node.kind}`;
16239
+ }
16240
+
16455
16241
  // kernel/orchestrator/post-walk-transforms.ts
16456
16242
  var POST_WALK_TRANSFORMS = [
16457
16243
  {
@@ -17448,6 +17234,7 @@ async function runScanInternal(_kernel, options) {
17448
17234
  const postWalkCtx = buildPostWalkTransformCtx(exts.providers, walked.nodes, activeProviderId);
17449
17235
  walked.internalLinks = applyPostWalkTransforms(walked.internalLinks, walked.nodes, postWalkCtx);
17450
17236
  const brokenLinks = collectBrokenLinks(walked.internalLinks, walked.nodes, postWalkCtx);
17237
+ const nameCollisions = collectNameCollisions(walked.nodes, postWalkCtx.kindRegistry);
17451
17238
  recomputeLinkCounts(walked.nodes, walked.internalLinks);
17452
17239
  recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);
17453
17240
  await dispatchExtractorCompleted(exts.extractors, emitter, hookDispatcher);
@@ -17462,7 +17249,6 @@ async function runScanInternal(_kernel, options) {
17462
17249
  walked.sidecarRoots,
17463
17250
  options.annotationContributions ?? [],
17464
17251
  options.viewContributions ?? [],
17465
- options.orphanJobFiles ?? [],
17466
17252
  options.referenceablePaths,
17467
17253
  options.cwd,
17468
17254
  registeredActionIds,
@@ -17470,6 +17256,7 @@ async function runScanInternal(_kernel, options) {
17470
17256
  hookDispatcher,
17471
17257
  postWalkCtx.reservedNodePaths,
17472
17258
  brokenLinks,
17259
+ nameCollisions,
17473
17260
  walked.signals,
17474
17261
  // Seed the accumulator with orchestrator-emitted frontmatter
17475
17262
  // issues so the aggregate phase (`core/issue-counter`) counts
@@ -17492,7 +17279,7 @@ async function runScanInternal(_kernel, options) {
17492
17279
  const scanCompletedEvent = makeEvent("scan.completed", { stats });
17493
17280
  emitter.emit(scanCompletedEvent);
17494
17281
  await hookDispatcher.dispatch("scan.completed", scanCompletedEvent);
17495
- return buildScanReturn(walked, issues, renameOps, stats, options, setup);
17282
+ return buildScanReturn(walked, issues, renameOps, stats, options, setup, analyzerResult.linkScores);
17496
17283
  }
17497
17284
  function buildPostWalkTransformCtx(providers, nodes, activeProvider) {
17498
17285
  const { kindRegistry, providerResolution, reservedNamesByProviderKind } = buildProviderIndexes(providers);
@@ -17620,7 +17407,7 @@ function buildScanStats(walked, issues, start) {
17620
17407
  durationMs: Date.now() - start
17621
17408
  };
17622
17409
  }
17623
- function buildScanReturn(walked, issues, renameOps, stats, options, setup) {
17410
+ function buildScanReturn(walked, issues, renameOps, stats, options, setup, linkScores) {
17624
17411
  return {
17625
17412
  result: {
17626
17413
  schemaVersion: 1,
@@ -17642,6 +17429,7 @@ function buildScanReturn(walked, issues, renameOps, stats, options, setup) {
17642
17429
  enrichments: walked.enrichments,
17643
17430
  contributions: walked.contributions,
17644
17431
  contributionErrors: walked.contributionErrors,
17432
+ linkScores,
17645
17433
  freshlyRunTuples: walked.freshlyRunTuples
17646
17434
  };
17647
17435
  }
@@ -17887,33 +17675,6 @@ function createKernel() {
17887
17675
  };
17888
17676
  }
17889
17677
 
17890
- // kernel/jobs/orphan-files.ts
17891
- import { readdirSync as readdirSync8, statSync as statSync7 } from "fs";
17892
- import { join as join14, resolve as resolve30 } from "path";
17893
- function findOrphanJobFiles(jobsDir, referencedPaths) {
17894
- let entries;
17895
- try {
17896
- const stat3 = statSync7(jobsDir);
17897
- if (!stat3.isDirectory()) {
17898
- return { orphanFilePaths: [], referencedCount: referencedPaths.size };
17899
- }
17900
- entries = readdirSync8(jobsDir, { withFileTypes: true });
17901
- } catch {
17902
- return { orphanFilePaths: [], referencedCount: referencedPaths.size };
17903
- }
17904
- const orphans = [];
17905
- for (const entry of entries) {
17906
- if (entry.isSymbolicLink()) continue;
17907
- if (!entry.isFile()) continue;
17908
- const name = entry.name;
17909
- if (!name.endsWith(".md")) continue;
17910
- const abs = resolve30(join14(jobsDir, name));
17911
- if (!referencedPaths.has(abs)) orphans.push(abs);
17912
- }
17913
- orphans.sort();
17914
- return { orphanFilePaths: orphans, referencedCount: referencedPaths.size };
17915
- }
17916
-
17917
17678
  // core/config/plugin-settings.ts
17918
17679
  var defaultWarn = (message) => log.warn(message);
17919
17680
  function resolveExtensionSettings(manifest, config, onWarn = defaultWarn) {
@@ -18218,9 +17979,9 @@ function resolveScanRoots(inputs) {
18218
17979
  }
18219
17980
 
18220
17981
  // core/runtime/reference-paths-walker.ts
18221
- import { readdirSync as readdirSync9, statSync as statSync8 } from "fs";
17982
+ import { readdirSync as readdirSync8, statSync as statSync7 } from "fs";
18222
17983
  import { homedir as osHomedir2 } from "os";
18223
- import { isAbsolute as isAbsolute8, join as join15, resolve as resolve31 } from "path";
17984
+ import { isAbsolute as isAbsolute8, join as join14, resolve as resolve30 } from "path";
18224
17985
  var REFERENCE_WALK_MAX_FILES = 5e4;
18225
17986
  var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
18226
17987
  "node_modules",
@@ -18228,10 +17989,10 @@ var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
18228
17989
  SKILL_MAP_DIR
18229
17990
  ]);
18230
17991
  function resolveScanPath(raw, cwd) {
18231
- if (raw.startsWith("~/")) return resolve31(join15(osHomedir2(), raw.slice(2)));
18232
- if (raw === "~") return resolve31(osHomedir2());
18233
- if (isAbsolute8(raw)) return resolve31(raw);
18234
- return resolve31(cwd, raw);
17992
+ if (raw.startsWith("~/")) return resolve30(join14(osHomedir2(), raw.slice(2)));
17993
+ if (raw === "~") return resolve30(osHomedir2());
17994
+ if (isAbsolute8(raw)) return resolve30(raw);
17995
+ return resolve30(cwd, raw);
18235
17996
  }
18236
17997
  function walkReferencePaths(rawRoots, cwd) {
18237
17998
  const paths = /* @__PURE__ */ new Set();
@@ -18253,14 +18014,14 @@ function walkInto(dir, out) {
18253
18014
  if (out.size >= REFERENCE_WALK_MAX_FILES) return true;
18254
18015
  let entries;
18255
18016
  try {
18256
- entries = readdirSync9(dir, { withFileTypes: true });
18017
+ entries = readdirSync8(dir, { withFileTypes: true });
18257
18018
  } catch {
18258
18019
  return false;
18259
18020
  }
18260
18021
  for (const entry of entries) {
18261
18022
  if (out.size >= REFERENCE_WALK_MAX_FILES) return true;
18262
18023
  if (entry.isSymbolicLink()) continue;
18263
- const full = join15(dir, entry.name);
18024
+ const full = join14(dir, entry.name);
18264
18025
  if (entry.isDirectory()) {
18265
18026
  if (SKIPPED_DIR_NAMES.has(entry.name)) continue;
18266
18027
  if (walkInto(full, out)) return true;
@@ -18272,7 +18033,7 @@ function walkInto(dir, out) {
18272
18033
  }
18273
18034
  function safeStat(path) {
18274
18035
  try {
18275
- return statSync8(path);
18036
+ return statSync7(path);
18276
18037
  } catch {
18277
18038
  return null;
18278
18039
  }
@@ -18280,7 +18041,7 @@ function safeStat(path) {
18280
18041
 
18281
18042
  // core/runtime/active-provider-bootstrap.ts
18282
18043
  import { createInterface as createInterface3 } from "readline";
18283
- import { isAbsolute as isAbsolute9, join as join16 } from "path";
18044
+ import { isAbsolute as isAbsolute9, join as join15 } from "path";
18284
18045
  async function bootstrapActiveProvider(opts) {
18285
18046
  const fromCwd = resolveActiveProvider(opts.cwd, opts.providers);
18286
18047
  if (fromCwd.source === "config") {
@@ -18339,7 +18100,7 @@ function aggregateDetected(cwd, effectiveRoots, cwdDetected, providers) {
18339
18100
  out.push(id);
18340
18101
  }
18341
18102
  for (const root of effectiveRoots) {
18342
- const absRoot = isAbsolute9(root) ? root : join16(cwd, root);
18103
+ const absRoot = isAbsolute9(root) ? root : join15(cwd, root);
18343
18104
  const r = resolveActiveProvider(absRoot, providers);
18344
18105
  for (const id of r.detected) {
18345
18106
  if (seen.has(id)) continue;
@@ -18578,7 +18339,6 @@ async function runScanForCommand(opts) {
18578
18339
  emitReferenceWalkAdvisory(walk3, opts);
18579
18340
  }
18580
18341
  const loadPrior = makePriorLoader(opts.noBuiltIns, strict);
18581
- const jobsDir = defaultProjectJobsDir(ctx);
18582
18342
  const lens = await resolveActiveLens(
18583
18343
  opts,
18584
18344
  ctx,
@@ -18603,7 +18363,7 @@ async function runScanForCommand(opts) {
18603
18363
  cfg.tokenizer
18604
18364
  );
18605
18365
  const willPersist = !opts.noBuiltIns && !opts.dryRun;
18606
- const scanned = await (willPersist ? runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanWith, extensions) : runEphemeralPath(opts, dbPath, strict, loadPrior, runScanWith));
18366
+ const scanned = await (willPersist ? runPersistPath(opts, dbPath, strict, loadPrior, runScanWith, extensions) : runEphemeralPath(opts, dbPath, strict, loadPrior, runScanWith));
18607
18367
  return scanned.kind === "ok" ? { ...scanned, lensAutoDetected: lens.autoDetected } : scanned;
18608
18368
  }
18609
18369
  function detectionProviders(extensions) {
@@ -18716,7 +18476,7 @@ function makePriorLoader(noBuiltIns, strict) {
18716
18476
  };
18717
18477
  }
18718
18478
  function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, extensions, referenceablePaths, scanCwd, activeProvider, recommendedNodeLimit, maxFileSizeBytes, tokenizer) {
18719
- return async (prior, priorExtractorRuns, orphanJobFiles) => {
18479
+ return async (prior, priorExtractorRuns) => {
18720
18480
  if (opts.changed && prior === null) {
18721
18481
  opts.stderr.write(SCAN_RUNNER_TEXTS.changedNoPriorWarning);
18722
18482
  }
@@ -18733,14 +18493,13 @@ function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, exte
18733
18493
  recommendedNodeLimit,
18734
18494
  maxFileSizeBytes,
18735
18495
  tokenizer,
18736
- ...priorExtractorRuns ? { priorExtractorRuns } : {},
18737
- ...orphanJobFiles ? { orphanJobFiles } : {}
18496
+ ...priorExtractorRuns ? { priorExtractorRuns } : {}
18738
18497
  });
18739
18498
  return runScanWithRenames(kernel, runOptions);
18740
18499
  };
18741
18500
  }
18742
18501
  function buildRunScanOptions(args2) {
18743
- const { opts, prior, priorExtractorRuns, orphanJobFiles, referenceablePaths } = args2;
18502
+ const { opts, prior, priorExtractorRuns, referenceablePaths } = args2;
18744
18503
  const runOptions = {
18745
18504
  roots: args2.effectiveRoots.slice(),
18746
18505
  tokenize: !opts.noTokens,
@@ -18748,11 +18507,6 @@ function buildRunScanOptions(args2) {
18748
18507
  ignoreFilter: args2.ignoreFilter,
18749
18508
  strict: args2.strict,
18750
18509
  emitter: buildRunScanEmitter(opts),
18751
- // Orphan job-file detection, empty list means "no orphans
18752
- // visible from this caller" (legacy behaviour). The orchestrator
18753
- // defaults to `[]` when the field is absent; we always pass the
18754
- // array (possibly empty) to keep the wiring uniform.
18755
- orphanJobFiles: orphanJobFiles ?? [],
18756
18510
  activeProvider: args2.activeProvider,
18757
18511
  recommendedNodeLimit: args2.recommendedNodeLimit,
18758
18512
  overrideMaxNodes: opts.maxNodes ?? null,
@@ -18795,7 +18549,7 @@ async function rebuildOnDrift(opts, dbPath) {
18795
18549
  })
18796
18550
  };
18797
18551
  }
18798
- async function runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanWith, extensions) {
18552
+ async function runPersistPath(opts, dbPath, strict, loadPrior, runScanWith, extensions) {
18799
18553
  const driftError = await rebuildOnDrift(opts, dbPath);
18800
18554
  if (driftError) return driftError;
18801
18555
  let outcome;
@@ -18803,11 +18557,9 @@ async function runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanW
18803
18557
  outcome = await withSqlite({ databasePath: dbPath }, async (adapter) => {
18804
18558
  const prior = await loadPrior(adapter);
18805
18559
  const priorExtractorRuns = opts.changed && prior ? await adapter.scans.loadExtractorRuns() : void 0;
18806
- const referencedJobFiles = await adapter.jobs.listReferencedFilePaths();
18807
- const orphanJobFiles = findOrphanJobFiles(jobsDir, referencedJobFiles).orphanFilePaths;
18808
18560
  let scanned;
18809
18561
  try {
18810
- scanned = await runScanWith(prior, priorExtractorRuns, orphanJobFiles);
18562
+ scanned = await runScanWith(prior, priorExtractorRuns);
18811
18563
  } catch (err) {
18812
18564
  return { kind: "scan-error", message: formatErrorMessage(err) };
18813
18565
  }
@@ -18822,6 +18574,7 @@ async function runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanW
18822
18574
  enrichments: scanned.enrichments,
18823
18575
  contributions: scanned.contributions,
18824
18576
  contributionErrors: scanned.contributionErrors,
18577
+ linkScores: scanned.linkScores,
18825
18578
  registeredContributionKeys: collectRegisteredContributionKeys(extensions),
18826
18579
  freshlyRunTuples: scanned.freshlyRunTuples
18827
18580
  });
@@ -18938,7 +18691,7 @@ var InitCommand = class extends SmCommand {
18938
18691
  async run() {
18939
18692
  const ctx = defaultRuntimeContext();
18940
18693
  const scopeRoot = ctx.cwd;
18941
- const skillMapDir = join17(scopeRoot, SKILL_MAP_DIR);
18694
+ const skillMapDir = join16(scopeRoot, SKILL_MAP_DIR);
18942
18695
  const settingsPath = defaultSettingsPath(scopeRoot);
18943
18696
  const localPath = defaultLocalSettingsPath(scopeRoot);
18944
18697
  const ignorePath = defaultIgnoreFilePath(scopeRoot);
@@ -18984,7 +18737,7 @@ var InitCommand = class extends SmCommand {
18984
18737
  const okGlyph = ansi.green("\u2713");
18985
18738
  const updated = await ensureGitignoreEntries(scopeRoot, GITIGNORE_ENTRIES);
18986
18739
  if (updated) {
18987
- const gitignorePath = join17(scopeRoot, ".gitignore");
18740
+ const gitignorePath = join16(scopeRoot, ".gitignore");
18988
18741
  printer.info(
18989
18742
  GITIGNORE_ENTRIES.length === 1 ? tx(INIT_TEXTS.gitignoreUpdatedSingular, { glyph: okGlyph, path: gitignorePath }) : tx(INIT_TEXTS.gitignoreUpdatedPlural, {
18990
18743
  glyph: okGlyph,
@@ -19045,7 +18798,7 @@ async function safeUnlink(path) {
19045
18798
  }
19046
18799
  async function writeDryRunGitignorePlan(printer, scopeRoot) {
19047
18800
  const wouldAdd = await previewGitignoreEntries(scopeRoot, GITIGNORE_ENTRIES);
19048
- const gitignorePath = join17(scopeRoot, ".gitignore");
18801
+ const gitignorePath = join16(scopeRoot, ".gitignore");
19049
18802
  if (wouldAdd.length === 0) {
19050
18803
  printer.info(tx(INIT_TEXTS.dryRunWouldLeaveGitignoreUnchanged, { path: gitignorePath }));
19051
18804
  } else if (wouldAdd.length === 1) {
@@ -19150,7 +18903,7 @@ async function runFirstScan(scopeRoot, strict, printer, stderr, stdin, ansi) {
19150
18903
  return hasErrors ? ExitCode.Issues : ExitCode.Ok;
19151
18904
  }
19152
18905
  async function previewGitignoreEntries(scopeRoot, entries) {
19153
- const path = join17(scopeRoot, ".gitignore");
18906
+ const path = join16(scopeRoot, ".gitignore");
19154
18907
  const body = await pathExists(path) ? await readFile3(path, "utf8") : "";
19155
18908
  const present = new Set(
19156
18909
  body.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"))
@@ -19158,7 +18911,7 @@ async function previewGitignoreEntries(scopeRoot, entries) {
19158
18911
  return entries.filter((entry) => !present.has(entry));
19159
18912
  }
19160
18913
  async function ensureGitignoreEntries(scopeRoot, entries) {
19161
- const path = join17(scopeRoot, ".gitignore");
18914
+ const path = join16(scopeRoot, ".gitignore");
19162
18915
  let body = "";
19163
18916
  if (await pathExists(path)) {
19164
18917
  body = await readFile3(path, "utf8");
@@ -19699,6 +19452,33 @@ import { unlink as unlink2 } from "fs/promises";
19699
19452
  import { relative as relative6 } from "path";
19700
19453
  import { Command as Command19, Option as Option18 } from "clipanion";
19701
19454
 
19455
+ // kernel/jobs/orphan-files.ts
19456
+ import { readdirSync as readdirSync9, statSync as statSync8 } from "fs";
19457
+ import { join as join17, resolve as resolve31 } from "path";
19458
+ function findOrphanJobFiles(jobsDir, referencedPaths) {
19459
+ let entries;
19460
+ try {
19461
+ const stat3 = statSync8(jobsDir);
19462
+ if (!stat3.isDirectory()) {
19463
+ return { orphanFilePaths: [], referencedCount: referencedPaths.size };
19464
+ }
19465
+ entries = readdirSync9(jobsDir, { withFileTypes: true });
19466
+ } catch {
19467
+ return { orphanFilePaths: [], referencedCount: referencedPaths.size };
19468
+ }
19469
+ const orphans = [];
19470
+ for (const entry of entries) {
19471
+ if (entry.isSymbolicLink()) continue;
19472
+ if (!entry.isFile()) continue;
19473
+ const name = entry.name;
19474
+ if (!name.endsWith(".md")) continue;
19475
+ const abs = resolve31(join17(jobsDir, name));
19476
+ if (!referencedPaths.has(abs)) orphans.push(abs);
19477
+ }
19478
+ orphans.sort();
19479
+ return { orphanFilePaths: orphans, referencedCount: referencedPaths.size };
19480
+ }
19481
+
19702
19482
  // cli/i18n/jobs.texts.ts
19703
19483
  var JOBS_TEXTS = {
19704
19484
  pruneErrorPrefix: "{{glyph}} sm job prune: {{message}}\n",
@@ -23899,6 +23679,7 @@ function createWatcherRuntime(opts) {
23899
23679
  enrichments,
23900
23680
  contributions,
23901
23681
  contributionErrors,
23682
+ linkScores,
23902
23683
  freshlyRunTuples
23903
23684
  } = ran;
23904
23685
  await withSqlite(
@@ -23909,6 +23690,7 @@ function createWatcherRuntime(opts) {
23909
23690
  enrichments,
23910
23691
  contributions,
23911
23692
  contributionErrors,
23693
+ linkScores,
23912
23694
  registeredContributionKeys: collectRegisteredContributionKeys(composed),
23913
23695
  freshlyRunTuples
23914
23696
  })
@@ -24391,8 +24173,8 @@ var ScanCommand = class extends SmCommand {
24391
24173
  details: `
24392
24174
  Walks the given roots with the built-in claude Provider, runs the
24393
24175
  frontmatter / slash / at-directive / external-url-counter
24394
- extractors per node, then the trigger-collision / broken-ref /
24395
- superseded analyzers over the full graph. Emits a ScanResult
24176
+ extractors per node, then the name-collision / broken-ref
24177
+ analyzers over the full graph. Emits a ScanResult
24396
24178
  conforming to scan-result.schema.json.
24397
24179
 
24398
24180
  The result is persisted into <cwd>/.skill-map/skill-map.db
@@ -28494,6 +28276,43 @@ var WsBroadcaster = class {
28494
28276
  }
28495
28277
  };
28496
28278
 
28279
+ // server/heartbeat.ts
28280
+ var WS_HEARTBEAT_INTERVAL_MS = 3e4;
28281
+ function startWsHeartbeat(wss, opts = {}) {
28282
+ const intervalMs = opts.intervalMs ?? WS_HEARTBEAT_INTERVAL_MS;
28283
+ const alive = /* @__PURE__ */ new WeakMap();
28284
+ const onConnection = (socket) => {
28285
+ alive.set(socket, true);
28286
+ socket.on("pong", () => {
28287
+ alive.set(socket, true);
28288
+ });
28289
+ };
28290
+ wss.on("connection", onConnection);
28291
+ const timer = setInterval(() => {
28292
+ for (const socket of wss.clients) {
28293
+ if (alive.get(socket) === false) {
28294
+ socket.terminate();
28295
+ continue;
28296
+ }
28297
+ alive.set(socket, false);
28298
+ try {
28299
+ socket.ping();
28300
+ } catch {
28301
+ }
28302
+ }
28303
+ }, intervalMs);
28304
+ timer.unref?.();
28305
+ let stopped = false;
28306
+ return {
28307
+ stop() {
28308
+ if (stopped) return;
28309
+ stopped = true;
28310
+ clearInterval(timer);
28311
+ wss.off("connection", onConnection);
28312
+ }
28313
+ };
28314
+ }
28315
+
28497
28316
  // server/kind-registry.ts
28498
28317
  function buildKindRegistry(providers) {
28499
28318
  const registry = {};
@@ -28777,6 +28596,7 @@ async function createServer(options, extra = {}) {
28777
28596
  });
28778
28597
  const wss = new WebSocketServer({ noServer: true });
28779
28598
  const server = await listenAsync(app.fetch, wss, options.host, options.port);
28599
+ const heartbeat = startWsHeartbeat(wss);
28780
28600
  const addr = server.address();
28781
28601
  const address = normalizeAddress(addr, options.host, options.port);
28782
28602
  let watcherService = null;
@@ -28810,6 +28630,7 @@ async function createServer(options, extra = {}) {
28810
28630
  const close = async () => {
28811
28631
  if (closed) return;
28812
28632
  closed = true;
28633
+ heartbeat.stop();
28813
28634
  if (watcherService) {
28814
28635
  try {
28815
28636
  await watcherService.stop();
@@ -31152,4 +30973,4 @@ function resolveBareDefault() {
31152
30973
  process.exit(ExitCode.Error);
31153
30974
  }
31154
30975
  //# sourceMappingURL=cli.js.map
31155
- //# debugId=49fdc2af-2694-5cd0-ac19-33f8d1dffa9e
30976
+ //# debugId=7e4fdbab-f037-5030-b67a-732bb7ee46fd