@skill-map/cli 0.53.6 → 0.54.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]="3bc438eb-6e57-550d-aab1-392242aca915")}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]="3118bb11-aad2-50b2-b70d-ed15c2f72a97")}catch(e){}}();
4
4
  import { existsSync as existsSync33 } from "fs";
5
5
  import { Builtins, Cli as Cli2 } from "clipanion";
6
6
 
@@ -246,7 +246,7 @@ function bucketByKind(kind, instance, bag) {
246
246
  // package.json
247
247
  var package_default = {
248
248
  name: "@skill-map/cli",
249
- version: "0.53.6",
249
+ version: "0.54.0",
250
250
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
251
251
  license: "MIT",
252
252
  type: "module",
@@ -796,6 +796,16 @@ function stripCodeBlocks(input) {
796
796
  const fenceless = stripFences(input);
797
797
  return stripInline(fenceless);
798
798
  }
799
+ function extractCodeRegions(input) {
800
+ if (!input) return input;
801
+ const stripped = stripCodeBlocks(input);
802
+ let out = "";
803
+ for (let i = 0; i < input.length; i++) {
804
+ const ch = input[i];
805
+ out += ch === stripped[i] ? ch === "\n" ? "\n" : " " : ch;
806
+ }
807
+ return out;
808
+ }
799
809
  function stripFences(input) {
800
810
  const out = [];
801
811
  const lines = input.split("\n");
@@ -1005,6 +1015,44 @@ var slashCommandExtractor = {
1005
1015
  }
1006
1016
  };
1007
1017
 
1018
+ // plugins/claude/extractors/tools-counter/index.ts
1019
+ var ID3 = "tools-counter";
1020
+ var count = {
1021
+ slot: "card.footer.left",
1022
+ icon: "pi-wrench",
1023
+ label: "tools",
1024
+ emitWhenEmpty: false,
1025
+ priority: 40
1026
+ };
1027
+ var TOOLTIP_MAX = 255;
1028
+ var toolsCounterExtractor = {
1029
+ id: ID3,
1030
+ pluginId: CLAUDE_PLUGIN_ID,
1031
+ kind: "extractor",
1032
+ description: "Counts the tools an agent declares in its frontmatter and shows the count on the agent card.",
1033
+ scope: "frontmatter",
1034
+ precondition: { kind: ["claude/agent"] },
1035
+ ui: { count },
1036
+ extract(ctx) {
1037
+ const raw = ctx.frontmatter["tools"];
1038
+ if (!Array.isArray(raw)) return;
1039
+ const names = [];
1040
+ for (const t of raw) {
1041
+ if (typeof t === "string" && t.length > 0) names.push(t);
1042
+ }
1043
+ if (names.length === 0) return;
1044
+ ctx.emitContribution(count, {
1045
+ value: names.length,
1046
+ tooltip: buildTooltip(names)
1047
+ });
1048
+ }
1049
+ };
1050
+ function buildTooltip(names) {
1051
+ const joined = names.join(" \xB7 ");
1052
+ if (joined.length <= TOOLTIP_MAX) return joined;
1053
+ return `${joined.slice(0, TOOLTIP_MAX - 1)}\u2026`;
1054
+ }
1055
+
1008
1056
  // plugins/antigravity/providers/antigravity/index.ts
1009
1057
  var antigravityProvider = {
1010
1058
  id: "antigravity",
@@ -1391,9 +1439,9 @@ var coreMarkdownProvider = {
1391
1439
  };
1392
1440
 
1393
1441
  // plugins/core/extractors/annotations/index.ts
1394
- var ID3 = "annotations";
1442
+ var ID4 = "annotations";
1395
1443
  var annotationsExtractor = {
1396
- id: ID3,
1444
+ id: ID4,
1397
1445
  pluginId: CORE_PLUGIN_ID,
1398
1446
  kind: "extractor",
1399
1447
  description: "Turns the `supersedes` and `supersededBy` entries from a node's `.sm` sidecar into arrows between nodes in the graph.",
@@ -1412,7 +1460,7 @@ var annotationsExtractor = {
1412
1460
  raw: target,
1413
1461
  candidates: [
1414
1462
  {
1415
- extractorId: ID3,
1463
+ extractorId: ID4,
1416
1464
  kind: "supersedes",
1417
1465
  target,
1418
1466
  confidence: 1,
@@ -1449,9 +1497,67 @@ function stringArray(value) {
1449
1497
  return value.filter((v) => typeof v === "string" && v.length > 0);
1450
1498
  }
1451
1499
 
1500
+ // plugins/core/extractors/backtick-path/index.ts
1501
+ import { posix as pathPosix2 } from "path";
1502
+ var ID5 = "backtick-path";
1503
+ var PATH_RE = /(?<![\w/:.-])(?:\.{1,2}\/)?[\w.-]+(?:\/[\w.-]+)+\.md\b(?![\w/])/g;
1504
+ var backtickPathExtractor = {
1505
+ id: ID5,
1506
+ pluginId: CORE_PLUGIN_ID,
1507
+ kind: "extractor",
1508
+ description: "Turns relative .md paths written inside code spans and fenced blocks into arrows between nodes in the graph.",
1509
+ scope: "body",
1510
+ extract(ctx) {
1511
+ const seen = /* @__PURE__ */ new Set();
1512
+ const body = extractCodeRegions(ctx.body);
1513
+ const lineStarts = computeLineStarts(body);
1514
+ const sourceDir = pathPosix2.dirname(ctx.node.path);
1515
+ for (const match of body.matchAll(PATH_RE)) {
1516
+ const original = match[0];
1517
+ const resolved = resolveTarget(sourceDir, original);
1518
+ if (resolved === null) continue;
1519
+ if (seen.has(resolved)) continue;
1520
+ seen.add(resolved);
1521
+ const offset = match.index ?? 0;
1522
+ const line = lineFor(lineStarts, offset);
1523
+ ctx.emitSignal({
1524
+ source: ctx.node.path,
1525
+ scope: "body",
1526
+ range: { start: offset, end: offset + original.length, line },
1527
+ raw: original,
1528
+ candidates: [
1529
+ {
1530
+ extractorId: ID5,
1531
+ kind: "points",
1532
+ target: resolved,
1533
+ // 0.85: a strong file signal with one degree of inference,
1534
+ // the author wrote a path inside a code region rather than
1535
+ // an explicit `[text](path)` link. Whether the path resolves
1536
+ // to a real node is a separate concern (`core/reference-broken`
1537
+ // flags unresolved targets), not a confidence question.
1538
+ confidence: 0.85,
1539
+ rationale: "relative .md path inside a code region",
1540
+ trigger: {
1541
+ originalTrigger: original,
1542
+ normalizedTrigger: resolved
1543
+ }
1544
+ }
1545
+ ]
1546
+ });
1547
+ }
1548
+ }
1549
+ };
1550
+ function resolveTarget(sourceDir, raw) {
1551
+ const trimmed = raw.trim();
1552
+ if (trimmed.length === 0) return null;
1553
+ if (trimmed.startsWith("/")) return null;
1554
+ const joined = sourceDir === "." ? trimmed : `${sourceDir}/${trimmed}`;
1555
+ return pathPosix2.normalize(joined);
1556
+ }
1557
+
1452
1558
  // plugins/core/extractors/external-url-counter/index.ts
1453
- var ID4 = "external-url-counter";
1454
- var count = {
1559
+ var ID6 = "external-url-counter";
1560
+ var count2 = {
1455
1561
  slot: "card.footer.left",
1456
1562
  icon: "pi-link",
1457
1563
  label: "urls",
@@ -1461,7 +1567,7 @@ var count = {
1461
1567
  var URL_RE = /https?:\/\/[^\s<>"'`)\]]+/g;
1462
1568
  var TRAILING_PUNCT = /[.,;:!?]+$/;
1463
1569
  var externalUrlCounterExtractor = {
1464
- id: ID4,
1570
+ id: ID6,
1465
1571
  pluginId: CORE_PLUGIN_ID,
1466
1572
  kind: "extractor",
1467
1573
  description: "Counts the distinct external URLs in a node's body and shows the count on the card.",
@@ -1482,7 +1588,7 @@ var externalUrlCounterExtractor = {
1482
1588
  * inherited from the footer `.sm-gnode__stat` styles cloned by
1483
1589
  * the `NodeCounter` renderer.
1484
1590
  */
1485
- ui: { count },
1591
+ ui: { count: count2 },
1486
1592
  extract(ctx) {
1487
1593
  const seen = /* @__PURE__ */ new Set();
1488
1594
  const body = stripCodeBlocks(ctx.body);
@@ -1503,7 +1609,7 @@ var externalUrlCounterExtractor = {
1503
1609
  raw: original,
1504
1610
  candidates: [
1505
1611
  {
1506
- extractorId: ID4,
1612
+ extractorId: ID6,
1507
1613
  kind: "references",
1508
1614
  target: normalized,
1509
1615
  confidence: 0.3,
@@ -1517,7 +1623,7 @@ var externalUrlCounterExtractor = {
1517
1623
  });
1518
1624
  }
1519
1625
  if (seen.size > 0) {
1520
- ctx.emitContribution(count, { value: seen.size });
1626
+ ctx.emitContribution(count2, { value: seen.size });
1521
1627
  }
1522
1628
  }
1523
1629
  };
@@ -1536,12 +1642,12 @@ function normalizeUrl(raw) {
1536
1642
  }
1537
1643
 
1538
1644
  // plugins/core/extractors/markdown-link/index.ts
1539
- import { posix as pathPosix2 } from "path";
1540
- var ID5 = "markdown-link";
1645
+ import { posix as pathPosix3 } from "path";
1646
+ var ID7 = "markdown-link";
1541
1647
  var LINK_RE = /(?<!!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
1542
1648
  var URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
1543
1649
  var markdownLinkExtractor = {
1544
- id: ID5,
1650
+ id: ID7,
1545
1651
  pluginId: CORE_PLUGIN_ID,
1546
1652
  kind: "extractor",
1547
1653
  description: "Turns markdown links (`[text](path)`) in a node's body into arrows between nodes in the graph.",
@@ -1550,10 +1656,10 @@ var markdownLinkExtractor = {
1550
1656
  const seen = /* @__PURE__ */ new Set();
1551
1657
  const body = stripCodeBlocks(ctx.body);
1552
1658
  const lineStarts = computeLineStarts(body);
1553
- const sourceDir = pathPosix2.dirname(ctx.node.path);
1659
+ const sourceDir = pathPosix3.dirname(ctx.node.path);
1554
1660
  for (const match of body.matchAll(LINK_RE)) {
1555
1661
  const original = match[2];
1556
- const resolved = resolveTarget(sourceDir, original);
1662
+ const resolved = resolveTarget2(sourceDir, original);
1557
1663
  if (resolved === null) continue;
1558
1664
  if (seen.has(resolved)) continue;
1559
1665
  seen.add(resolved);
@@ -1566,7 +1672,7 @@ var markdownLinkExtractor = {
1566
1672
  raw: match[0],
1567
1673
  candidates: [
1568
1674
  {
1569
- extractorId: ID5,
1675
+ extractorId: ID7,
1570
1676
  kind: "references",
1571
1677
  target: resolved,
1572
1678
  // 1.0: the `[text](path)` syntax is unambiguous. Markdown
@@ -1587,7 +1693,7 @@ var markdownLinkExtractor = {
1587
1693
  }
1588
1694
  }
1589
1695
  };
1590
- function resolveTarget(sourceDir, raw) {
1696
+ function resolveTarget2(sourceDir, raw) {
1591
1697
  const noFragment = raw.split("#", 1)[0];
1592
1698
  const noQuery = noFragment.split("?", 1)[0];
1593
1699
  const trimmed = noQuery.trim();
@@ -1595,17 +1701,21 @@ function resolveTarget(sourceDir, raw) {
1595
1701
  if (URL_SCHEME_RE.test(trimmed)) return null;
1596
1702
  if (trimmed.startsWith("/")) return null;
1597
1703
  const joined = sourceDir === "." ? trimmed : `${sourceDir}/${trimmed}`;
1598
- return pathPosix2.normalize(joined);
1704
+ return pathPosix3.normalize(joined);
1599
1705
  }
1600
1706
 
1601
1707
  // plugins/core/extractors/mcp-tools/index.ts
1602
- var ID6 = "mcp-tools";
1708
+ var ID8 = "mcp-tools";
1603
1709
  var MCP_PATTERN = /^mcp__([a-z0-9][a-z0-9_-]*)__[a-z0-9_-]+$/i;
1604
1710
  var mcpToolsExtractor = {
1605
- id: ID6,
1711
+ id: ID8,
1606
1712
  pluginId: CORE_PLUGIN_ID,
1607
1713
  kind: "extractor",
1608
1714
  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.",
1715
+ // Claude-convention pattern only; per-vendor flavours and the
1716
+ // config-side MCP declaration (Phase 5b) are still pending, so the
1717
+ // extractor ships flagged as experimental in list / show / Settings.
1718
+ stability: "experimental",
1609
1719
  scope: "frontmatter",
1610
1720
  extract(ctx) {
1611
1721
  const raw = ctx.frontmatter["tools"];
@@ -1629,7 +1739,7 @@ var mcpToolsExtractor = {
1629
1739
  raw: `mcp__${server}__*`,
1630
1740
  candidates: [
1631
1741
  {
1632
- extractorId: ID6,
1742
+ extractorId: ID8,
1633
1743
  kind: "references",
1634
1744
  target: mcpPath,
1635
1745
  confidence: 0.85,
@@ -1659,44 +1769,6 @@ function collectMcpServers(tools) {
1659
1769
  return out;
1660
1770
  }
1661
1771
 
1662
- // plugins/core/extractors/tools-counter/index.ts
1663
- var ID7 = "tools-counter";
1664
- var count2 = {
1665
- slot: "card.footer.left",
1666
- icon: "pi-wrench",
1667
- label: "tools",
1668
- emitWhenEmpty: false,
1669
- priority: 40
1670
- };
1671
- var TOOLTIP_MAX = 255;
1672
- var toolsCounterExtractor = {
1673
- id: ID7,
1674
- pluginId: CORE_PLUGIN_ID,
1675
- kind: "extractor",
1676
- description: "Counts the tools an agent declares in its frontmatter and shows the count on the agent card.",
1677
- scope: "frontmatter",
1678
- precondition: { kind: ["claude/agent"] },
1679
- ui: { count: count2 },
1680
- extract(ctx) {
1681
- const raw = ctx.frontmatter["tools"];
1682
- if (!Array.isArray(raw)) return;
1683
- const names = [];
1684
- for (const t of raw) {
1685
- if (typeof t === "string" && t.length > 0) names.push(t);
1686
- }
1687
- if (names.length === 0) return;
1688
- ctx.emitContribution(count2, {
1689
- value: names.length,
1690
- tooltip: buildTooltip(names)
1691
- });
1692
- }
1693
- };
1694
- function buildTooltip(names) {
1695
- const joined = names.join(" \xB7 ");
1696
- if (joined.length <= TOOLTIP_MAX) return joined;
1697
- return `${joined.slice(0, TOOLTIP_MAX - 1)}\u2026`;
1698
- }
1699
-
1700
1772
  // plugins/core/analyzers/annotation-field-unknown/index.ts
1701
1773
  import { readFileSync } from "fs";
1702
1774
  import { dirname, resolve } from "path";
@@ -1712,12 +1784,14 @@ function applyAjvFormats(ajv) {
1712
1784
 
1713
1785
  // plugins/core/analyzers/annotation-field-unknown/text.ts
1714
1786
  var ANNOTATION_FIELD_UNKNOWN_TEXTS = {
1787
+ // Compact finding grammar: the affected node is the finding's own
1788
+ // node, so its path never appears in the message.
1715
1789
  /** Key inside `annotations:` is not in the curated catalog. */
1716
- unknownAnnotationKey: "{{path}}: sidecar annotations contain unknown key '{{key}}' (not in annotations.schema.json catalog).",
1790
+ unknownAnnotationKey: "Unknown sidecar key '{{key}}'; not in the annotations catalog.",
1717
1791
  /** Top-level key is neither reserved, nor a registered plugin namespace, nor a registered root key. */
1718
- unknownRootKey: "{{path}}: sidecar declares unknown top-level key '{{key}}'; not a reserved block, not a registered plugin namespace, not a registered root contribution.",
1792
+ unknownRootKey: "Unknown sidecar top-level key '{{key}}'; not a reserved block, a plugin namespace, or a root contribution.",
1719
1793
  /** Value under a registered plugin namespace fails the contributed schema. */
1720
- pluginNamespaceInvalid: "{{path}}: sidecar block '{{pluginId}}.{{key}}' fails the schema contributed by plugin '{{pluginId}}': {{errors}}.",
1794
+ pluginNamespaceInvalid: "Sidecar block '{{pluginId}}.{{key}}' fails the schema from plugin '{{pluginId}}': {{errors}}.",
1721
1795
  // Tooltips for the per-node view-contribution badges. Singular vs
1722
1796
  // plural keeps the count grammar correct without a sub-template.
1723
1797
  alertTooltipSingle: "This node has 1 unknown field in its sidecar. Open the inspector for details.",
@@ -1725,10 +1799,10 @@ var ANNOTATION_FIELD_UNKNOWN_TEXTS = {
1725
1799
  };
1726
1800
 
1727
1801
  // plugins/core/analyzers/annotation-field-unknown/index.ts
1728
- var ID8 = "annotation-field-unknown";
1802
+ var ID9 = "annotation-field-unknown";
1729
1803
  var RESERVED_ROOT_BLOCKS = /* @__PURE__ */ new Set(["identity", "annotations", "settings", "audit"]);
1730
1804
  var annotationFieldUnknownAnalyzer = {
1731
- id: ID8,
1805
+ id: ID9,
1732
1806
  pluginId: CORE_PLUGIN_ID,
1733
1807
  kind: "analyzer",
1734
1808
  description: "Flags typos or unrecognized keys in sidecars (`.sm`).",
@@ -1767,7 +1841,7 @@ var annotationFieldUnknownAnalyzer = {
1767
1841
  for (const key of Object.keys(annotations)) {
1768
1842
  if (!knownAnnotationKeys.has(key)) {
1769
1843
  issues.push({
1770
- analyzerId: ID8,
1844
+ analyzerId: ID9,
1771
1845
  severity: "warn",
1772
1846
  nodeIds: [node.path],
1773
1847
  message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.unknownAnnotationKey, {
@@ -1794,7 +1868,7 @@ var annotationFieldUnknownAnalyzer = {
1794
1868
  if (validator(value)) continue;
1795
1869
  const errors = (validator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
1796
1870
  issues.push({
1797
- analyzerId: ID8,
1871
+ analyzerId: ID9,
1798
1872
  severity: "warn",
1799
1873
  nodeIds: [node.path],
1800
1874
  message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.pluginNamespaceInvalid, {
@@ -1810,7 +1884,7 @@ var annotationFieldUnknownAnalyzer = {
1810
1884
  continue;
1811
1885
  }
1812
1886
  issues.push({
1813
- analyzerId: ID8,
1887
+ analyzerId: ID9,
1814
1888
  severity: "warn",
1815
1889
  nodeIds: [node.path],
1816
1890
  message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.unknownRootKey, {
@@ -1871,14 +1945,19 @@ function collectPluginIds(contributions) {
1871
1945
 
1872
1946
  // plugins/core/analyzers/annotation-orphan/text.ts
1873
1947
  var ANNOTATION_ORPHAN_TEXTS = {
1874
- /** Sidecar `<path>.sm` has no matching `<path>.md`. */
1875
- message: "Orphan sidecar: {{sidecarPath}} has no matching markdown node at {{expectedMdPath}}."
1948
+ /**
1949
+ * Compact finding grammar: line 1 = the orphan sidecar file, line 2
1950
+ * = the diagnosis. The expected markdown path IS the finding's
1951
+ * `nodeIds[0]` (the issue files under the path the sidecar points
1952
+ * at), so it never appears in the message.
1953
+ */
1954
+ message: "{{sidecarPath}}:\nOrphan sidecar; no matching markdown node."
1876
1955
  };
1877
1956
 
1878
1957
  // plugins/core/analyzers/annotation-orphan/index.ts
1879
- var ID9 = "annotation-orphan";
1958
+ var ID10 = "annotation-orphan";
1880
1959
  var annotationOrphanAnalyzer = {
1881
- id: ID9,
1960
+ id: ID10,
1882
1961
  pluginId: CORE_PLUGIN_ID,
1883
1962
  kind: "analyzer",
1884
1963
  description: "Flags sidecars (`.sm`) whose `.md` file no longer exists.",
@@ -1890,7 +1969,7 @@ var annotationOrphanAnalyzer = {
1890
1969
  for (const orphan of orphans) {
1891
1970
  const expectedMdRelative = orphan.relativePath.endsWith(".sm") ? `${orphan.relativePath.slice(0, -".sm".length)}.md` : `${orphan.relativePath}.md`;
1892
1971
  issues.push({
1893
- analyzerId: ID9,
1972
+ analyzerId: ID10,
1894
1973
  severity: "warn",
1895
1974
  nodeIds: [expectedMdRelative],
1896
1975
  message: tx(ANNOTATION_ORPHAN_TEXTS.message, {
@@ -1909,12 +1988,14 @@ var annotationOrphanAnalyzer = {
1909
1988
 
1910
1989
  // plugins/core/analyzers/annotation-stale/text.ts
1911
1990
  var ANNOTATION_STALE_TEXTS = {
1991
+ // Compact finding grammar: the affected node is the finding's own
1992
+ // node, so its path never appears in the message.
1912
1993
  /** body changed since last bump */
1913
- bodyDrift: "{{path}}: sidecar `.sm` is stale (body changed since last bump).",
1994
+ bodyDrift: "Sidecar `.sm` is stale: body changed since last bump.",
1914
1995
  /** frontmatter changed since last bump */
1915
- frontmatterDrift: "{{path}}: sidecar `.sm` is stale (frontmatter changed since last bump).",
1996
+ frontmatterDrift: "Sidecar `.sm` is stale: frontmatter changed since last bump.",
1916
1997
  /** both body and frontmatter changed */
1917
- bothDrift: "{{path}}: sidecar `.sm` is stale (body and frontmatter changed since last bump).",
1998
+ bothDrift: "Sidecar `.sm` is stale: body and frontmatter changed since last bump.",
1918
1999
  // Tooltips for the `card.footer.right` clock chip emitted alongside
1919
2000
  // the issue. Lists only the drifted face(s), in-sync faces are
1920
2001
  // omitted so the operator immediately sees what's modified without
@@ -1931,7 +2012,7 @@ var ANNOTATION_STALE_TEXTS = {
1931
2012
  };
1932
2013
 
1933
2014
  // plugins/core/analyzers/annotation-stale/index.ts
1934
- var ID10 = "annotation-stale";
2015
+ var ID11 = "annotation-stale";
1935
2016
  var staleIcon = {
1936
2017
  slot: "card.footer.right",
1937
2018
  icon: "pi-clock",
@@ -1948,7 +2029,7 @@ var bumpButton = {
1948
2029
  priority: 10
1949
2030
  };
1950
2031
  var annotationStaleAnalyzer = {
1951
- id: ID10,
2032
+ id: ID11,
1952
2033
  pluginId: CORE_PLUGIN_ID,
1953
2034
  kind: "analyzer",
1954
2035
  description: "Marks sidecars (`.sm`) that are out of date with their `.md`.",
@@ -1966,7 +2047,7 @@ var annotationStaleAnalyzer = {
1966
2047
  }
1967
2048
  if (status === null) continue;
1968
2049
  issues.push({
1969
- analyzerId: ID10,
2050
+ analyzerId: ID11,
1970
2051
  severity: "info",
1971
2052
  nodeIds: [node.path],
1972
2053
  message: messageFor(status, node.path),
@@ -2020,9 +2101,9 @@ function tooltipFor(status) {
2020
2101
  }
2021
2102
 
2022
2103
  // plugins/core/analyzers/contribution-orphan/index.ts
2023
- var ID11 = "contribution-orphan";
2104
+ var ID12 = "contribution-orphan";
2024
2105
  var contributionOrphanAnalyzer = {
2025
- id: ID11,
2106
+ id: ID12,
2026
2107
  pluginId: CORE_PLUGIN_ID,
2027
2108
  kind: "analyzer",
2028
2109
  description: "Warns about plugin data referencing nodes renamed or deleted in the latest scan.",
@@ -2041,7 +2122,7 @@ var ISSUE_COUNTER_TEXTS = {
2041
2122
  };
2042
2123
 
2043
2124
  // plugins/core/analyzers/issue-counter/index.ts
2044
- var ID12 = "issue-counter";
2125
+ var ID13 = "issue-counter";
2045
2126
  var warnCount = {
2046
2127
  slot: "card.footer.right",
2047
2128
  icon: "pi-exclamation-triangle",
@@ -2077,7 +2158,7 @@ function emitTierChips(ctx, ref, severity, counts, singleTooltip, manyTooltip) {
2077
2158
  }
2078
2159
  }
2079
2160
  var issueCounterAnalyzer = {
2080
- id: ID12,
2161
+ id: ID13,
2081
2162
  pluginId: CORE_PLUGIN_ID,
2082
2163
  kind: "analyzer",
2083
2164
  description: "Emits one aggregate severity chip per node (error + warn counts) from the live issue accumulator.",
@@ -2112,15 +2193,16 @@ var issueCounterAnalyzer = {
2112
2193
  var JOB_FILE_ORPHAN_TEXTS = {
2113
2194
  /**
2114
2195
  * `<path>.md` lives under `.skill-map/jobs/` but no `state_jobs.filePath`
2115
- * row references it. Run `sm job prune --orphan-files` to remove.
2196
+ * row references it. Compact finding grammar: the file IS the
2197
+ * finding's own node, so its path never appears in the message.
2116
2198
  */
2117
- message: "Orphan job file: {{filePath}} is not referenced by any state_jobs row. Run `sm job prune --orphan-files` to remove it."
2199
+ message: "Orphan job file; not referenced by any job. Run `sm job prune --orphan-files` to remove it."
2118
2200
  };
2119
2201
 
2120
2202
  // plugins/core/analyzers/job-file-orphan/index.ts
2121
- var ID13 = "job-file-orphan";
2203
+ var ID14 = "job-file-orphan";
2122
2204
  var jobFileOrphanAnalyzer = {
2123
- id: ID13,
2205
+ id: ID14,
2124
2206
  pluginId: CORE_PLUGIN_ID,
2125
2207
  kind: "analyzer",
2126
2208
  description: "Flags leftover job result files (no live job references them). Clean up via `sm job prune --orphan-files`.",
@@ -2131,7 +2213,7 @@ var jobFileOrphanAnalyzer = {
2131
2213
  const issues = [];
2132
2214
  for (const filePath of orphans) {
2133
2215
  issues.push({
2134
- analyzerId: ID13,
2216
+ analyzerId: ID14,
2135
2217
  severity: "warn",
2136
2218
  nodeIds: [filePath],
2137
2219
  message: tx(JOB_FILE_ORPHAN_TEXTS.message, { filePath }),
@@ -2144,14 +2226,19 @@ var jobFileOrphanAnalyzer = {
2144
2226
 
2145
2227
  // plugins/core/analyzers/link-conflict/text.ts
2146
2228
  var LINK_CONFLICT_TEXTS = {
2147
- /** `Detectors disagree on link kind for <source> → <target> (<kindList>)` */
2148
- message: "Detectors disagree on link kind for {{source}} \u2192 {{target}} ({{kindList}})"
2229
+ /**
2230
+ * Compact finding grammar: line 1 = the disputed target, line 2 =
2231
+ * the short diagnosis. The source is the finding's own node, so it
2232
+ * never appears in the message.
2233
+ */
2234
+ message: "{{target}}:\nDetectors disagree on link kind ({{kindList}})."
2149
2235
  };
2150
2236
 
2151
2237
  // plugins/core/analyzers/link-conflict/index.ts
2152
- var ID14 = "link-conflict";
2238
+ var ID15 = "link-conflict";
2239
+ var NON_CONFLICTING_KINDS = /* @__PURE__ */ new Set(["points"]);
2153
2240
  var linkConflictAnalyzer = {
2154
- id: ID14,
2241
+ id: ID15,
2155
2242
  pluginId: "core",
2156
2243
  kind: "analyzer",
2157
2244
  description: "Flags conflicting arrow meanings between extractors (e.g. `references` vs `invokes`).",
@@ -2163,6 +2250,7 @@ var linkConflictAnalyzer = {
2163
2250
  evaluate(ctx) {
2164
2251
  const groups = /* @__PURE__ */ new Map();
2165
2252
  for (const link of ctx.links) {
2253
+ if (NON_CONFLICTING_KINDS.has(link.kind)) continue;
2166
2254
  const key = `${link.source}\0${link.target}`;
2167
2255
  const bucket = groups.get(key);
2168
2256
  if (bucket) bucket.push(link);
@@ -2198,7 +2286,7 @@ var linkConflictAnalyzer = {
2198
2286
  const [source, target] = key.split("\0");
2199
2287
  const kindList = variants.map((v) => v.kind).join(" / ");
2200
2288
  issues.push({
2201
- analyzerId: ID14,
2289
+ analyzerId: ID15,
2202
2290
  severity: "warn",
2203
2291
  nodeIds: [source, target],
2204
2292
  message: tx(LINK_CONFLICT_TEXTS.message, {
@@ -2265,7 +2353,7 @@ function resolveLinkTargetToPath(link, nameIndex) {
2265
2353
  }
2266
2354
 
2267
2355
  // plugins/core/analyzers/link-counter/index.ts
2268
- var ID15 = "link-counter";
2356
+ var ID16 = "link-counter";
2269
2357
  var linksIn = {
2270
2358
  slot: "card.footer.left",
2271
2359
  icon: "pi-download",
@@ -2281,7 +2369,7 @@ var linksOut = {
2281
2369
  priority: 20
2282
2370
  };
2283
2371
  var linkCounterAnalyzer = {
2284
- id: ID15,
2372
+ id: ID16,
2285
2373
  pluginId: CORE_PLUGIN_ID,
2286
2374
  kind: "analyzer",
2287
2375
  description: "Counts incoming and outgoing links per node.",
@@ -2328,6 +2416,27 @@ function formatBreakdown(byKind, direction) {
2328
2416
  return [direction, ...lines].join("\n");
2329
2417
  }
2330
2418
 
2419
+ // kernel/util/link-lines.ts
2420
+ function linkLines(link) {
2421
+ const lines = /* @__PURE__ */ new Set();
2422
+ for (const occ of link.occurrences ?? []) {
2423
+ const line = occ.location?.line;
2424
+ if (typeof line === "number") lines.add(line);
2425
+ }
2426
+ if (lines.size === 0) {
2427
+ const line = link.location?.line;
2428
+ if (typeof line === "number") lines.add(line);
2429
+ }
2430
+ return [...lines].sort((a, b) => a - b);
2431
+ }
2432
+ function linkWhere(link, texts) {
2433
+ const lines = linkLines(link);
2434
+ if (lines.length === 0) return "";
2435
+ return tx(lines.length === 1 ? texts.single : texts.plural, {
2436
+ lines: lines.join(", ")
2437
+ });
2438
+ }
2439
+
2331
2440
  // plugins/core/analyzers/link-self-loop/text.ts
2332
2441
  var LINK_SELF_LOOP_TEXTS = {
2333
2442
  /**
@@ -2338,13 +2447,17 @@ var LINK_SELF_LOOP_TEXTS = {
2338
2447
  * the operator's intent; UI consumers MAY hide it by default and
2339
2448
  * surface a count.
2340
2449
  */
2341
- message: "{{source}} references itself via `{{trigger}}` ({{kind}}). Self-loops typically come from the file's own heading or label and are noise rather than intent. Either remove the in-body token or treat this finding as expected and acknowledged."
2450
+ message: "`{{trigger}}`:\nSelf-reference ({{kind}}{{where}}); typically the file's own heading or label. Remove the token or ignore deliberately.",
2451
+ /** Location suffix inside the kind parens, one detection site. */
2452
+ whereSingle: ", line {{lines}}",
2453
+ /** Location suffix inside the kind parens, several detection sites. */
2454
+ wherePlural: ", lines {{lines}}"
2342
2455
  };
2343
2456
 
2344
2457
  // plugins/core/analyzers/link-self-loop/index.ts
2345
- var ID16 = "link-self-loop";
2458
+ var ID17 = "link-self-loop";
2346
2459
  var linkSelfLoopAnalyzer = {
2347
- id: ID16,
2460
+ id: ID17,
2348
2461
  pluginId: CORE_PLUGIN_ID,
2349
2462
  kind: "analyzer",
2350
2463
  description: "Flags links whose source is also their own resolved target (e.g. a body heading like `# /deploy` inside the file that defines `/deploy`).",
@@ -2355,13 +2468,16 @@ var linkSelfLoopAnalyzer = {
2355
2468
  for (const link of ctx.links) {
2356
2469
  if (!isSelfLoop(link)) continue;
2357
2470
  issues.push({
2358
- analyzerId: ID16,
2471
+ analyzerId: ID17,
2359
2472
  severity: "warn",
2360
2473
  nodeIds: [link.source],
2361
2474
  message: tx(LINK_SELF_LOOP_TEXTS.message, {
2362
- source: link.source,
2363
2475
  trigger: link.trigger?.originalTrigger ?? link.target,
2364
- kind: link.kind
2476
+ kind: link.kind,
2477
+ where: linkWhere(link, {
2478
+ single: LINK_SELF_LOOP_TEXTS.whereSingle,
2479
+ plural: LINK_SELF_LOOP_TEXTS.wherePlural
2480
+ })
2365
2481
  }),
2366
2482
  data: {
2367
2483
  target: link.target,
@@ -2384,7 +2500,7 @@ function isSelfLoop(link) {
2384
2500
  }
2385
2501
 
2386
2502
  // kernel/orchestrator/node-identifiers.ts
2387
- import { posix as pathPosix3 } from "path";
2503
+ import { posix as pathPosix4 } from "path";
2388
2504
  function deriveNodeIdentifiers(node, kindDescriptor) {
2389
2505
  const sources = kindDescriptor?.identifiers;
2390
2506
  if (!sources || sources.length === 0) return [];
@@ -2408,16 +2524,16 @@ function readFrontmatterName(node) {
2408
2524
  return raw.length > 0 ? raw : null;
2409
2525
  }
2410
2526
  function readFilenameBasename(node) {
2411
- const base = pathPosix3.basename(node.path);
2527
+ const base = pathPosix4.basename(node.path);
2412
2528
  if (!base) return null;
2413
- const ext = pathPosix3.extname(base);
2529
+ const ext = pathPosix4.extname(base);
2414
2530
  const stem = ext ? base.slice(0, -ext.length) : base;
2415
2531
  return stem.length > 0 ? stem : null;
2416
2532
  }
2417
2533
  function readDirname(node) {
2418
- const dir = pathPosix3.dirname(node.path);
2534
+ const dir = pathPosix4.dirname(node.path);
2419
2535
  if (!dir || dir === "." || dir === "/") return null;
2420
- const base = pathPosix3.basename(dir);
2536
+ const base = pathPosix4.basename(dir);
2421
2537
  return base.length > 0 ? base : null;
2422
2538
  }
2423
2539
 
@@ -2492,7 +2608,7 @@ var NAME_RESERVED_TEXTS = {
2492
2608
  * a runtime built-in. Same wording skill-map shipped before the
2493
2609
  * source-side link finding landed.
2494
2610
  */
2495
- message: "{{path}} shadows a built-in {{provider}} {{kind}}. The runtime ignores this file in favour of its own built-in. Rename the file or `frontmatter.name` to a non-reserved value.",
2611
+ message: "Built-in {{provider}} {{kind}}:\nShadowed by this file; the runtime uses its built-in instead. Rename the file or its `frontmatter.name`.",
2496
2612
  /**
2497
2613
  * Source-side message: emitted on the node that AUTHORED a link
2498
2614
  * whose target resolves to a reserved name. Explains WHY the link's
@@ -2500,13 +2616,17 @@ var NAME_RESERVED_TEXTS = {
2500
2616
  * the kernel saw the target match a runtime built-in and downgraded
2501
2617
  * the edge so the operator notices.
2502
2618
  */
2503
- linkMessage: "Link `{{kind}} {{target}}` resolves to a name reserved by the {{provider}} runtime ({{reservedKind}} `{{reservedPath}}`). The runtime shadows the user file, so this edge is downgraded to confidence {{confidence}} instead of 1.0. Rename the target file or its `frontmatter.name` to a non-reserved value."
2619
+ linkMessage: "{{target}}:\nResolves to a {{provider}} built-in ({{reservedKind}} `{{reservedPath}}`){{where}}; edge downgraded to confidence {{confidence}}. Rename the target file or its `frontmatter.name`.",
2620
+ /** Location suffix after the built-in parens, one detection site. */
2621
+ whereSingle: " (line {{lines}})",
2622
+ /** Location suffix after the built-in parens, several detection sites. */
2623
+ wherePlural: " (lines {{lines}})"
2504
2624
  };
2505
2625
 
2506
2626
  // plugins/core/analyzers/name-reserved/index.ts
2507
- var ID17 = "name-reserved";
2627
+ var ID18 = "name-reserved";
2508
2628
  var nameReservedAnalyzer = {
2509
- id: ID17,
2629
+ id: ID18,
2510
2630
  pluginId: CORE_PLUGIN_ID,
2511
2631
  kind: "analyzer",
2512
2632
  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.",
@@ -2522,7 +2642,7 @@ var nameReservedAnalyzer = {
2522
2642
  const node = byPath3.get(path);
2523
2643
  if (!node) continue;
2524
2644
  issues.push({
2525
- analyzerId: ID17,
2645
+ analyzerId: ID18,
2526
2646
  severity: "warn",
2527
2647
  nodeIds: [node.path],
2528
2648
  message: tx(NAME_RESERVED_TEXTS.message, {
@@ -2538,16 +2658,16 @@ var nameReservedAnalyzer = {
2538
2658
  const reservedNode = findReservedNodeForLink(link, reserved, byPath3);
2539
2659
  if (!reservedNode) continue;
2540
2660
  issues.push({
2541
- analyzerId: ID17,
2661
+ analyzerId: ID18,
2542
2662
  severity: "warn",
2543
2663
  nodeIds: [link.source],
2544
2664
  message: tx(NAME_RESERVED_TEXTS.linkMessage, {
2545
- kind: link.kind,
2546
2665
  target: link.target,
2547
2666
  provider: reservedNode.provider,
2548
2667
  reservedKind: reservedNode.kind,
2549
2668
  reservedPath: reservedNode.path,
2550
- confidence: RESERVED_TARGET_CONFIDENCE.toFixed(2)
2669
+ confidence: RESERVED_TARGET_CONFIDENCE.toFixed(2),
2670
+ where: linkWhereSuffix(link)
2551
2671
  }),
2552
2672
  data: {
2553
2673
  target: link.target,
@@ -2562,6 +2682,12 @@ var nameReservedAnalyzer = {
2562
2682
  return issues;
2563
2683
  }
2564
2684
  };
2685
+ function linkWhereSuffix(link) {
2686
+ return linkWhere(link, {
2687
+ single: NAME_RESERVED_TEXTS.whereSingle,
2688
+ plural: NAME_RESERVED_TEXTS.wherePlural
2689
+ });
2690
+ }
2565
2691
  function findReservedNodeForLink(link, reserved, byPath3) {
2566
2692
  if (reserved.has(link.target)) {
2567
2693
  const node = byPath3.get(link.target);
@@ -2613,7 +2739,7 @@ var NODE_STABILITY_TEXTS = {
2613
2739
  };
2614
2740
 
2615
2741
  // plugins/core/analyzers/node-stability/index.ts
2616
- var ID18 = "node-stability";
2742
+ var ID19 = "node-stability";
2617
2743
  var EXPERIMENTAL_TOOLTIP = "Experimental: API may change";
2618
2744
  var DEPRECATED_TOOLTIP = "Deprecated: avoid in new code";
2619
2745
  var experimental = {
@@ -2635,7 +2761,7 @@ var setStabilityButton = {
2635
2761
  priority: 15
2636
2762
  };
2637
2763
  var nodeStabilityAnalyzer = {
2638
- id: ID18,
2764
+ id: ID19,
2639
2765
  pluginId: CORE_PLUGIN_ID,
2640
2766
  kind: "analyzer",
2641
2767
  description: "Reports a node's stability stage (`experimental`, `deprecated`) on the card.",
@@ -2654,7 +2780,7 @@ var nodeStabilityAnalyzer = {
2654
2780
  tooltip: EXPERIMENTAL_TOOLTIP
2655
2781
  });
2656
2782
  issues.push({
2657
- analyzerId: ID18,
2783
+ analyzerId: ID19,
2658
2784
  severity: "info",
2659
2785
  nodeIds: [node.path],
2660
2786
  message: `Node '${node.path}' is marked experimental: API may change.`,
@@ -2667,7 +2793,7 @@ var nodeStabilityAnalyzer = {
2667
2793
  severity: "warn"
2668
2794
  });
2669
2795
  issues.push({
2670
- analyzerId: ID18,
2796
+ analyzerId: ID19,
2671
2797
  severity: "warn",
2672
2798
  nodeIds: [node.path],
2673
2799
  message: `Node '${node.path}' is marked deprecated: avoid in new code.`,
@@ -2715,14 +2841,18 @@ function emitSetStabilityButton(ctx, nodePath, current) {
2715
2841
 
2716
2842
  // plugins/core/analyzers/node-superseded/text.ts
2717
2843
  var NODE_SUPERSEDED_TEXTS = {
2718
- /** `<path> is superseded by <supersededBy>` */
2719
- message: "{{path}} is superseded by {{supersededBy}}"
2844
+ /**
2845
+ * Compact finding grammar: line 1 = the superseding artifact, line
2846
+ * 2 = what it means. The superseded node is the finding's own node,
2847
+ * so its path never appears in the message.
2848
+ */
2849
+ message: "{{supersededBy}}:\nSupersedes this node."
2720
2850
  };
2721
2851
 
2722
2852
  // plugins/core/analyzers/node-superseded/index.ts
2723
- var ID19 = "node-superseded";
2853
+ var ID20 = "node-superseded";
2724
2854
  var nodeSupersededAnalyzer = {
2725
- id: ID19,
2855
+ id: ID20,
2726
2856
  pluginId: CORE_PLUGIN_ID,
2727
2857
  kind: "analyzer",
2728
2858
  description: "Marks nodes replaced by a newer one via `supersededBy`.",
@@ -2733,7 +2863,7 @@ var nodeSupersededAnalyzer = {
2733
2863
  const supersededBy = pickSupersededBy(node);
2734
2864
  if (supersededBy === null) continue;
2735
2865
  issues.push({
2736
- analyzerId: ID19,
2866
+ analyzerId: ID20,
2737
2867
  severity: "info",
2738
2868
  nodeIds: [node.path],
2739
2869
  message: tx(NODE_SUPERSEDED_TEXTS.message, {
@@ -2757,12 +2887,34 @@ function pickSupersededBy(node) {
2757
2887
  }
2758
2888
 
2759
2889
  // plugins/core/analyzers/reference-broken/index.ts
2760
- import { posix as pathPosix4, resolve as resolve3 } from "path";
2890
+ import { posix as pathPosix5, resolve as resolve3 } from "path";
2761
2891
 
2762
2892
  // plugins/core/analyzers/reference-broken/text.ts
2763
2893
  var REFERENCE_BROKEN_TEXTS = {
2764
- /** `Broken <kind> reference from <source> → <target>` */
2765
- message: "Broken {{kind}} reference from {{source}} \u2192 {{target}}",
2894
+ /**
2895
+ * Compact finding grammar: line 1 = the unresolved target, line 2 =
2896
+ * the short diagnosis plus WHERE the reference sits (`{{where}}` is
2897
+ * the pre-rendered location suffix below, or empty when the link
2898
+ * carries no line info). The source is the finding's own node, so it
2899
+ * never appears in the message.
2900
+ */
2901
+ message: "{{target}}:\nBroken {{kindLabel}}{{where}}.",
2902
+ /** Location suffix, one detection site. */
2903
+ whereSingle: " (line {{lines}})",
2904
+ /** Location suffix, several detection sites. */
2905
+ wherePlural: " (lines {{lines}})",
2906
+ /**
2907
+ * Human noun per link kind for the message above. Fallback for an
2908
+ * off-catalog kind: `<kind> link` (composed in the analyzer).
2909
+ */
2910
+ kindLabels: {
2911
+ references: "reference",
2912
+ mentions: "mention",
2913
+ invokes: "invocation",
2914
+ supersedes: "supersession",
2915
+ points: "pointer"
2916
+ },
2917
+ kindLabelFallback: "{{kind}} link",
2766
2918
  // Tooltips for the per-node view-contribution badges. Singular vs
2767
2919
  // plural keeps the count grammar correct without a sub-template.
2768
2920
  alertTooltipSingle: "This node has a broken reference. Open the inspector for details.",
@@ -2776,9 +2928,9 @@ var REFERENCE_BROKEN_TEXTS = {
2776
2928
  };
2777
2929
 
2778
2930
  // plugins/core/analyzers/reference-broken/index.ts
2779
- var ID20 = "reference-broken";
2931
+ var ID21 = "reference-broken";
2780
2932
  var referenceBrokenAnalyzer = {
2781
- id: ID20,
2933
+ id: ID21,
2782
2934
  pluginId: CORE_PLUGIN_ID,
2783
2935
  kind: "analyzer",
2784
2936
  description: "Flags arrows pointing at a node not part of the current scan.",
@@ -2815,7 +2967,7 @@ function buildIssue(link, hintCandidates = []) {
2815
2967
  trigger: link.trigger?.normalizedTrigger ?? null
2816
2968
  };
2817
2969
  const issue = {
2818
- analyzerId: ID20,
2970
+ analyzerId: ID21,
2819
2971
  // `error`, not `warn`: a link whose target is not in the scan is a
2820
2972
  // structural defect the operator must notice, and the card chip
2821
2973
  // paints `danger` (red) to match. Per the chip-vs-issue policy in
@@ -2825,33 +2977,37 @@ function buildIssue(link, hintCandidates = []) {
2825
2977
  severity: "error",
2826
2978
  nodeIds: [link.source],
2827
2979
  message: tx(REFERENCE_BROKEN_TEXTS.message, {
2828
- kind: link.kind,
2829
- source: link.source,
2830
- target: link.target
2980
+ target: link.target,
2981
+ kindLabel: REFERENCE_BROKEN_TEXTS.kindLabels[link.kind] ?? tx(REFERENCE_BROKEN_TEXTS.kindLabelFallback, { kind: link.kind }),
2982
+ where: linkWhere(link, {
2983
+ single: REFERENCE_BROKEN_TEXTS.whereSingle,
2984
+ plural: REFERENCE_BROKEN_TEXTS.wherePlural
2985
+ })
2831
2986
  }),
2832
2987
  data
2833
2988
  };
2834
- if (hintCandidates.length > 0) {
2835
- const suggestedName = (link.trigger?.normalizedTrigger ?? "").replace(/^[/@]/, "").trim();
2836
- const candidatePaths = hintCandidates.map((n) => n.path);
2837
- data["hint"] = {
2838
- kind: "missing-frontmatter-name",
2839
- suggestedName,
2840
- candidates: candidatePaths
2841
- };
2842
- issue.fix = {
2843
- summary: candidatePaths.length === 1 ? tx(REFERENCE_BROKEN_TEXTS.hintSummarySingle, {
2844
- name: suggestedName,
2845
- candidate: candidatePaths[0]
2846
- }) : tx(REFERENCE_BROKEN_TEXTS.hintSummaryMany, {
2847
- name: suggestedName,
2848
- candidates: candidatePaths.join(", ")
2849
- }),
2850
- autofixable: false
2851
- };
2852
- }
2989
+ if (hintCandidates.length > 0) attachHint(issue, data, link, hintCandidates);
2853
2990
  return issue;
2854
2991
  }
2992
+ function attachHint(issue, data, link, hintCandidates) {
2993
+ const suggestedName = (link.trigger?.normalizedTrigger ?? "").replace(/^[/@]/, "").trim();
2994
+ const candidatePaths = hintCandidates.map((n) => n.path);
2995
+ data["hint"] = {
2996
+ kind: "missing-frontmatter-name",
2997
+ suggestedName,
2998
+ candidates: candidatePaths
2999
+ };
3000
+ issue.fix = {
3001
+ summary: candidatePaths.length === 1 ? tx(REFERENCE_BROKEN_TEXTS.hintSummarySingle, {
3002
+ name: suggestedName,
3003
+ candidate: candidatePaths[0]
3004
+ }) : tx(REFERENCE_BROKEN_TEXTS.hintSummaryMany, {
3005
+ name: suggestedName,
3006
+ candidates: candidatePaths.join(", ")
3007
+ }),
3008
+ autofixable: false
3009
+ };
3010
+ }
2855
3011
  function resolvesViaReferencePaths(link, refIndex) {
2856
3012
  if (!isPathStyleLink(link)) return false;
2857
3013
  return refIndex.paths.has(resolve3(refIndex.cwd, link.target));
@@ -2870,8 +3026,8 @@ function indexByNormalizedName(nodes) {
2870
3026
  return out;
2871
3027
  }
2872
3028
  function basenameWithoutExt(path) {
2873
- const base = pathPosix4.basename(path);
2874
- const ext = pathPosix4.extname(base);
3029
+ const base = pathPosix5.basename(path);
3030
+ const ext = pathPosix5.extname(base);
2875
3031
  return ext ? base.slice(0, -ext.length) : base;
2876
3032
  }
2877
3033
  function indexByBasenameWithoutName(nodes) {
@@ -2917,25 +3073,29 @@ function isPathStyleLink(link) {
2917
3073
  // plugins/core/analyzers/reference-redundant/text.ts
2918
3074
  var REFERENCE_REDUNDANT_TEXTS = {
2919
3075
  /**
2920
- * Multi-form / multi-occurrence reference message. Short and direct:
2921
- * names the duplicated target + count and lists each occurrence
2922
- * (trigger + line) so the operator sees the offending spots at a
2923
- * glance. The source node is the finding's own node, so it is not
2924
- * repeated here.
3076
+ * Compact finding grammar (subject first, `\n` renders as a line
3077
+ * break in the inspector and flattens to a space in `sm check`):
3078
+ *
3079
+ * <resolvedTarget>:
3080
+ * Duplicate reference (2): `references/x.md` (124, 145).
3081
+ *
3082
+ * Occurrences are grouped BY TRIGGER: each distinct trigger text
3083
+ * appears once with its line numbers collapsed into one paren list.
3084
+ * The source node is the finding's own node, so it never appears.
2925
3085
  */
2926
- message: "Duplicate reference to {{resolvedTarget}} ({{count}} occurrences): {{occurrences}}.",
2927
- /** Inline separator between occurrences in the message. */
3086
+ message: "{{resolvedTarget}}:\nDuplicate reference ({{count}}): {{occurrences}}.",
3087
+ /** Inline separator between trigger groups in the message. */
2928
3088
  occurrenceSeparator: ", ",
2929
- /** Per-occurrence formatting (trigger + line). */
2930
- occurrence: "`{{trigger}}` ({{kind}}, line {{line}})",
2931
- /** Per-occurrence formatting when the extractor did not record a line. */
2932
- occurrenceUnknownLine: "`{{trigger}}` ({{kind}}, unknown line)"
3089
+ /** Per-trigger formatting: the trigger once, its lines grouped. */
3090
+ occurrence: "`{{trigger}}` ({{lines}})",
3091
+ /** Placeholder for an occurrence whose extractor recorded no line. */
3092
+ lineUnknown: "?"
2933
3093
  };
2934
3094
 
2935
3095
  // plugins/core/analyzers/reference-redundant/index.ts
2936
- var ID21 = "reference-redundant";
3096
+ var ID22 = "reference-redundant";
2937
3097
  var referenceRedundantAnalyzer = {
2938
- id: ID21,
3098
+ id: ID22,
2939
3099
  pluginId: CORE_PLUGIN_ID,
2940
3100
  kind: "analyzer",
2941
3101
  description: "Flags when one node references the same target through two or more different links (e.g. a markdown link plus a `references:` entry).",
@@ -2961,14 +3121,13 @@ var referenceRedundantAnalyzer = {
2961
3121
  const [source, resolvedTarget] = key.split("\0");
2962
3122
  const flat = flattenOccurrences(links);
2963
3123
  issues.push({
2964
- analyzerId: ID21,
2965
- severity: "warn",
3124
+ analyzerId: ID22,
3125
+ severity: "info",
2966
3126
  nodeIds: [source],
2967
3127
  message: tx(REFERENCE_REDUNDANT_TEXTS.message, {
2968
- source,
2969
3128
  resolvedTarget,
2970
3129
  count: flat.length,
2971
- occurrences: flat.map(formatOccurrence).join(REFERENCE_REDUNDANT_TEXTS.occurrenceSeparator)
3130
+ occurrences: formatGroupedOccurrences(flat)
2972
3131
  }),
2973
3132
  data: {
2974
3133
  target: resolvedTarget,
@@ -3015,11 +3174,19 @@ function flattenOccurrences(links) {
3015
3174
  });
3016
3175
  return out;
3017
3176
  }
3018
- function formatOccurrence(occ) {
3019
- if (occ.line === null) {
3020
- return tx(REFERENCE_REDUNDANT_TEXTS.occurrenceUnknownLine, { trigger: occ.originalTrigger, kind: occ.kind });
3021
- }
3022
- return tx(REFERENCE_REDUNDANT_TEXTS.occurrence, { trigger: occ.originalTrigger, kind: occ.kind, line: occ.line });
3177
+ function formatGroupedOccurrences(occurrences) {
3178
+ const byTrigger = /* @__PURE__ */ new Map();
3179
+ for (const occ of occurrences) {
3180
+ const bucket = byTrigger.get(occ.originalTrigger);
3181
+ if (bucket) bucket.push(occ);
3182
+ else byTrigger.set(occ.originalTrigger, [occ]);
3183
+ }
3184
+ return [...byTrigger.entries()].map(
3185
+ ([trigger, occs]) => tx(REFERENCE_REDUNDANT_TEXTS.occurrence, {
3186
+ trigger,
3187
+ lines: occs.map((o) => o.line === null ? REFERENCE_REDUNDANT_TEXTS.lineUnknown : String(o.line)).join(", ")
3188
+ })
3189
+ ).join(REFERENCE_REDUNDANT_TEXTS.occurrenceSeparator);
3023
3190
  }
3024
3191
  function buildNameIndex2(nodes) {
3025
3192
  const out = /* @__PURE__ */ new Map();
@@ -3311,12 +3478,14 @@ function existsSyncSafe(path) {
3311
3478
 
3312
3479
  // plugins/core/analyzers/schema-violation/text.ts
3313
3480
  var SCHEMA_VIOLATION_TEXTS = {
3314
- /** `Node <path> failed schema validation: <errors>` */
3315
- nodeFailure: "Node {{path}} failed schema validation: {{errors}}",
3316
- /** `Link <source> → <target> failed schema validation: <errors>` */
3317
- linkFailure: "Link {{source}} \u2192 {{target}} failed schema validation: {{errors}}",
3318
- /** `Node <path> is missing required frontmatter fields: <missing>` */
3319
- frontmatterBaseFailure: "Node {{path}} is missing required frontmatter fields: {{missing}}.",
3481
+ // Compact finding grammar: the affected node (or the link's source)
3482
+ // is the finding's own node, so its path never appears.
3483
+ /** `Schema validation failed: <errors>` */
3484
+ nodeFailure: "Schema validation failed: {{errors}}",
3485
+ /** `<target>:\nLink failed schema validation: <errors>` */
3486
+ linkFailure: "{{target}}:\nLink failed schema validation: {{errors}}",
3487
+ /** `Missing required frontmatter: <missing>.` */
3488
+ frontmatterBaseFailure: "Missing required frontmatter: {{missing}}.",
3320
3489
  /** Singular tooltip on the alert / chip when a node has exactly one validation failure. */
3321
3490
  alertTooltipSingle: "Frontmatter or schema validation failed.",
3322
3491
  /** Plural tooltip; `{{count}}` capped at 99 in the chip badge but the tooltip text shows the raw count. */
@@ -3324,9 +3493,9 @@ var SCHEMA_VIOLATION_TEXTS = {
3324
3493
  };
3325
3494
 
3326
3495
  // plugins/core/analyzers/schema-violation/index.ts
3327
- var ID22 = "schema-violation";
3496
+ var ID23 = "schema-violation";
3328
3497
  var schemaViolationAnalyzer = {
3329
- id: ID22,
3498
+ id: ID23,
3330
3499
  pluginId: CORE_PLUGIN_ID,
3331
3500
  kind: "analyzer",
3332
3501
  description: "Flags nodes or links that violate the project schemas.",
@@ -3377,7 +3546,7 @@ function collectNodeFindings(v, node, out) {
3377
3546
  const result = v.validate("node", toNodeForSchema(node));
3378
3547
  if (result.ok) return;
3379
3548
  out.push({
3380
- analyzerId: ID22,
3549
+ analyzerId: ID23,
3381
3550
  severity: "error",
3382
3551
  nodeIds: [node.path],
3383
3552
  message: tx(SCHEMA_VIOLATION_TEXTS.nodeFailure, {
@@ -3396,7 +3565,7 @@ function collectFrontmatterBaseFindings(node, out) {
3396
3565
  if (isMissingStringField(fm, "description")) missing.push("description");
3397
3566
  if (missing.length === 0) return;
3398
3567
  out.push({
3399
- analyzerId: ID22,
3568
+ analyzerId: ID23,
3400
3569
  // `warn` (not `error`) so the default `sm scan` exit code stays
3401
3570
  // 0 even when nodes are missing frontmatter base fields. Strict
3402
3571
  // mode (`sm scan --strict`) still escalates to exit 1. Matches
@@ -3418,7 +3587,7 @@ function collectLinkFindings(v, link, out) {
3418
3587
  const result = v.validate("link", toLinkForSchema(link));
3419
3588
  if (result.ok) return;
3420
3589
  out.push({
3421
- analyzerId: ID22,
3590
+ analyzerId: ID23,
3422
3591
  severity: "error",
3423
3592
  nodeIds: [link.source],
3424
3593
  message: tx(SCHEMA_VIOLATION_TEXTS.linkFailure, {
@@ -3473,7 +3642,7 @@ var SIGNAL_COLLISION_TEXTS = {
3473
3642
  * the same paragraph, the markdown-link wins and the at-directive
3474
3643
  * silently disappears without this warning).
3475
3644
  */
3476
- message: "{{loserExtractor}} detected `{{loserRaw}}` at offset {{loserRange}} but {{winnerExtractor}}'s detection at {{winnerRange}} won the overlap collision ({{reason}}). The graph shows the winning edge only; the loser is not persisted.",
3645
+ message: "`{{loserRaw}}`:\nOverlap collision: {{loserExtractor}} (at {{loserRange}}) lost to {{winnerExtractor}} (at {{winnerRange}}) by {{reason}}; only the winning edge persists.",
3477
3646
  /**
3478
3647
  * Same warn but for the rare case the resolver rejected a Signal
3479
3648
  * because the operator disabled its extractor via
@@ -3482,20 +3651,20 @@ var SIGNAL_COLLISION_TEXTS = {
3482
3651
  * resolver; documented now so the analyzer stays forward-compatible
3483
3652
  * with the upcoming filter pass.
3484
3653
  */
3485
- messageExtractorDisabled: "Extension `{{extractorId}}` is disabled; its detection `{{loserRaw}}` (offset {{loserRange}}) did not produce a Link. Re-enable the extension in Settings or via `sm plugins enable` to surface its edges.",
3654
+ messageExtractorDisabled: "`{{loserRaw}}`:\nDropped: extension `{{extractorId}}` is disabled. Re-enable it in Settings or via `sm plugins enable`.",
3486
3655
  /**
3487
3656
  * Same warn but for the future confidence floor case. Phase 4+ stub:
3488
3657
  * today the resolver materialises every winning candidate regardless
3489
3658
  * of confidence, so this template is unreachable; documented for
3490
3659
  * forward compatibility.
3491
3660
  */
3492
- messageBelowFloor: "Detection `{{loserRaw}}` (offset {{loserRange}}, confidence {{confidence}}) fell below the configured threshold {{threshold}} and was dropped."
3661
+ messageBelowFloor: "`{{loserRaw}}`:\nDropped: confidence {{confidence}} is below the threshold {{threshold}}."
3493
3662
  };
3494
3663
 
3495
3664
  // plugins/core/analyzers/signal-collision/index.ts
3496
- var ID23 = "signal-collision";
3665
+ var ID24 = "signal-collision";
3497
3666
  var signalCollisionAnalyzer = {
3498
- id: ID23,
3667
+ id: ID24,
3499
3668
  pluginId: CORE_PLUGIN_ID,
3500
3669
  kind: "analyzer",
3501
3670
  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.",
@@ -3520,7 +3689,7 @@ function makeIssue(signal) {
3520
3689
  const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
3521
3690
  const winnerRange = `${winner.range.start}-${winner.range.end}`;
3522
3691
  return {
3523
- analyzerId: ID23,
3692
+ analyzerId: ID24,
3524
3693
  severity: "warn",
3525
3694
  nodeIds: [signal.source],
3526
3695
  message: tx(SIGNAL_COLLISION_TEXTS.message, {
@@ -3553,7 +3722,7 @@ function makeIssue(signal) {
3553
3722
  if (resolution.extractorDisabled) {
3554
3723
  const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
3555
3724
  return {
3556
- analyzerId: ID23,
3725
+ analyzerId: ID24,
3557
3726
  severity: "warn",
3558
3727
  nodeIds: [signal.source],
3559
3728
  message: tx(SIGNAL_COLLISION_TEXTS.messageExtractorDisabled, {
@@ -3572,7 +3741,7 @@ function makeIssue(signal) {
3572
3741
  const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
3573
3742
  const topCandidate = signal.candidates[0];
3574
3743
  return {
3575
- analyzerId: ID23,
3744
+ analyzerId: ID24,
3576
3745
  severity: "warn",
3577
3746
  nodeIds: [signal.source],
3578
3747
  message: tx(SIGNAL_COLLISION_TEXTS.messageBelowFloor, {
@@ -3604,13 +3773,13 @@ var SUPERSEDE_TEXTS = {
3604
3773
  };
3605
3774
 
3606
3775
  // plugins/core/analyzers/supersede/index.ts
3607
- var ID24 = "supersede";
3776
+ var ID25 = "supersede";
3608
3777
  var supersedeButton = {
3609
3778
  slot: "inspector.action.button",
3610
3779
  priority: 10
3611
3780
  };
3612
3781
  var supersedeAnalyzer = {
3613
- id: ID24,
3782
+ id: ID25,
3614
3783
  pluginId: CORE_PLUGIN_ID,
3615
3784
  kind: "analyzer",
3616
3785
  description: 'Projects the inspector "Supersede" button (declares a node replaced by another).',
@@ -3665,13 +3834,13 @@ var TAGS_TEXTS = {
3665
3834
  };
3666
3835
 
3667
3836
  // plugins/core/analyzers/tags/index.ts
3668
- var ID25 = "tags";
3837
+ var ID26 = "tags";
3669
3838
  var setTagsButton = {
3670
3839
  slot: "inspector.action.button",
3671
3840
  priority: 15
3672
3841
  };
3673
3842
  var tagsAnalyzer = {
3674
- id: ID25,
3843
+ id: ID26,
3675
3844
  pluginId: CORE_PLUGIN_ID,
3676
3845
  kind: "analyzer",
3677
3846
  description: `Projects the inspector "Edit tags" button (edits a node's taxonomy tags).`,
@@ -3714,18 +3883,18 @@ var TRIGGER_COLLISION_TEXTS = {
3714
3883
  * cause part. Used for the advertiser-ambiguous-only, invocation-
3715
3884
  * ambiguous-only, and cross-kind-only branches.
3716
3885
  */
3717
- messageOnePart: 'Trigger "{{normalized}}" has {{part}}.',
3886
+ messageOnePart: '"{{normalized}}":\nTrigger collision: {{part}}.',
3718
3887
  /**
3719
3888
  * Top-level message when `analyzeTriggerBucket` accumulated two cause
3720
3889
  * parts (advertiser-ambiguous AND invocation-ambiguous fire together).
3721
3890
  * The joiner lives inside the template so future locales can adapt it
3722
3891
  * (e.g. `'; y '` in Spanish) without touching the rule code.
3723
3892
  */
3724
- messageTwoParts: 'Trigger "{{normalized}}" has {{first}}; and {{second}}.',
3725
- /** `<n> nodes advertise it: <list>` part, fires on the advertiser-ambiguous branch. */
3726
- partAdvertisers: "{{count}} nodes advertise it: {{paths}}",
3727
- /** `<n> distinct invocation forms: <list>` part, fires on the invocation-ambiguous branch. */
3728
- partInvocations: "{{count}} distinct invocation forms: {{forms}}",
3893
+ messageTwoParts: '"{{normalized}}":\nTrigger collision: {{first}}; and {{second}}.',
3894
+ /** `<n> advertisers: <list>` part, fires on the advertiser-ambiguous branch. */
3895
+ partAdvertisers: "{{count}} advertisers: {{paths}}",
3896
+ /** `<n> invocation forms: <list>` part, fires on the invocation-ambiguous branch. */
3897
+ partInvocations: "{{count}} invocation forms: {{forms}}",
3729
3898
  /** Singular cross-kind cause: `non-canonical invocation <form> against advertiser <path>`. */
3730
3899
  partNonCanonicalSingular: "non-canonical invocation {{forms}} against advertiser {{advertiser}}",
3731
3900
  /** Plural cross-kind cause: `non-canonical invocations <forms> against advertiser <path>`. */
@@ -3733,14 +3902,14 @@ var TRIGGER_COLLISION_TEXTS = {
3733
3902
  };
3734
3903
 
3735
3904
  // plugins/core/analyzers/trigger-collision/index.ts
3736
- var ID26 = "trigger-collision";
3905
+ var ID27 = "trigger-collision";
3737
3906
  var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
3738
3907
  "command",
3739
3908
  "skill",
3740
3909
  "agent"
3741
3910
  ]);
3742
3911
  var triggerCollisionAnalyzer = {
3743
- id: ID26,
3912
+ id: ID27,
3744
3913
  pluginId: CORE_PLUGIN_ID,
3745
3914
  kind: "analyzer",
3746
3915
  mode: "deterministic",
@@ -3838,7 +4007,7 @@ function analyzeTriggerBucket(normalized, claims) {
3838
4007
  part: parts[0]
3839
4008
  });
3840
4009
  return {
3841
- analyzerId: ID26,
4010
+ analyzerId: ID27,
3842
4011
  severity: "error",
3843
4012
  nodeIds,
3844
4013
  message,
@@ -3878,13 +4047,13 @@ var ASCII_FORMATTER_TEXTS = {
3878
4047
  };
3879
4048
 
3880
4049
  // plugins/core/formatters/ascii/index.ts
3881
- var ID27 = "ascii";
4050
+ var ID28 = "ascii";
3882
4051
  var KIND_ORDER = ["agent", "command", "skill", "markdown"];
3883
4052
  var asciiFormatter = {
3884
- id: ID27,
4053
+ id: ID28,
3885
4054
  pluginId: CORE_PLUGIN_ID,
3886
4055
  kind: "formatter",
3887
- formatId: ID27,
4056
+ formatId: ID28,
3888
4057
  description: "Renders the scan as plain text in three sections: nodes (grouped by kind), arrows, and issues. Used by `sm scan --format ascii`.",
3889
4058
  // ASCII tree formatter, header + per-kind sections + per-issue
3890
4059
  // section. Each section iterates and renders; splitting per section
@@ -3978,13 +4147,13 @@ function renderSection(out, kind, group) {
3978
4147
  }
3979
4148
 
3980
4149
  // plugins/core/formatters/json/index.ts
3981
- var ID28 = "json";
4150
+ var ID29 = "json";
3982
4151
  var jsonFormatter = {
3983
- id: ID28,
4152
+ id: ID29,
3984
4153
  pluginId: CORE_PLUGIN_ID,
3985
4154
  kind: "formatter",
3986
4155
  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`.",
3987
- formatId: ID28,
4156
+ formatId: ID29,
3988
4157
  format(ctx) {
3989
4158
  if (ctx.scanResult !== void 0) {
3990
4159
  return JSON.stringify(ctx.scanResult);
@@ -4123,9 +4292,9 @@ function resolveSpecRoot2() {
4123
4292
  }
4124
4293
 
4125
4294
  // plugins/core/actions/node-bump/index.ts
4126
- var ID29 = "node-bump";
4295
+ var ID30 = "node-bump";
4127
4296
  var nodeBumpAction = {
4128
- id: ID29,
4297
+ id: ID30,
4129
4298
  pluginId: CORE_PLUGIN_ID,
4130
4299
  kind: "action",
4131
4300
  description: "Marks a node as updated: bumps `annotations.version`, refreshes sidecar hashes, and records the timestamp.",
@@ -4184,9 +4353,9 @@ function pickCurrentVersion(overlay) {
4184
4353
 
4185
4354
  // plugins/core/actions/node-set-stability/index.ts
4186
4355
  var STABILITY_VALUES = ["experimental", "stable", "deprecated"];
4187
- var ID30 = "node-set-stability";
4356
+ var ID31 = "node-set-stability";
4188
4357
  var nodeSetStabilityAction = {
4189
- id: ID30,
4358
+ id: ID31,
4190
4359
  pluginId: CORE_PLUGIN_ID,
4191
4360
  kind: "action",
4192
4361
  description: "Sets the lifecycle stage of the current node (writes `stability` to the sidecar).",
@@ -4226,9 +4395,9 @@ function invokeSetStability(input, ctx) {
4226
4395
  }
4227
4396
 
4228
4397
  // plugins/core/actions/node-set-tags/index.ts
4229
- var ID31 = "node-set-tags";
4398
+ var ID32 = "node-set-tags";
4230
4399
  var nodeSetTagsAction = {
4231
- id: ID31,
4400
+ id: ID32,
4232
4401
  pluginId: CORE_PLUGIN_ID,
4233
4402
  kind: "action",
4234
4403
  description: "Sets the taxonomy tags of the current node (writes `tags` to the sidecar; whole-array replace).",
@@ -4265,9 +4434,9 @@ function invokeSetTags(input, ctx) {
4265
4434
  }
4266
4435
 
4267
4436
  // plugins/core/actions/node-supersede/index.ts
4268
- var ID32 = "node-supersede";
4437
+ var ID33 = "node-supersede";
4269
4438
  var nodeSupersedeAction = {
4270
- id: ID32,
4439
+ id: ID33,
4271
4440
  pluginId: CORE_PLUGIN_ID,
4272
4441
  kind: "action",
4273
4442
  description: "Declares the current node as superseded by another (writes `supersededBy` to the sidecar).",
@@ -4785,15 +4954,16 @@ var updateCheckHook = {
4785
4954
  var claudeProvider2 = { ...claudeProvider, pluginId: "claude", version: VERSION };
4786
4955
  var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "claude", version: VERSION };
4787
4956
  var slashCommandExtractor2 = { ...slashCommandExtractor, pluginId: "claude", version: VERSION };
4957
+ var toolsCounterExtractor2 = { ...toolsCounterExtractor, pluginId: "claude", version: VERSION };
4788
4958
  var antigravityProvider2 = { ...antigravityProvider, pluginId: "antigravity", version: VERSION };
4789
4959
  var openaiProvider2 = { ...openaiProvider, pluginId: "openai", version: VERSION };
4790
4960
  var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills", version: VERSION };
4791
4961
  var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core", version: VERSION };
4792
4962
  var annotationsExtractor2 = { ...annotationsExtractor, pluginId: "core", version: VERSION };
4963
+ var backtickPathExtractor2 = { ...backtickPathExtractor, pluginId: "core", version: VERSION };
4793
4964
  var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core", version: VERSION };
4794
4965
  var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core", version: VERSION };
4795
4966
  var mcpToolsExtractor2 = { ...mcpToolsExtractor, pluginId: "core", version: VERSION };
4796
- var toolsCounterExtractor2 = { ...toolsCounterExtractor, pluginId: "core", version: VERSION };
4797
4967
  var annotationFieldUnknownAnalyzer2 = { ...annotationFieldUnknownAnalyzer, pluginId: "core", version: VERSION };
4798
4968
  var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core", version: VERSION };
4799
4969
  var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core", version: VERSION };
@@ -4827,7 +4997,8 @@ var builtInPlugins = [
4827
4997
  extensions: [
4828
4998
  claudeProvider2,
4829
4999
  atDirectiveExtractor2,
4830
- slashCommandExtractor2
5000
+ slashCommandExtractor2,
5001
+ toolsCounterExtractor2
4831
5002
  ]
4832
5003
  },
4833
5004
  {
@@ -4857,10 +5028,10 @@ var builtInPlugins = [
4857
5028
  extensions: [
4858
5029
  coreMarkdownProvider2,
4859
5030
  annotationsExtractor2,
5031
+ backtickPathExtractor2,
4860
5032
  externalUrlCounterExtractor2,
4861
5033
  markdownLinkExtractor2,
4862
5034
  mcpToolsExtractor2,
4863
- toolsCounterExtractor2,
4864
5035
  annotationFieldUnknownAnalyzer2,
4865
5036
  annotationOrphanAnalyzer2,
4866
5037
  annotationStaleAnalyzer2,
@@ -7784,7 +7955,8 @@ var LINK_KIND_VALUES = Object.freeze([
7784
7955
  "invokes",
7785
7956
  "references",
7786
7957
  "mentions",
7787
- "supersedes"
7958
+ "supersedes",
7959
+ "points"
7788
7960
  ]);
7789
7961
  var SEVERITY_VALUES = Object.freeze([
7790
7962
  "error",
@@ -10802,11 +10974,13 @@ var PluginLoader = class {
10802
10974
  if (kind === "provider" && discoveredKinds) {
10803
10975
  instance["kinds"] = discoveredKinds;
10804
10976
  }
10977
+ const stability = exported["stability"];
10805
10978
  return { ok: true, extension: {
10806
10979
  kind,
10807
10980
  id: pathId2,
10808
10981
  pluginId,
10809
10982
  version: exported["version"],
10983
+ ...stability !== void 0 ? { stability } : {},
10810
10984
  entryPath: abs,
10811
10985
  module: mod,
10812
10986
  instance
@@ -11796,7 +11970,7 @@ function renderHuman(issues, ansi) {
11796
11970
  tx(CHECK_TEXTS.issueRow, {
11797
11971
  glyph: severityGlyph(row.severity, ansi),
11798
11972
  analyzerId: ansi.dim(row.analyzerId.padEnd(analyzerWidth)),
11799
- message: trimRedundantPath(row.message, row.primary)
11973
+ message: flattenMessage(trimRedundantPath(row.message, row.primary))
11800
11974
  })
11801
11975
  );
11802
11976
  }
@@ -11850,6 +12024,9 @@ function trimRedundantPath(message, primary) {
11850
12024
  if (!message.includes(needle)) return message;
11851
12025
  return message.replace(needle, "");
11852
12026
  }
12027
+ function flattenMessage(message) {
12028
+ return message.replace(/\n+/g, " ");
12029
+ }
11853
12030
 
11854
12031
  // cli/commands/config.ts
11855
12032
  import { existsSync as existsSync16 } from "fs";
@@ -15823,8 +16000,9 @@ function buildVirtualNode(extractor, emitted, emitter) {
15823
16000
  if (emitted.frontmatter) node.frontmatter = emitted.frontmatter;
15824
16001
  return node;
15825
16002
  }
16003
+ var KNOWN_LINK_KINDS = ["invokes", "references", "mentions", "supersedes", "points"];
15826
16004
  function validateLink(extractor, link, emitter) {
15827
- const knownKinds = ["invokes", "references", "mentions", "supersedes"];
16005
+ const knownKinds = KNOWN_LINK_KINDS;
15828
16006
  if (!knownKinds.includes(link.kind)) {
15829
16007
  const qualifiedId2 = `${extractor.pluginId}/${extractor.id}`;
15830
16008
  emitter.emit(
@@ -15859,7 +16037,6 @@ function validateLink(extractor, link, emitter) {
15859
16037
  const confidence = c ?? ConfidenceTier.MEDIUM;
15860
16038
  return { ...link, confidence };
15861
16039
  }
15862
- var KNOWN_LINK_KINDS = ["invokes", "references", "mentions", "supersedes"];
15863
16040
  function validateSignal(extractor, signal, emitter) {
15864
16041
  const qualifiedId2 = qualifiedExtensionId(extractor.pluginId, extractor.id);
15865
16042
  if (!Array.isArray(signal.candidates) || signal.candidates.length === 0) {
@@ -20469,6 +20646,13 @@ var PLUGINS_TEXTS = {
20469
20646
  * the visible output stay in sync.
20470
20647
  */
20471
20648
  pluginSubIndent: " ",
20649
+ /**
20650
+ * Lifecycle tag appended to an extension name in list / show rows
20651
+ * when the manifest declares a non-default `stability` (anything but
20652
+ * `stable`). Inherits the surrounding line's color; `stable`
20653
+ * (declared or defaulted) renders no tag.
20654
+ */
20655
+ stabilityTag: " ({{stability}})",
20472
20656
  listTipShow: "\nTip: `sm plugins show <id>` for kinds, versions, and per-extension status.\n",
20473
20657
  /** Show command, built-in header (no version row, no path). */
20474
20658
  detailHeaderBuiltIn: " {{glyph}} {{id}} {{source}} {{count}} extension{{plural}}\n",
@@ -20624,9 +20808,14 @@ function extensionRowFromBuiltIn(ext, plugin, resolveEnabled) {
20624
20808
  enabled: resolveEnabled(qualifiedExtensionId(plugin.id, ext.id)),
20625
20809
  description: ext.description ?? ""
20626
20810
  };
20811
+ if (ext.stability !== void 0) row.stability = ext.stability;
20627
20812
  if (ext.entry !== void 0) row.entry = ext.entry;
20628
20813
  return row;
20629
20814
  }
20815
+ function withStabilityTag(name, stability) {
20816
+ if (!stability || stability === "stable") return name;
20817
+ return name + tx(PLUGINS_TEXTS.stabilityTag, { stability: sanitizeForTerminal(stability) });
20818
+ }
20630
20819
  function omitModule(key, value) {
20631
20820
  if (key !== "module") return value;
20632
20821
  if (value === null || typeof value !== "object") return value;
@@ -20713,9 +20902,10 @@ function renderListHuman(builtIns2, plugins, resolveEnabled, ansi) {
20713
20902
  return lines.join("\n") + "\n" + PLUGINS_TEXTS.listTipShow;
20714
20903
  }
20715
20904
  function builtInToListRow(b) {
20716
- const names = b.extensions.map(
20717
- (e) => e.enabled ? e.id : `${PLUGINS_TEXTS.rowGlyphOff} ${e.id}`
20718
- );
20905
+ const names = b.extensions.map((e) => {
20906
+ const name = withStabilityTag(e.id, e.stability);
20907
+ return e.enabled ? name : `${PLUGINS_TEXTS.rowGlyphOff} ${name}`;
20908
+ });
20719
20909
  return {
20720
20910
  id: b.id,
20721
20911
  enabled: b.enabled,
@@ -20728,7 +20918,7 @@ function pluginToListRow(p, resolveEnabled) {
20728
20918
  const extensions = p.extensions ?? [];
20729
20919
  const enabled = isLoaded ? extensions.length === 0 || extensions.some((e) => resolveEnabled(qualifiedExtensionId(p.id, e.id))) : false;
20730
20920
  const names = extensions.map((e) => {
20731
- const safeId = sanitizeForTerminal(e.id);
20921
+ const safeId = withStabilityTag(sanitizeForTerminal(e.id), e.stability);
20732
20922
  return resolveEnabled(qualifiedExtensionId(p.id, e.id)) ? safeId : `${PLUGINS_TEXTS.rowGlyphOff} ${safeId}`;
20733
20923
  });
20734
20924
  const reason = p.status === "enabled" ? void 0 : sanitizeForTerminal(p.reason ?? "") || void 0;
@@ -20908,7 +21098,7 @@ function renderBuiltInDetail(b, ansi) {
20908
21098
  const items = sorted.map((ext) => ({
20909
21099
  glyph: ext.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff),
20910
21100
  kind: ext.kind,
20911
- name: `${b.id}/${ext.id}`
21101
+ name: withStabilityTag(`${b.id}/${ext.id}`, ext.stability)
20912
21102
  }));
20913
21103
  return tx(PLUGINS_TEXTS.detailHeaderBuiltIn, {
20914
21104
  glyph,
@@ -20987,7 +21177,7 @@ function collectPluginExtensionItems(match, ansi) {
20987
21177
  // status header above (✕ on the row).
20988
21178
  glyph: ansi.green(PLUGINS_TEXTS.rowGlyphOk),
20989
21179
  kind: sanitizeForTerminal(ext.kind),
20990
- name: `${safePluginId}/${safeExtId}`,
21180
+ name: withStabilityTag(`${safePluginId}/${safeExtId}`, ext.stability),
20991
21181
  version: sanitizeForTerminal(ext.version)
20992
21182
  };
20993
21183
  });
@@ -21021,6 +21211,7 @@ function renderBuiltInExtensionDetail(pluginId, ext, ansi) {
21021
21211
  source: ansi.dim(PLUGINS_TEXTS.sourceBuiltIn)
21022
21212
  });
21023
21213
  const meta = { kind: ext.kind };
21214
+ if (ext.stability && ext.stability !== "stable") meta.stability = ext.stability;
21024
21215
  if (ext.description) meta.description = ext.description;
21025
21216
  if (ext.entry !== void 0) meta.entry = ext.entry;
21026
21217
  return header + "\n" + renderExtensionFields(meta);
@@ -21038,7 +21229,7 @@ function renderUserExtensionDetail(pluginId, ext, ansi) {
21038
21229
  version: ext.version,
21039
21230
  entry: ext.entryPath
21040
21231
  };
21041
- if (meta.stability !== void 0) input.stability = meta.stability;
21232
+ if (ext.stability && ext.stability !== "stable") input.stability = ext.stability;
21042
21233
  if (meta.description !== void 0) input.description = meta.description;
21043
21234
  if (meta.preconditions !== void 0) input.preconditions = meta.preconditions;
21044
21235
  return header + "\n" + renderExtensionFields(input);
@@ -21048,7 +21239,6 @@ function readInstanceMeta(instance) {
21048
21239
  const obj = instance;
21049
21240
  const out = {};
21050
21241
  if (typeof obj["description"] === "string") out.description = obj["description"];
21051
- if (typeof obj["stability"] === "string") out.stability = obj["stability"];
21052
21242
  if (Array.isArray(obj["preconditions"])) {
21053
21243
  out.preconditions = obj["preconditions"].filter(
21054
21244
  (p) => typeof p === "string"
@@ -25737,6 +25927,7 @@ function buildBuiltInItems(resolveEnabled) {
25737
25927
  version: ext.version,
25738
25928
  enabled: resolveEnabled(qualified),
25739
25929
  ...ext.description ? { description: ext.description } : {},
25930
+ ...ext.stability ? { stability: ext.stability } : {},
25740
25931
  ...extLocked ? { locked: true } : {}
25741
25932
  };
25742
25933
  });
@@ -25792,6 +25983,7 @@ function projectExtensionRows(plugin, resolveEnabled, pluginLocked) {
25792
25983
  version: ext.version,
25793
25984
  enabled: resolveEnabled(qualified),
25794
25985
  ...description ? { description } : {},
25986
+ ...ext.stability ? { stability: ext.stability } : {},
25795
25987
  ...extLocked ? { locked: true } : {}
25796
25988
  };
25797
25989
  });
@@ -30277,4 +30469,4 @@ function resolveBareDefault() {
30277
30469
  process.exit(ExitCode.Error);
30278
30470
  }
30279
30471
  //# sourceMappingURL=cli.js.map
30280
- //# debugId=3bc438eb-6e57-550d-aab1-392242aca915
30472
+ //# debugId=3118bb11-aad2-50b2-b70d-ed15c2f72a97