@skill-map/cli 0.38.0 → 0.39.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 +1217 -1133
- package/dist/cli.js.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/kernel/index.d.ts +13 -13
- package/dist/kernel/index.js +3 -2
- package/dist/kernel/index.js.map +1 -1
- package/dist/ui/{chunk-O5N7UH37.js → chunk-AALYQ3RG.js} +1 -1
- package/dist/ui/{chunk-KKOZFBXQ.js → chunk-JECPBFFX.js} +1 -1
- package/dist/ui/{chunk-QZM2G474.js → chunk-LGLLRAJ6.js} +1 -1
- package/dist/ui/{chunk-NTM2J2WO.js → chunk-NHI5UPNK.js} +1 -1
- package/dist/ui/{chunk-4CDTW64C.js → chunk-PVVKVGJ6.js} +1 -1
- package/dist/ui/{chunk-5AZ5S6JB.js → chunk-SKA7ZFUX.js} +1 -1
- package/dist/ui/{chunk-Z4LANJFK.js → chunk-VDMXHCXR.js} +1 -1
- package/dist/ui/{chunk-AAR3Y55J.js → chunk-VZIYRREA.js} +1 -1
- package/dist/ui/chunk-XRDZZC5F.js +123 -0
- package/dist/ui/{chunk-47MG5XH2.js → chunk-YPO2DTMO.js} +8 -8
- package/dist/ui/{chunk-G5CKBDBB.js → chunk-ZYIKNMFV.js} +1 -1
- package/dist/ui/index.html +1 -1
- package/dist/ui/main-TYWMNAII.js +2 -0
- package/package.json +2 -2
- package/dist/ui/chunk-4XEJUDPL.js +0 -123
- package/dist/ui/main-EO5QNLE4.js +0 -2
package/dist/cli.js
CHANGED
|
@@ -442,7 +442,7 @@ var claudeProvider = {
|
|
|
442
442
|
pluginId: CLAUDE_PLUGIN_ID,
|
|
443
443
|
kind: "provider",
|
|
444
444
|
version: "1.0.0",
|
|
445
|
-
description: "
|
|
445
|
+
description: "Classifies files under `.claude/{agents,commands,skills}` as Claude Code agents, commands, and skills.",
|
|
446
446
|
// Vendor provider: Claude Code only reads its own `.claude/` territory
|
|
447
447
|
// and ignores `.codex/` / Antigravity layouts at runtime. Gating the
|
|
448
448
|
// classifier behind the active lens prevents the walker from inventing
|
|
@@ -727,7 +727,7 @@ var atDirectiveExtractor = {
|
|
|
727
727
|
pluginId: CLAUDE_PLUGIN_ID,
|
|
728
728
|
kind: "extractor",
|
|
729
729
|
version: "1.0.0",
|
|
730
|
-
description: "Detects `@<token>` directives in a node's body using Claude Code
|
|
730
|
+
description: "Detects `@<token>` directives in a node's body using Claude Code rules. A bare handle (e.g. `@team`) becomes a `mentions` link; a file-flavoured token (e.g. `@docs/api.md`, `@./readme.md`) becomes a `references` link.",
|
|
731
731
|
scope: "body",
|
|
732
732
|
precondition: { provider: ["claude"] },
|
|
733
733
|
// eslint-disable-next-line complexity
|
|
@@ -808,15 +808,15 @@ function resolveSourceRelative(sourceDir, bare) {
|
|
|
808
808
|
return pathPosix.normalize(joined);
|
|
809
809
|
}
|
|
810
810
|
|
|
811
|
-
// plugins/claude/extractors/slash/index.ts
|
|
812
|
-
var ID2 = "slash";
|
|
811
|
+
// plugins/claude/extractors/slash-command/index.ts
|
|
812
|
+
var ID2 = "slash-command";
|
|
813
813
|
var SLASH_RE = /(?<![A-Za-z0-9_/.:?#=&])(\/[a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)/gi;
|
|
814
|
-
var
|
|
814
|
+
var slashCommandExtractor = {
|
|
815
815
|
id: ID2,
|
|
816
816
|
pluginId: CLAUDE_PLUGIN_ID,
|
|
817
817
|
kind: "extractor",
|
|
818
818
|
version: "1.0.0",
|
|
819
|
-
description: "
|
|
819
|
+
description: "Turns `/command` invocations in a node's body into arrows that point at the resolved slash command or skill, using Claude Code routing rules.",
|
|
820
820
|
scope: "body",
|
|
821
821
|
precondition: { provider: ["claude"] },
|
|
822
822
|
extract(ctx) {
|
|
@@ -866,7 +866,7 @@ var antigravityProvider = {
|
|
|
866
866
|
pluginId: ANTIGRAVITY_PLUGIN_ID,
|
|
867
867
|
kind: "provider",
|
|
868
868
|
version: "1.0.0",
|
|
869
|
-
description: "
|
|
869
|
+
description: "Declares the Google Antigravity runtime and its reserved built-in names.",
|
|
870
870
|
// Vendor provider: marked gated for the day Antigravity grows its own
|
|
871
871
|
// on-disk kind beyond the open standard. Today `kinds: {}` and
|
|
872
872
|
// `classify` returns `null` for every path, so the flag is inert; the
|
|
@@ -896,7 +896,7 @@ var antigravityProvider = {
|
|
|
896
896
|
// Gemini CLI's. We mirror the full 38-verb Gemini CLI catalog (plus its
|
|
897
897
|
// four documented aliases: `dir`, `?`, `exit`, `bashes`) so a user file
|
|
898
898
|
// that names a skill / command `help`, `clear`, `mcp`, etc. is flagged
|
|
899
|
-
// immediately by `core/reserved
|
|
899
|
+
// immediately by `core/name-reserved` once the lens activates the catalog.
|
|
900
900
|
//
|
|
901
901
|
// The catalog is INACTIVE today: the analyzer keys on `node.provider`
|
|
902
902
|
// and this Provider's `classify()` returns `null` for every path, so
|
|
@@ -1015,7 +1015,7 @@ var openaiProvider = {
|
|
|
1015
1015
|
pluginId: OPENAI_PLUGIN_ID,
|
|
1016
1016
|
kind: "provider",
|
|
1017
1017
|
version: "1.0.0",
|
|
1018
|
-
description: "
|
|
1018
|
+
description: "Classifies files under `.codex/agents/*.toml` as OpenAI Codex CLI sub-agents.",
|
|
1019
1019
|
// Vendor provider: Codex CLI only reads its own `.codex/` territory.
|
|
1020
1020
|
// Gating the classifier behind the active lens keeps the walker from
|
|
1021
1021
|
// claiming Codex agents under a `claude` (or any other) lens, where
|
|
@@ -1073,7 +1073,7 @@ var agentSkillsProvider = {
|
|
|
1073
1073
|
pluginId: AGENT_SKILLS_PLUGIN_ID,
|
|
1074
1074
|
kind: "provider",
|
|
1075
1075
|
version: "1.0.0",
|
|
1076
|
-
description: "
|
|
1076
|
+
description: "Classifies files under `.agents/skills/<name>/SKILL.md` as Agent Skills.",
|
|
1077
1077
|
read: { extensions: [".md"], parser: "frontmatter-yaml" },
|
|
1078
1078
|
kinds: {
|
|
1079
1079
|
skill: {
|
|
@@ -1121,7 +1121,7 @@ var coreMarkdownProvider = {
|
|
|
1121
1121
|
pluginId: CORE_PLUGIN_ID,
|
|
1122
1122
|
kind: "provider",
|
|
1123
1123
|
version: "1.0.0",
|
|
1124
|
-
description: "Universal `.md` fallback. Claims any markdown file no vendor-specific
|
|
1124
|
+
description: "Universal `.md` fallback. Claims any markdown file that no vendor-specific provider has classified.",
|
|
1125
1125
|
read: { extensions: [".md"], parser: "frontmatter-yaml" },
|
|
1126
1126
|
// Per spec § A.6, defaultRefreshAction values MUST be qualified
|
|
1127
1127
|
// action ids. The summarize-markdown action is not yet implemented
|
|
@@ -1172,7 +1172,7 @@ var annotationsExtractor = {
|
|
|
1172
1172
|
pluginId: CORE_PLUGIN_ID,
|
|
1173
1173
|
kind: "extractor",
|
|
1174
1174
|
version: "1.0.0",
|
|
1175
|
-
description: "Turns the `supersedes` and `supersededBy` entries
|
|
1175
|
+
description: "Turns the `supersedes` and `supersededBy` entries from a node's `.sm` sidecar into arrows between nodes in the graph.",
|
|
1176
1176
|
scope: "frontmatter",
|
|
1177
1177
|
extract(ctx) {
|
|
1178
1178
|
const sourcePath = ctx.node.path;
|
|
@@ -1234,7 +1234,7 @@ var externalUrlCounterExtractor = {
|
|
|
1234
1234
|
pluginId: CORE_PLUGIN_ID,
|
|
1235
1235
|
kind: "extractor",
|
|
1236
1236
|
version: "1.0.0",
|
|
1237
|
-
description: "Counts the distinct external URLs in a node's body and shows the
|
|
1237
|
+
description: "Counts the distinct external URLs in a node's body and shows the count on the card.",
|
|
1238
1238
|
scope: "body",
|
|
1239
1239
|
/**
|
|
1240
1240
|
* Phase 6 / View contribution system, surface the distinct-URL
|
|
@@ -1323,7 +1323,7 @@ var markdownLinkExtractor = {
|
|
|
1323
1323
|
pluginId: CORE_PLUGIN_ID,
|
|
1324
1324
|
kind: "extractor",
|
|
1325
1325
|
version: "1.0.0",
|
|
1326
|
-
description: "
|
|
1326
|
+
description: "Turns markdown links (`[text](path)`) in a node's body into arrows between nodes in the graph.",
|
|
1327
1327
|
scope: "body",
|
|
1328
1328
|
extract(ctx) {
|
|
1329
1329
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1352,7 +1352,7 @@ var markdownLinkExtractor = {
|
|
|
1352
1352
|
// explicitly designates an out-link via the brackets +
|
|
1353
1353
|
// parentheses pair; there is no inference left to discount.
|
|
1354
1354
|
// Whether the path resolves to a real node is a separate
|
|
1355
|
-
// concern (the `core/broken
|
|
1355
|
+
// concern (the `core/reference-broken` analyzer flags unresolved
|
|
1356
1356
|
// targets), not a confidence question.
|
|
1357
1357
|
confidence: 1,
|
|
1358
1358
|
rationale: "unambiguous markdown link syntax",
|
|
@@ -1385,7 +1385,7 @@ var mcpToolsExtractor = {
|
|
|
1385
1385
|
pluginId: CORE_PLUGIN_ID,
|
|
1386
1386
|
kind: "extractor",
|
|
1387
1387
|
version: "1.0.0",
|
|
1388
|
-
description: "
|
|
1388
|
+
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.",
|
|
1389
1389
|
scope: "frontmatter",
|
|
1390
1390
|
extract(ctx) {
|
|
1391
1391
|
const raw = ctx.frontmatter["tools"];
|
|
@@ -1439,15 +1439,15 @@ function collectMcpServers(tools) {
|
|
|
1439
1439
|
return out;
|
|
1440
1440
|
}
|
|
1441
1441
|
|
|
1442
|
-
// plugins/core/extractors/tools-
|
|
1443
|
-
var ID7 = "tools-
|
|
1442
|
+
// plugins/core/extractors/tools-counter/index.ts
|
|
1443
|
+
var ID7 = "tools-counter";
|
|
1444
1444
|
var TOOLTIP_MAX = 255;
|
|
1445
|
-
var
|
|
1445
|
+
var toolsCounterExtractor = {
|
|
1446
1446
|
id: ID7,
|
|
1447
1447
|
pluginId: CORE_PLUGIN_ID,
|
|
1448
1448
|
kind: "extractor",
|
|
1449
1449
|
version: "1.0.0",
|
|
1450
|
-
description: "Counts the tools an agent declares in its frontmatter and shows the
|
|
1450
|
+
description: "Counts the tools an agent declares in its frontmatter and shows the count on the agent card.",
|
|
1451
1451
|
scope: "frontmatter",
|
|
1452
1452
|
precondition: { kind: ["claude/agent"] },
|
|
1453
1453
|
ui: {
|
|
@@ -1479,6 +1479,209 @@ function buildTooltip(names) {
|
|
|
1479
1479
|
return `${joined.slice(0, TOOLTIP_MAX - 1)}\u2026`;
|
|
1480
1480
|
}
|
|
1481
1481
|
|
|
1482
|
+
// plugins/core/analyzers/annotation-field-unknown/index.ts
|
|
1483
|
+
import { readFileSync } from "fs";
|
|
1484
|
+
import { dirname, resolve } from "path";
|
|
1485
|
+
import { createRequire } from "module";
|
|
1486
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
1487
|
+
|
|
1488
|
+
// kernel/util/ajv-interop.ts
|
|
1489
|
+
import addFormatsModule from "ajv-formats";
|
|
1490
|
+
var addFormats = addFormatsModule.default ?? addFormatsModule;
|
|
1491
|
+
function applyAjvFormats(ajv) {
|
|
1492
|
+
addFormats(ajv);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// plugins/core/analyzers/annotation-field-unknown/text.ts
|
|
1496
|
+
var ANNOTATION_FIELD_UNKNOWN_TEXTS = {
|
|
1497
|
+
/** Key inside `annotations:` is not in the curated catalog. */
|
|
1498
|
+
unknownAnnotationKey: "{{path}}: sidecar annotations contain unknown key '{{key}}' (not in annotations.schema.json catalog).",
|
|
1499
|
+
/** Top-level key is neither reserved, nor a registered plugin namespace, nor a registered root key. */
|
|
1500
|
+
unknownRootKey: "{{path}}: sidecar declares unknown top-level key '{{key}}'; not a reserved block, not a registered plugin namespace, not a registered root contribution.",
|
|
1501
|
+
/** Value under a registered plugin namespace fails the contributed schema. */
|
|
1502
|
+
pluginNamespaceInvalid: "{{path}}: sidecar block '{{pluginId}}.{{key}}' fails the schema contributed by plugin '{{pluginId}}': {{errors}}.",
|
|
1503
|
+
// Tooltips for the per-node view-contribution badges. Singular vs
|
|
1504
|
+
// plural keeps the count grammar correct without a sub-template.
|
|
1505
|
+
alertTooltipSingle: "This node has 1 unknown field in its sidecar. Open the inspector for details.",
|
|
1506
|
+
alertTooltipMany: "This node has {{count}} unknown fields in its sidecar. Open the inspector for details."
|
|
1507
|
+
};
|
|
1508
|
+
|
|
1509
|
+
// plugins/core/analyzers/annotation-field-unknown/index.ts
|
|
1510
|
+
var ID8 = "annotation-field-unknown";
|
|
1511
|
+
var RESERVED_ROOT_BLOCKS = /* @__PURE__ */ new Set(["identity", "annotations", "settings", "audit"]);
|
|
1512
|
+
var annotationFieldUnknownAnalyzer = {
|
|
1513
|
+
id: ID8,
|
|
1514
|
+
pluginId: CORE_PLUGIN_ID,
|
|
1515
|
+
kind: "analyzer",
|
|
1516
|
+
version: "1.0.0",
|
|
1517
|
+
description: "Flags typos or unrecognized keys in sidecars (`.sm`).",
|
|
1518
|
+
mode: "deterministic",
|
|
1519
|
+
ui: {
|
|
1520
|
+
// Corner badge on the graph card; count omitted when there is a
|
|
1521
|
+
// single unknown field (avoids a noisy "icon + 1" chip).
|
|
1522
|
+
alert: {
|
|
1523
|
+
slot: "graph.node.alert",
|
|
1524
|
+
// Filled warning triangle on the corner, matches the broken-ref
|
|
1525
|
+
// alert's "attention-grabbing solid" pattern; the footer chip
|
|
1526
|
+
// below stays outlined for the quieter counter pairing.
|
|
1527
|
+
icon: "fa-solid fa-triangle-exclamation",
|
|
1528
|
+
emitWhenEmpty: false
|
|
1529
|
+
},
|
|
1530
|
+
// Footer chip on the card, `_counter` shape but rendered icon-only
|
|
1531
|
+
// (the analyzer emits `value: 0` so NodeCounter hides the number
|
|
1532
|
+
// and only the glyph shows). PrimeIcons `pi-question-circle` so the
|
|
1533
|
+
// visual weight matches `annotation-stale`'s `pi-clock` chip
|
|
1534
|
+
// sitting next to it on the same footer row. `emitWhenEmpty: true`
|
|
1535
|
+
// is required: with `value: 0` the slot treats the payload as
|
|
1536
|
+
// empty, so the manifest has to opt in to keep the emission.
|
|
1537
|
+
chip: {
|
|
1538
|
+
slot: "card.footer.right",
|
|
1539
|
+
icon: "pi-question-circle",
|
|
1540
|
+
emitWhenEmpty: true,
|
|
1541
|
+
priority: 30
|
|
1542
|
+
}
|
|
1543
|
+
},
|
|
1544
|
+
// Analyzer body iterates every sidecar root and classifies each
|
|
1545
|
+
// key against three buckets (catalog / plugin namespace / unknown
|
|
1546
|
+
// root). The per-key branching IS the classification table; factoring
|
|
1547
|
+
// it out would rebuild the discriminator elsewhere. Per
|
|
1548
|
+
// `context/lint.md` category 7 (recursive type-discriminator walkers).
|
|
1549
|
+
// eslint-disable-next-line complexity
|
|
1550
|
+
evaluate(ctx) {
|
|
1551
|
+
const sidecarRoots = ctx.sidecarRoots;
|
|
1552
|
+
if (!sidecarRoots || sidecarRoots.size === 0) return [];
|
|
1553
|
+
const knownAnnotationKeys = getKnownAnnotationKeys();
|
|
1554
|
+
const contributions = ctx.annotationContributions ?? [];
|
|
1555
|
+
const namespacedByPlugin = indexNamespacedContributions(contributions);
|
|
1556
|
+
const rootKeys = indexRootContributions(contributions);
|
|
1557
|
+
const knownPluginIds = collectPluginIds(contributions);
|
|
1558
|
+
const issues = [];
|
|
1559
|
+
const perNode = /* @__PURE__ */ new Map();
|
|
1560
|
+
const bump2 = (nodePath) => {
|
|
1561
|
+
perNode.set(nodePath, (perNode.get(nodePath) ?? 0) + 1);
|
|
1562
|
+
};
|
|
1563
|
+
for (const node of ctx.nodes) {
|
|
1564
|
+
const root = sidecarRoots.get(node.path);
|
|
1565
|
+
if (!root) continue;
|
|
1566
|
+
const annotations = root["annotations"];
|
|
1567
|
+
if (annotations !== void 0 && annotations !== null && typeof annotations === "object" && !Array.isArray(annotations)) {
|
|
1568
|
+
for (const key of Object.keys(annotations)) {
|
|
1569
|
+
if (!knownAnnotationKeys.has(key)) {
|
|
1570
|
+
issues.push({
|
|
1571
|
+
analyzerId: ID8,
|
|
1572
|
+
severity: "warn",
|
|
1573
|
+
nodeIds: [node.path],
|
|
1574
|
+
message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.unknownAnnotationKey, {
|
|
1575
|
+
path: node.path,
|
|
1576
|
+
key
|
|
1577
|
+
}),
|
|
1578
|
+
data: { surface: "annotations", key }
|
|
1579
|
+
});
|
|
1580
|
+
bump2(node.path);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
for (const key of Object.keys(root)) {
|
|
1585
|
+
if (RESERVED_ROOT_BLOCKS.has(key)) continue;
|
|
1586
|
+
if (rootKeys.has(key)) continue;
|
|
1587
|
+
if (knownPluginIds.has(key)) {
|
|
1588
|
+
const block = root[key];
|
|
1589
|
+
if (block === null || typeof block !== "object" || Array.isArray(block)) continue;
|
|
1590
|
+
const contribsForPlugin = namespacedByPlugin.get(key);
|
|
1591
|
+
if (!contribsForPlugin) continue;
|
|
1592
|
+
for (const [contribKey, validator] of contribsForPlugin) {
|
|
1593
|
+
const value = block[contribKey];
|
|
1594
|
+
if (value === void 0) continue;
|
|
1595
|
+
if (validator(value)) continue;
|
|
1596
|
+
const errors = (validator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
|
|
1597
|
+
issues.push({
|
|
1598
|
+
analyzerId: ID8,
|
|
1599
|
+
severity: "warn",
|
|
1600
|
+
nodeIds: [node.path],
|
|
1601
|
+
message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.pluginNamespaceInvalid, {
|
|
1602
|
+
path: node.path,
|
|
1603
|
+
pluginId: key,
|
|
1604
|
+
key: contribKey,
|
|
1605
|
+
errors
|
|
1606
|
+
}),
|
|
1607
|
+
data: { surface: "plugin-namespace", pluginId: key, key: contribKey }
|
|
1608
|
+
});
|
|
1609
|
+
bump2(node.path);
|
|
1610
|
+
}
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
issues.push({
|
|
1614
|
+
analyzerId: ID8,
|
|
1615
|
+
severity: "warn",
|
|
1616
|
+
nodeIds: [node.path],
|
|
1617
|
+
message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.unknownRootKey, {
|
|
1618
|
+
path: node.path,
|
|
1619
|
+
key
|
|
1620
|
+
}),
|
|
1621
|
+
data: { surface: "root", key }
|
|
1622
|
+
});
|
|
1623
|
+
bump2(node.path);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
for (const [nodePath, count] of perNode) {
|
|
1627
|
+
const tooltip = count === 1 ? ANNOTATION_FIELD_UNKNOWN_TEXTS.alertTooltipSingle : tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.alertTooltipMany, { count });
|
|
1628
|
+
ctx.emitContribution(nodePath, "alert", {
|
|
1629
|
+
icon: "fa-solid fa-triangle-exclamation",
|
|
1630
|
+
severity: "warn",
|
|
1631
|
+
tooltip
|
|
1632
|
+
});
|
|
1633
|
+
ctx.emitContribution(nodePath, "chip", {
|
|
1634
|
+
value: 0,
|
|
1635
|
+
severity: "warn",
|
|
1636
|
+
tooltip
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
return issues;
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
var cachedKnownKeys = null;
|
|
1643
|
+
function getKnownAnnotationKeys() {
|
|
1644
|
+
if (cachedKnownKeys) return cachedKnownKeys;
|
|
1645
|
+
const require2 = createRequire(import.meta.url);
|
|
1646
|
+
const indexPath = require2.resolve("@skill-map/spec/index.json");
|
|
1647
|
+
const specRoot = dirname(indexPath);
|
|
1648
|
+
const schema = JSON.parse(
|
|
1649
|
+
readFileSync(resolve(specRoot, "schemas/annotations.schema.json"), "utf8")
|
|
1650
|
+
);
|
|
1651
|
+
cachedKnownKeys = new Set(Object.keys(schema.properties ?? {}));
|
|
1652
|
+
return cachedKnownKeys;
|
|
1653
|
+
}
|
|
1654
|
+
function indexNamespacedContributions(contributions) {
|
|
1655
|
+
const out = /* @__PURE__ */ new Map();
|
|
1656
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true, allowUnionTypes: true });
|
|
1657
|
+
applyAjvFormats(ajv);
|
|
1658
|
+
for (const entry of contributions) {
|
|
1659
|
+
if (entry.location !== "namespaced") continue;
|
|
1660
|
+
let bucket = out.get(entry.pluginId);
|
|
1661
|
+
if (!bucket) {
|
|
1662
|
+
bucket = /* @__PURE__ */ new Map();
|
|
1663
|
+
out.set(entry.pluginId, bucket);
|
|
1664
|
+
}
|
|
1665
|
+
try {
|
|
1666
|
+
bucket.set(entry.key, ajv.compile(entry.schema));
|
|
1667
|
+
} catch {
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return out;
|
|
1671
|
+
}
|
|
1672
|
+
function indexRootContributions(contributions) {
|
|
1673
|
+
const out = /* @__PURE__ */ new Set();
|
|
1674
|
+
for (const entry of contributions) {
|
|
1675
|
+
if (entry.location === "root") out.add(entry.key);
|
|
1676
|
+
}
|
|
1677
|
+
return out;
|
|
1678
|
+
}
|
|
1679
|
+
function collectPluginIds(contributions) {
|
|
1680
|
+
const out = /* @__PURE__ */ new Set();
|
|
1681
|
+
for (const entry of contributions) out.add(entry.pluginId);
|
|
1682
|
+
return out;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1482
1685
|
// plugins/core/analyzers/annotation-orphan/text.ts
|
|
1483
1686
|
var ANNOTATION_ORPHAN_TEXTS = {
|
|
1484
1687
|
/** Sidecar `<path>.sm` has no matching `<path>.md`. */
|
|
@@ -1486,13 +1689,13 @@ var ANNOTATION_ORPHAN_TEXTS = {
|
|
|
1486
1689
|
};
|
|
1487
1690
|
|
|
1488
1691
|
// plugins/core/analyzers/annotation-orphan/index.ts
|
|
1489
|
-
var
|
|
1692
|
+
var ID9 = "annotation-orphan";
|
|
1490
1693
|
var annotationOrphanAnalyzer = {
|
|
1491
|
-
id:
|
|
1694
|
+
id: ID9,
|
|
1492
1695
|
pluginId: CORE_PLUGIN_ID,
|
|
1493
1696
|
kind: "analyzer",
|
|
1494
1697
|
version: "1.0.0",
|
|
1495
|
-
description: "
|
|
1698
|
+
description: "Flags sidecars (`.sm`) whose `.md` file no longer exists.",
|
|
1496
1699
|
mode: "deterministic",
|
|
1497
1700
|
evaluate(ctx) {
|
|
1498
1701
|
const orphans = ctx.orphanSidecars;
|
|
@@ -1501,7 +1704,7 @@ var annotationOrphanAnalyzer = {
|
|
|
1501
1704
|
for (const orphan of orphans) {
|
|
1502
1705
|
const expectedMdRelative = orphan.relativePath.endsWith(".sm") ? `${orphan.relativePath.slice(0, -".sm".length)}.md` : `${orphan.relativePath}.md`;
|
|
1503
1706
|
issues.push({
|
|
1504
|
-
analyzerId:
|
|
1707
|
+
analyzerId: ID9,
|
|
1505
1708
|
severity: "warn",
|
|
1506
1709
|
nodeIds: [expectedMdRelative],
|
|
1507
1710
|
message: tx(ANNOTATION_ORPHAN_TEXTS.message, {
|
|
@@ -1538,17 +1741,17 @@ var ANNOTATION_STALE_TEXTS = {
|
|
|
1538
1741
|
};
|
|
1539
1742
|
|
|
1540
1743
|
// plugins/core/analyzers/annotation-stale/index.ts
|
|
1541
|
-
var
|
|
1744
|
+
var ID10 = "annotation-stale";
|
|
1542
1745
|
var annotationStaleAnalyzer = {
|
|
1543
|
-
id:
|
|
1746
|
+
id: ID10,
|
|
1544
1747
|
pluginId: CORE_PLUGIN_ID,
|
|
1545
1748
|
kind: "analyzer",
|
|
1546
1749
|
version: "1.0.0",
|
|
1547
|
-
description: "
|
|
1750
|
+
description: "Marks sidecars (`.sm`) that are out of date with their `.md`.",
|
|
1548
1751
|
mode: "deterministic",
|
|
1549
1752
|
// The natural fix is to bump the node: refreshes `for` hashes,
|
|
1550
1753
|
// increments `annotations.version`, and stamps the audit block. The
|
|
1551
|
-
// UI surfaces `core/bump` in the node inspector under "Recommended
|
|
1754
|
+
// UI surfaces `core/node-bump` in the node inspector under "Recommended
|
|
1552
1755
|
// for issues" whenever this analyzer fires.
|
|
1553
1756
|
ui: {
|
|
1554
1757
|
// A `pi-clock` chip in the footer-right cluster so the operator
|
|
@@ -1575,7 +1778,7 @@ var annotationStaleAnalyzer = {
|
|
|
1575
1778
|
if (status === "fresh") continue;
|
|
1576
1779
|
const message = status === "stale-body" ? tx(ANNOTATION_STALE_TEXTS.bodyDrift, { path: node.path }) : status === "stale-frontmatter" ? tx(ANNOTATION_STALE_TEXTS.frontmatterDrift, { path: node.path }) : tx(ANNOTATION_STALE_TEXTS.bothDrift, { path: node.path });
|
|
1577
1780
|
issues.push({
|
|
1578
|
-
analyzerId:
|
|
1781
|
+
analyzerId: ID10,
|
|
1579
1782
|
severity: "warn",
|
|
1580
1783
|
nodeIds: [node.path],
|
|
1581
1784
|
message,
|
|
@@ -1601,232 +1804,48 @@ function tooltipFor(status) {
|
|
|
1601
1804
|
}
|
|
1602
1805
|
}
|
|
1603
1806
|
|
|
1604
|
-
// plugins/core/analyzers/
|
|
1605
|
-
|
|
1807
|
+
// plugins/core/analyzers/contribution-orphan/index.ts
|
|
1808
|
+
var ID11 = "contribution-orphan";
|
|
1809
|
+
var contributionOrphanAnalyzer = {
|
|
1810
|
+
id: ID11,
|
|
1811
|
+
pluginId: CORE_PLUGIN_ID,
|
|
1812
|
+
kind: "analyzer",
|
|
1813
|
+
version: "0.0.0",
|
|
1814
|
+
description: "Warns about plugin data referencing nodes renamed or deleted in the latest scan.",
|
|
1815
|
+
mode: "deterministic",
|
|
1816
|
+
evaluate(_ctx) {
|
|
1817
|
+
return [];
|
|
1818
|
+
}
|
|
1819
|
+
};
|
|
1606
1820
|
|
|
1607
|
-
// plugins/core/analyzers/
|
|
1608
|
-
var
|
|
1609
|
-
/**
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
alertTooltipMany: "This node has {{count}} broken references. Open the inspector for details.",
|
|
1615
|
-
// Fix-summary copy when the broken trigger has a same-named file on
|
|
1616
|
-
// disk that does not advertise `name:` in its frontmatter. Two
|
|
1617
|
-
// variants for single vs multiple candidates; same template family
|
|
1618
|
-
// as the alert tooltips above.
|
|
1619
|
-
hintSummarySingle: "Add `name: {{name}}` to the frontmatter of {{candidate}} so this reference resolves.",
|
|
1620
|
-
hintSummaryMany: "Add `name: {{name}}` to the frontmatter of one of these files so this reference resolves: {{candidates}}."
|
|
1821
|
+
// plugins/core/analyzers/job-file-orphan/text.ts
|
|
1822
|
+
var JOB_FILE_ORPHAN_TEXTS = {
|
|
1823
|
+
/**
|
|
1824
|
+
* `<path>.md` lives under `.skill-map/jobs/` but no `state_jobs.filePath`
|
|
1825
|
+
* row references it. Run `sm job prune --orphan-files` to remove.
|
|
1826
|
+
*/
|
|
1827
|
+
message: "Orphan job file: {{filePath}} is not referenced by any state_jobs row. Run `sm job prune --orphan-files` to remove it."
|
|
1621
1828
|
};
|
|
1622
1829
|
|
|
1623
|
-
// plugins/core/analyzers/
|
|
1624
|
-
var
|
|
1625
|
-
var
|
|
1626
|
-
id:
|
|
1830
|
+
// plugins/core/analyzers/job-file-orphan/index.ts
|
|
1831
|
+
var ID12 = "job-file-orphan";
|
|
1832
|
+
var jobFileOrphanAnalyzer = {
|
|
1833
|
+
id: ID12,
|
|
1627
1834
|
pluginId: CORE_PLUGIN_ID,
|
|
1628
1835
|
kind: "analyzer",
|
|
1629
1836
|
version: "1.0.0",
|
|
1630
|
-
description: "
|
|
1837
|
+
description: "Flags leftover job result files (no live job references them). Clean up via `sm job prune --orphan-files`.",
|
|
1631
1838
|
mode: "deterministic",
|
|
1632
|
-
ui: {
|
|
1633
|
-
// Corner badge on the graph card; count omitted when there is a
|
|
1634
|
-
// single broken ref (avoids a noisy "icon + 1" chip).
|
|
1635
|
-
alert: {
|
|
1636
|
-
slot: "graph.node.alert",
|
|
1637
|
-
icon: "fa-solid fa-circle-xmark",
|
|
1638
|
-
emitWhenEmpty: false
|
|
1639
|
-
},
|
|
1640
|
-
// Footer chip on the card. `_counter` shape, `value` always shows,
|
|
1641
|
-
// so the operator sees "how many" at a glance. Renders OUTLINED
|
|
1642
|
-
// (`fa-regular`) so the corner alert (filled, attention-grabbing)
|
|
1643
|
-
// and the footer chip (quieter, paired with a number) read as two
|
|
1644
|
-
// beats of the same signal rather than two identical glyphs.
|
|
1645
|
-
chip: {
|
|
1646
|
-
slot: "card.footer.right",
|
|
1647
|
-
icon: "fa-regular fa-circle-xmark",
|
|
1648
|
-
emitWhenEmpty: false,
|
|
1649
|
-
priority: 40
|
|
1650
|
-
}
|
|
1651
|
-
},
|
|
1652
|
-
// The resolver, the reference-paths escape hatch, the per-source
|
|
1653
|
-
// aggregation, and the dual-slot emit (with single/plural tooltip and
|
|
1654
|
-
// optional count) all live in one flow because they share the per-link
|
|
1655
|
-
// loop. Splitting them would re-walk `ctx.links` three times.
|
|
1656
|
-
// eslint-disable-next-line complexity
|
|
1657
1839
|
evaluate(ctx) {
|
|
1658
|
-
const
|
|
1659
|
-
|
|
1660
|
-
const byBasenameWithoutName = indexByBasenameWithoutName(ctx.nodes);
|
|
1661
|
-
const refIndex = ctx.referenceablePaths && ctx.referenceablePaths.size > 0 && ctx.cwd ? { paths: ctx.referenceablePaths, cwd: ctx.cwd } : null;
|
|
1662
|
-
const issues = [];
|
|
1663
|
-
const perNode = /* @__PURE__ */ new Map();
|
|
1664
|
-
for (const link of ctx.links) {
|
|
1665
|
-
if (isResolved(link, byPath3, byNormalizedName)) continue;
|
|
1666
|
-
if (refIndex && resolvesViaReferencePaths(link, refIndex)) continue;
|
|
1667
|
-
const candidates = findHintCandidates(link, byBasenameWithoutName);
|
|
1668
|
-
issues.push(buildIssue(link, candidates));
|
|
1669
|
-
perNode.set(link.source, (perNode.get(link.source) ?? 0) + 1);
|
|
1670
|
-
}
|
|
1671
|
-
for (const [nodePath, count] of perNode) {
|
|
1672
|
-
const tooltip = count === 1 ? BROKEN_REF_TEXTS.alertTooltipSingle : tx(BROKEN_REF_TEXTS.alertTooltipMany, { count });
|
|
1673
|
-
const capped = Math.min(count, 99);
|
|
1674
|
-
ctx.emitContribution(nodePath, "alert", {
|
|
1675
|
-
icon: "fa-solid fa-circle-xmark",
|
|
1676
|
-
severity: "danger",
|
|
1677
|
-
tooltip
|
|
1678
|
-
});
|
|
1679
|
-
ctx.emitContribution(nodePath, "chip", {
|
|
1680
|
-
value: capped,
|
|
1681
|
-
severity: "danger",
|
|
1682
|
-
tooltip
|
|
1683
|
-
});
|
|
1684
|
-
}
|
|
1685
|
-
return issues;
|
|
1686
|
-
}
|
|
1687
|
-
};
|
|
1688
|
-
function buildIssue(link, hintCandidates = []) {
|
|
1689
|
-
const data = {
|
|
1690
|
-
target: link.target,
|
|
1691
|
-
kind: link.kind,
|
|
1692
|
-
trigger: link.trigger?.normalizedTrigger ?? null
|
|
1693
|
-
};
|
|
1694
|
-
const issue = {
|
|
1695
|
-
analyzerId: ID10,
|
|
1696
|
-
severity: "warn",
|
|
1697
|
-
nodeIds: [link.source],
|
|
1698
|
-
message: tx(BROKEN_REF_TEXTS.message, {
|
|
1699
|
-
kind: link.kind,
|
|
1700
|
-
source: link.source,
|
|
1701
|
-
target: link.target
|
|
1702
|
-
}),
|
|
1703
|
-
data
|
|
1704
|
-
};
|
|
1705
|
-
if (hintCandidates.length > 0) {
|
|
1706
|
-
const suggestedName = (link.trigger?.normalizedTrigger ?? "").replace(/^[/@]/, "").trim();
|
|
1707
|
-
const candidatePaths = hintCandidates.map((n) => n.path);
|
|
1708
|
-
data["hint"] = {
|
|
1709
|
-
kind: "missing-frontmatter-name",
|
|
1710
|
-
suggestedName,
|
|
1711
|
-
candidates: candidatePaths
|
|
1712
|
-
};
|
|
1713
|
-
issue.fix = {
|
|
1714
|
-
summary: candidatePaths.length === 1 ? tx(BROKEN_REF_TEXTS.hintSummarySingle, {
|
|
1715
|
-
name: suggestedName,
|
|
1716
|
-
candidate: candidatePaths[0]
|
|
1717
|
-
}) : tx(BROKEN_REF_TEXTS.hintSummaryMany, {
|
|
1718
|
-
name: suggestedName,
|
|
1719
|
-
candidates: candidatePaths.join(", ")
|
|
1720
|
-
}),
|
|
1721
|
-
autofixable: false
|
|
1722
|
-
};
|
|
1723
|
-
}
|
|
1724
|
-
return issue;
|
|
1725
|
-
}
|
|
1726
|
-
function resolvesViaReferencePaths(link, refIndex) {
|
|
1727
|
-
if (!isPathStyleLink(link)) return false;
|
|
1728
|
-
return refIndex.paths.has(resolve(refIndex.cwd, link.target));
|
|
1729
|
-
}
|
|
1730
|
-
function indexByNormalizedName(nodes) {
|
|
1731
|
-
const out = /* @__PURE__ */ new Map();
|
|
1732
|
-
for (const node of nodes) {
|
|
1733
|
-
const raw = node.frontmatter?.["name"];
|
|
1734
|
-
const name = typeof raw === "string" ? raw : "";
|
|
1735
|
-
if (!name) continue;
|
|
1736
|
-
const key = normalizeTrigger(name);
|
|
1737
|
-
const bucket = out.get(key) ?? [];
|
|
1738
|
-
bucket.push(node);
|
|
1739
|
-
out.set(key, bucket);
|
|
1740
|
-
}
|
|
1741
|
-
return out;
|
|
1742
|
-
}
|
|
1743
|
-
function basenameWithoutExt(path) {
|
|
1744
|
-
const base = pathPosix3.basename(path);
|
|
1745
|
-
const ext = pathPosix3.extname(base);
|
|
1746
|
-
return ext ? base.slice(0, -ext.length) : base;
|
|
1747
|
-
}
|
|
1748
|
-
function indexByBasenameWithoutName(nodes) {
|
|
1749
|
-
const out = /* @__PURE__ */ new Map();
|
|
1750
|
-
for (const node of nodes) {
|
|
1751
|
-
const raw = node.frontmatter?.["name"];
|
|
1752
|
-
const name = typeof raw === "string" ? raw : "";
|
|
1753
|
-
if (name) continue;
|
|
1754
|
-
const bare = basenameWithoutExt(node.path);
|
|
1755
|
-
if (!bare) continue;
|
|
1756
|
-
const key = normalizeTrigger(bare);
|
|
1757
|
-
if (!key) continue;
|
|
1758
|
-
const bucket = out.get(key) ?? [];
|
|
1759
|
-
bucket.push(node);
|
|
1760
|
-
out.set(key, bucket);
|
|
1761
|
-
}
|
|
1762
|
-
return out;
|
|
1763
|
-
}
|
|
1764
|
-
function findHintCandidates(link, idx) {
|
|
1765
|
-
const normalized = link.trigger?.normalizedTrigger;
|
|
1766
|
-
if (!normalized) return [];
|
|
1767
|
-
const sigil = normalized.charAt(0);
|
|
1768
|
-
if (sigil !== "/" && sigil !== "@") return [];
|
|
1769
|
-
const withoutSigil = normalized.slice(1).trim();
|
|
1770
|
-
if (!withoutSigil) return [];
|
|
1771
|
-
return idx.get(withoutSigil) ?? [];
|
|
1772
|
-
}
|
|
1773
|
-
function isResolved(link, byPath3, byNormalizedName) {
|
|
1774
|
-
const normalized = link.trigger?.normalizedTrigger;
|
|
1775
|
-
if (normalized) {
|
|
1776
|
-
const withoutSigil = normalized.replace(/^[/@]/, "").trim();
|
|
1777
|
-
if (byNormalizedName.has(withoutSigil)) return true;
|
|
1778
|
-
}
|
|
1779
|
-
if (byPath3.has(link.target)) return true;
|
|
1780
|
-
return false;
|
|
1781
|
-
}
|
|
1782
|
-
function isPathStyleLink(link) {
|
|
1783
|
-
const sigil = link.trigger?.normalizedTrigger?.charAt(0);
|
|
1784
|
-
if (sigil === "/" || sigil === "@") return false;
|
|
1785
|
-
return true;
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
// plugins/core/analyzers/contribution-orphan/index.ts
|
|
1789
|
-
var ID11 = "contribution-orphan";
|
|
1790
|
-
var contributionOrphanAnalyzer = {
|
|
1791
|
-
id: ID11,
|
|
1792
|
-
pluginId: CORE_PLUGIN_ID,
|
|
1793
|
-
kind: "analyzer",
|
|
1794
|
-
version: "0.0.0",
|
|
1795
|
-
description: "Detects and warns about plugin data referencing nodes renamed or deleted in the latest scan.",
|
|
1796
|
-
mode: "deterministic",
|
|
1797
|
-
evaluate(_ctx) {
|
|
1798
|
-
return [];
|
|
1799
|
-
}
|
|
1800
|
-
};
|
|
1801
|
-
|
|
1802
|
-
// plugins/core/analyzers/job-orphan-file/text.ts
|
|
1803
|
-
var JOB_ORPHAN_FILE_TEXTS = {
|
|
1804
|
-
/**
|
|
1805
|
-
* `<path>.md` lives under `.skill-map/jobs/` but no `state_jobs.filePath`
|
|
1806
|
-
* row references it. Run `sm job prune --orphan-files` to remove.
|
|
1807
|
-
*/
|
|
1808
|
-
message: "Orphan job file: {{filePath}} is not referenced by any state_jobs row. Run `sm job prune --orphan-files` to remove it."
|
|
1809
|
-
};
|
|
1810
|
-
|
|
1811
|
-
// plugins/core/analyzers/job-orphan-file/index.ts
|
|
1812
|
-
var ID12 = "job-orphan-file";
|
|
1813
|
-
var jobOrphanFileAnalyzer = {
|
|
1814
|
-
id: ID12,
|
|
1815
|
-
pluginId: CORE_PLUGIN_ID,
|
|
1816
|
-
kind: "analyzer",
|
|
1817
|
-
version: "1.0.0",
|
|
1818
|
-
description: "Detects and flags leftover job result files (no live job references them). Cleanup via `sm job prune --orphan-files`.",
|
|
1819
|
-
mode: "deterministic",
|
|
1820
|
-
evaluate(ctx) {
|
|
1821
|
-
const orphans = ctx.orphanJobFiles;
|
|
1822
|
-
if (!orphans || orphans.length === 0) return [];
|
|
1840
|
+
const orphans = ctx.orphanJobFiles;
|
|
1841
|
+
if (!orphans || orphans.length === 0) return [];
|
|
1823
1842
|
const issues = [];
|
|
1824
1843
|
for (const filePath of orphans) {
|
|
1825
1844
|
issues.push({
|
|
1826
1845
|
analyzerId: ID12,
|
|
1827
1846
|
severity: "warn",
|
|
1828
1847
|
nodeIds: [filePath],
|
|
1829
|
-
message: tx(
|
|
1848
|
+
message: tx(JOB_FILE_ORPHAN_TEXTS.message, { filePath }),
|
|
1830
1849
|
data: { filePath }
|
|
1831
1850
|
});
|
|
1832
1851
|
}
|
|
@@ -1847,7 +1866,7 @@ var linkConflictAnalyzer = {
|
|
|
1847
1866
|
pluginId: "core",
|
|
1848
1867
|
kind: "analyzer",
|
|
1849
1868
|
version: "1.0.0",
|
|
1850
|
-
description:
|
|
1869
|
+
description: "Flags conflicting arrow meanings between extractors (e.g. `references` vs `invokes`).",
|
|
1851
1870
|
mode: "deterministic",
|
|
1852
1871
|
// Bucket links by (source, target), then per-bucket detect distinct
|
|
1853
1872
|
// kinds. The branching is intrinsic to the per-bucket conflict
|
|
@@ -1957,9 +1976,9 @@ function resolveLinkTargetToPath(link, nameIndex) {
|
|
|
1957
1976
|
return resolved ?? raw;
|
|
1958
1977
|
}
|
|
1959
1978
|
|
|
1960
|
-
// plugins/core/analyzers/link-
|
|
1961
|
-
var ID14 = "link-
|
|
1962
|
-
var
|
|
1979
|
+
// plugins/core/analyzers/link-counter/index.ts
|
|
1980
|
+
var ID14 = "link-counter";
|
|
1981
|
+
var linkCounterAnalyzer = {
|
|
1963
1982
|
id: ID14,
|
|
1964
1983
|
pluginId: CORE_PLUGIN_ID,
|
|
1965
1984
|
kind: "analyzer",
|
|
@@ -2024,170 +2043,80 @@ function formatBreakdown(byKind, direction) {
|
|
|
2024
2043
|
return [direction, ...lines].join("\n");
|
|
2025
2044
|
}
|
|
2026
2045
|
|
|
2027
|
-
// plugins/core/analyzers/
|
|
2028
|
-
var
|
|
2046
|
+
// plugins/core/analyzers/link-self-loop/text.ts
|
|
2047
|
+
var LINK_SELF_LOOP_TEXTS = {
|
|
2029
2048
|
/**
|
|
2030
|
-
*
|
|
2031
|
-
*
|
|
2032
|
-
*
|
|
2049
|
+
* Per-edge warn: a node body references itself via the slash /
|
|
2050
|
+
* at-directive / markdown-link surface (most commonly because the
|
|
2051
|
+
* file's heading IS the invocation token, e.g. `# /deploy` inside
|
|
2052
|
+
* `commands/deploy.md`). The link is structurally valid but rarely
|
|
2053
|
+
* the operator's intent; UI consumers MAY hide it by default and
|
|
2054
|
+
* surface a count.
|
|
2033
2055
|
*/
|
|
2034
|
-
message: "{{source}} references
|
|
2035
|
-
/** Inline separator between occurrences in the message. */
|
|
2036
|
-
occurrenceSeparator: ", ",
|
|
2037
|
-
/** Per-occurrence formatting (trigger + line). */
|
|
2038
|
-
occurrence: "`{{trigger}}` ({{kind}}, line {{line}})",
|
|
2039
|
-
/** Per-occurrence formatting when the extractor did not record a line. */
|
|
2040
|
-
occurrenceUnknownLine: "`{{trigger}}` ({{kind}}, unknown line)"
|
|
2056
|
+
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."
|
|
2041
2057
|
};
|
|
2042
2058
|
|
|
2043
|
-
// plugins/core/analyzers/
|
|
2044
|
-
var ID15 = "
|
|
2045
|
-
var
|
|
2059
|
+
// plugins/core/analyzers/link-self-loop/index.ts
|
|
2060
|
+
var ID15 = "link-self-loop";
|
|
2061
|
+
var linkSelfLoopAnalyzer = {
|
|
2046
2062
|
id: ID15,
|
|
2047
2063
|
pluginId: CORE_PLUGIN_ID,
|
|
2048
2064
|
kind: "analyzer",
|
|
2049
2065
|
version: "1.0.0",
|
|
2050
|
-
description: "Flags
|
|
2066
|
+
description: "Flags links whose source is also their own resolved target (e.g. a body heading like `# /deploy` inside the file that defines `/deploy`).",
|
|
2051
2067
|
mode: "deterministic",
|
|
2052
2068
|
evaluate(ctx) {
|
|
2053
2069
|
if (ctx.links.length === 0) return [];
|
|
2054
|
-
const byPath3 = /* @__PURE__ */ new Map();
|
|
2055
|
-
for (const node of ctx.nodes) byPath3.set(node.path, node);
|
|
2056
|
-
const byName = buildNameIndex2(ctx.nodes);
|
|
2057
|
-
const groups = /* @__PURE__ */ new Map();
|
|
2058
|
-
for (const link of ctx.links) {
|
|
2059
|
-
const resolved = resolveTargetPath(link, byPath3, byName);
|
|
2060
|
-
if (!resolved) continue;
|
|
2061
|
-
const key = `${link.source}\0${resolved}`;
|
|
2062
|
-
const bucket = groups.get(key);
|
|
2063
|
-
if (bucket) bucket.push(link);
|
|
2064
|
-
else groups.set(key, [link]);
|
|
2065
|
-
}
|
|
2066
2070
|
const issues = [];
|
|
2067
|
-
for (const
|
|
2068
|
-
|
|
2069
|
-
if (totalOccurrences < 2) continue;
|
|
2070
|
-
const [source, resolvedTarget] = key.split("\0");
|
|
2071
|
-
const flat = flattenOccurrences(links);
|
|
2071
|
+
for (const link of ctx.links) {
|
|
2072
|
+
if (!isSelfLoop(link)) continue;
|
|
2072
2073
|
issues.push({
|
|
2073
2074
|
analyzerId: ID15,
|
|
2074
2075
|
severity: "warn",
|
|
2075
|
-
nodeIds: [source],
|
|
2076
|
-
message: tx(
|
|
2077
|
-
source,
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
occurrences: flat.map(formatOccurrence).join(REDUNDANT_TARGET_REFERENCE_TEXTS.occurrenceSeparator)
|
|
2076
|
+
nodeIds: [link.source],
|
|
2077
|
+
message: tx(LINK_SELF_LOOP_TEXTS.message, {
|
|
2078
|
+
source: link.source,
|
|
2079
|
+
trigger: link.trigger?.originalTrigger ?? link.target,
|
|
2080
|
+
kind: link.kind
|
|
2081
2081
|
}),
|
|
2082
2082
|
data: {
|
|
2083
|
-
target:
|
|
2084
|
-
resolvedTarget,
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
}))
|
|
2083
|
+
target: link.target,
|
|
2084
|
+
resolvedTarget: link.resolvedTarget ?? link.target,
|
|
2085
|
+
kind: link.kind,
|
|
2086
|
+
// Mark explicitly so UI / downstream consumers can read this
|
|
2087
|
+
// single field instead of re-computing the `source === target`
|
|
2088
|
+
// predicate themselves.
|
|
2089
|
+
selfLoop: true
|
|
2091
2090
|
}
|
|
2092
2091
|
});
|
|
2093
2092
|
}
|
|
2094
2093
|
return issues;
|
|
2095
2094
|
}
|
|
2096
2095
|
};
|
|
2097
|
-
function
|
|
2096
|
+
function isSelfLoop(link) {
|
|
2097
|
+
if (link.source === link.target) return true;
|
|
2098
|
+
if (link.resolvedTarget && link.source === link.resolvedTarget) return true;
|
|
2099
|
+
return false;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// kernel/orchestrator/node-identifiers.ts
|
|
2103
|
+
import { posix as pathPosix3 } from "path";
|
|
2104
|
+
function deriveNodeIdentifiers(node, kindDescriptor) {
|
|
2105
|
+
const sources = kindDescriptor?.identifiers;
|
|
2106
|
+
if (!sources || sources.length === 0) return [];
|
|
2098
2107
|
const out = [];
|
|
2099
|
-
for (const
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
originalTrigger: occ.originalTrigger,
|
|
2105
|
-
extractor: occ.extractor,
|
|
2106
|
-
line: occ.location?.line ?? null
|
|
2107
|
-
});
|
|
2108
|
-
}
|
|
2109
|
-
continue;
|
|
2110
|
-
}
|
|
2111
|
-
const trigger = link.trigger?.originalTrigger ?? link.target;
|
|
2112
|
-
out.push({
|
|
2113
|
-
kind: link.kind,
|
|
2114
|
-
originalTrigger: trigger,
|
|
2115
|
-
extractor: link.sources[0] ?? "unknown",
|
|
2116
|
-
line: link.location?.line ?? null
|
|
2117
|
-
});
|
|
2108
|
+
for (const source of sources) {
|
|
2109
|
+
const raw = readIdentifier(source, node);
|
|
2110
|
+
if (!raw) continue;
|
|
2111
|
+
const normalised = normalizeTrigger(raw);
|
|
2112
|
+
if (normalised) out.push(normalised);
|
|
2118
2113
|
}
|
|
2119
|
-
out.sort((a, b) => {
|
|
2120
|
-
const la = a.line ?? Number.MAX_SAFE_INTEGER;
|
|
2121
|
-
const lb = b.line ?? Number.MAX_SAFE_INTEGER;
|
|
2122
|
-
if (la !== lb) return la - lb;
|
|
2123
|
-
return a.originalTrigger.localeCompare(b.originalTrigger);
|
|
2124
|
-
});
|
|
2125
2114
|
return out;
|
|
2126
2115
|
}
|
|
2127
|
-
function
|
|
2128
|
-
if (
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
return tx(REDUNDANT_TARGET_REFERENCE_TEXTS.occurrence, { trigger: occ.originalTrigger, kind: occ.kind, line: occ.line });
|
|
2132
|
-
}
|
|
2133
|
-
function buildNameIndex2(nodes) {
|
|
2134
|
-
const out = /* @__PURE__ */ new Map();
|
|
2135
|
-
for (const node of nodes) {
|
|
2136
|
-
for (const candidate of collectIdentifiers(node)) {
|
|
2137
|
-
const normalised = normalizeTrigger(candidate);
|
|
2138
|
-
if (!normalised) continue;
|
|
2139
|
-
const bucket = out.get(normalised);
|
|
2140
|
-
if (bucket) bucket.push(node.path);
|
|
2141
|
-
else out.set(normalised, [node.path]);
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2144
|
-
return out;
|
|
2145
|
-
}
|
|
2146
|
-
function collectIdentifiers(node) {
|
|
2147
|
-
const out = [];
|
|
2148
|
-
const fmName = node.frontmatter?.["name"];
|
|
2149
|
-
if (typeof fmName === "string" && fmName.length > 0) out.push(fmName);
|
|
2150
|
-
const segs = node.path.split("/");
|
|
2151
|
-
const last = segs[segs.length - 1] ?? "";
|
|
2152
|
-
if (last) {
|
|
2153
|
-
const stem = last.replace(/\.[^.]+$/, "");
|
|
2154
|
-
if (stem) out.push(stem);
|
|
2155
|
-
}
|
|
2156
|
-
if (segs.length >= 2) {
|
|
2157
|
-
const dirBase = segs[segs.length - 2];
|
|
2158
|
-
if (dirBase) out.push(dirBase);
|
|
2159
|
-
}
|
|
2160
|
-
return out;
|
|
2161
|
-
}
|
|
2162
|
-
function resolveTargetPath(link, byPath3, byName) {
|
|
2163
|
-
if (byPath3.has(link.target)) return link.target;
|
|
2164
|
-
const trigger = link.trigger?.normalizedTrigger;
|
|
2165
|
-
if (!trigger) return null;
|
|
2166
|
-
const stripped = trigger.replace(/^[/@]/, "").trim();
|
|
2167
|
-
if (!stripped) return null;
|
|
2168
|
-
const candidates = byName.get(stripped);
|
|
2169
|
-
if (!candidates || candidates.length === 0) return null;
|
|
2170
|
-
return candidates[0] ?? null;
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
// kernel/orchestrator/node-identifiers.ts
|
|
2174
|
-
import { posix as pathPosix4 } from "path";
|
|
2175
|
-
function deriveNodeIdentifiers(node, kindDescriptor) {
|
|
2176
|
-
const sources = kindDescriptor?.identifiers;
|
|
2177
|
-
if (!sources || sources.length === 0) return [];
|
|
2178
|
-
const out = [];
|
|
2179
|
-
for (const source of sources) {
|
|
2180
|
-
const raw = readIdentifier(source, node);
|
|
2181
|
-
if (!raw) continue;
|
|
2182
|
-
const normalised = normalizeTrigger(raw);
|
|
2183
|
-
if (normalised) out.push(normalised);
|
|
2184
|
-
}
|
|
2185
|
-
return out;
|
|
2186
|
-
}
|
|
2187
|
-
function readIdentifier(source, node) {
|
|
2188
|
-
if (source === "frontmatter.name") return readFrontmatterName(node);
|
|
2189
|
-
if (source === "filename-basename") return readFilenameBasename(node);
|
|
2190
|
-
return readDirname(node);
|
|
2116
|
+
function readIdentifier(source, node) {
|
|
2117
|
+
if (source === "frontmatter.name") return readFrontmatterName(node);
|
|
2118
|
+
if (source === "filename-basename") return readFilenameBasename(node);
|
|
2119
|
+
return readDirname(node);
|
|
2191
2120
|
}
|
|
2192
2121
|
function readFrontmatterName(node) {
|
|
2193
2122
|
const raw = node.frontmatter?.["name"];
|
|
@@ -2195,16 +2124,16 @@ function readFrontmatterName(node) {
|
|
|
2195
2124
|
return raw.length > 0 ? raw : null;
|
|
2196
2125
|
}
|
|
2197
2126
|
function readFilenameBasename(node) {
|
|
2198
|
-
const base =
|
|
2127
|
+
const base = pathPosix3.basename(node.path);
|
|
2199
2128
|
if (!base) return null;
|
|
2200
|
-
const ext =
|
|
2129
|
+
const ext = pathPosix3.extname(base);
|
|
2201
2130
|
const stem = ext ? base.slice(0, -ext.length) : base;
|
|
2202
2131
|
return stem.length > 0 ? stem : null;
|
|
2203
2132
|
}
|
|
2204
2133
|
function readDirname(node) {
|
|
2205
|
-
const dir =
|
|
2134
|
+
const dir = pathPosix3.dirname(node.path);
|
|
2206
2135
|
if (!dir || dir === "." || dir === "/") return null;
|
|
2207
|
-
const base =
|
|
2136
|
+
const base = pathPosix3.basename(dir);
|
|
2208
2137
|
return base.length > 0 ? base : null;
|
|
2209
2138
|
}
|
|
2210
2139
|
|
|
@@ -2273,8 +2202,8 @@ function kindKey(node) {
|
|
|
2273
2202
|
return `${node.provider}/${node.kind}`;
|
|
2274
2203
|
}
|
|
2275
2204
|
|
|
2276
|
-
// plugins/core/analyzers/reserved
|
|
2277
|
-
var
|
|
2205
|
+
// plugins/core/analyzers/name-reserved/text.ts
|
|
2206
|
+
var NAME_RESERVED_TEXTS = {
|
|
2278
2207
|
/**
|
|
2279
2208
|
* Target-side message: emitted on the user file that collides with
|
|
2280
2209
|
* a runtime built-in. Same wording skill-map shipped before the
|
|
@@ -2291,14 +2220,14 @@ var RESERVED_NAME_TEXTS = {
|
|
|
2291
2220
|
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."
|
|
2292
2221
|
};
|
|
2293
2222
|
|
|
2294
|
-
// plugins/core/analyzers/reserved
|
|
2295
|
-
var ID16 = "reserved
|
|
2296
|
-
var
|
|
2223
|
+
// plugins/core/analyzers/name-reserved/index.ts
|
|
2224
|
+
var ID16 = "name-reserved";
|
|
2225
|
+
var nameReservedAnalyzer = {
|
|
2297
2226
|
id: ID16,
|
|
2298
2227
|
pluginId: CORE_PLUGIN_ID,
|
|
2299
2228
|
kind: "analyzer",
|
|
2300
2229
|
version: "1.0.0",
|
|
2301
|
-
description: "Flags reserved-name
|
|
2230
|
+
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.",
|
|
2302
2231
|
mode: "deterministic",
|
|
2303
2232
|
// eslint-disable-next-line complexity
|
|
2304
2233
|
evaluate(ctx) {
|
|
@@ -2314,7 +2243,7 @@ var reservedNameAnalyzer = {
|
|
|
2314
2243
|
analyzerId: ID16,
|
|
2315
2244
|
severity: "warn",
|
|
2316
2245
|
nodeIds: [node.path],
|
|
2317
|
-
message: tx(
|
|
2246
|
+
message: tx(NAME_RESERVED_TEXTS.message, {
|
|
2318
2247
|
path: node.path,
|
|
2319
2248
|
provider: node.provider,
|
|
2320
2249
|
kind: node.kind
|
|
@@ -2330,7 +2259,7 @@ var reservedNameAnalyzer = {
|
|
|
2330
2259
|
analyzerId: ID16,
|
|
2331
2260
|
severity: "warn",
|
|
2332
2261
|
nodeIds: [link.source],
|
|
2333
|
-
message: tx(
|
|
2262
|
+
message: tx(NAME_RESERVED_TEXTS.linkMessage, {
|
|
2334
2263
|
kind: link.kind,
|
|
2335
2264
|
target: link.target,
|
|
2336
2265
|
provider: reservedNode.provider,
|
|
@@ -2387,206 +2316,16 @@ function normaliseId(raw) {
|
|
|
2387
2316
|
return raw.normalize("NFD").replace(new RegExp("\\p{Mn}+", "gu"), "").toLowerCase().replace(/[-_\s]+/g, " ").replace(/ +/g, " ").trim();
|
|
2388
2317
|
}
|
|
2389
2318
|
|
|
2390
|
-
// plugins/core/analyzers/
|
|
2391
|
-
var
|
|
2392
|
-
/**
|
|
2393
|
-
* Per-edge warn: a node body references itself via the slash /
|
|
2394
|
-
* at-directive / markdown-link surface (most commonly because the
|
|
2395
|
-
* file's heading IS the invocation token, e.g. `# /deploy` inside
|
|
2396
|
-
* `commands/deploy.md`). The link is structurally valid but rarely
|
|
2397
|
-
* the operator's intent; UI consumers MAY hide it by default and
|
|
2398
|
-
* surface a count.
|
|
2399
|
-
*/
|
|
2400
|
-
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."
|
|
2401
|
-
};
|
|
2402
|
-
|
|
2403
|
-
// plugins/core/analyzers/self-loop/index.ts
|
|
2404
|
-
var ID17 = "self-loop";
|
|
2405
|
-
var selfLoopAnalyzer = {
|
|
2406
|
-
id: ID17,
|
|
2407
|
-
pluginId: CORE_PLUGIN_ID,
|
|
2408
|
-
kind: "analyzer",
|
|
2409
|
-
version: "1.0.0",
|
|
2410
|
-
description: "Flags links whose source is its own resolved target (a body heading like `# /deploy` inside the file that defines `/deploy`). One warn per self-looping link, attached to the source. UI consumers hide self-loops by default; this analyzer is the authoritative detector.",
|
|
2411
|
-
mode: "deterministic",
|
|
2412
|
-
evaluate(ctx) {
|
|
2413
|
-
if (ctx.links.length === 0) return [];
|
|
2414
|
-
const issues = [];
|
|
2415
|
-
for (const link of ctx.links) {
|
|
2416
|
-
if (!isSelfLoop(link)) continue;
|
|
2417
|
-
issues.push({
|
|
2418
|
-
analyzerId: ID17,
|
|
2419
|
-
severity: "warn",
|
|
2420
|
-
nodeIds: [link.source],
|
|
2421
|
-
message: tx(SELF_LOOP_TEXTS.message, {
|
|
2422
|
-
source: link.source,
|
|
2423
|
-
trigger: link.trigger?.originalTrigger ?? link.target,
|
|
2424
|
-
kind: link.kind
|
|
2425
|
-
}),
|
|
2426
|
-
data: {
|
|
2427
|
-
target: link.target,
|
|
2428
|
-
resolvedTarget: link.resolvedTarget ?? link.target,
|
|
2429
|
-
kind: link.kind,
|
|
2430
|
-
// Mark explicitly so UI / downstream consumers can read this
|
|
2431
|
-
// single field instead of re-computing the `source === target`
|
|
2432
|
-
// predicate themselves.
|
|
2433
|
-
selfLoop: true
|
|
2434
|
-
}
|
|
2435
|
-
});
|
|
2436
|
-
}
|
|
2437
|
-
return issues;
|
|
2438
|
-
}
|
|
2439
|
-
};
|
|
2440
|
-
function isSelfLoop(link) {
|
|
2441
|
-
if (link.source === link.target) return true;
|
|
2442
|
-
if (link.resolvedTarget && link.source === link.resolvedTarget) return true;
|
|
2443
|
-
return false;
|
|
2444
|
-
}
|
|
2445
|
-
|
|
2446
|
-
// plugins/core/analyzers/signal-collision/text.ts
|
|
2447
|
-
var SIGNAL_COLLISION_TEXTS = {
|
|
2448
|
-
/**
|
|
2449
|
-
* Per-Signal warn issue: two extractors detected something at
|
|
2450
|
-
* overlapping byte ranges within the same node and the resolver
|
|
2451
|
-
* dropped the loser. Surfaces WHO lost, WHO won, and the tiebreak
|
|
2452
|
-
* reason so the operator can understand why a candidate edge did NOT
|
|
2453
|
-
* become a Link.
|
|
2454
|
-
*
|
|
2455
|
-
* Placeholders are deliberately verbose because this is one of the
|
|
2456
|
-
* few diagnostic surfaces where the operator may need to disambiguate
|
|
2457
|
-
* a confusing graph (e.g. a `[link](path)` followed by `@path` inside
|
|
2458
|
-
* the same paragraph, the markdown-link wins and the at-directive
|
|
2459
|
-
* silently disappears without this warning).
|
|
2460
|
-
*/
|
|
2461
|
-
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.",
|
|
2462
|
-
/**
|
|
2463
|
-
* Same warn but for the rare case the resolver rejected a Signal
|
|
2464
|
-
* because the operator disabled its extractor via
|
|
2465
|
-
* `plugins.<id>.extensions.<extId>.enabled`. Phase 4+ stub: today the
|
|
2466
|
-
* filter is not wired so this template is unreachable from the
|
|
2467
|
-
* resolver; documented now so the analyzer stays forward-compatible
|
|
2468
|
-
* with the upcoming filter pass.
|
|
2469
|
-
*/
|
|
2470
|
-
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.",
|
|
2471
|
-
/**
|
|
2472
|
-
* Same warn but for the future confidence floor case. Phase 4+ stub:
|
|
2473
|
-
* today the resolver materialises every winning candidate regardless
|
|
2474
|
-
* of confidence, so this template is unreachable; documented for
|
|
2475
|
-
* forward compatibility.
|
|
2476
|
-
*/
|
|
2477
|
-
messageBelowFloor: "Detection `{{loserRaw}}` (offset {{loserRange}}, confidence {{confidence}}) fell below the configured threshold {{threshold}} and was dropped."
|
|
2478
|
-
};
|
|
2479
|
-
|
|
2480
|
-
// plugins/core/analyzers/signal-collision/index.ts
|
|
2481
|
-
var ID18 = "signal-collision";
|
|
2482
|
-
var signalCollisionAnalyzer = {
|
|
2483
|
-
id: ID18,
|
|
2484
|
-
pluginId: CORE_PLUGIN_ID,
|
|
2485
|
-
kind: "analyzer",
|
|
2486
|
-
version: "1.0.0",
|
|
2487
|
-
description: "Surfaces Signal IR resolver rejections (range-overlap losers, disabled extractors, below-floor candidates) as warn issues attached to the Signal's source node.",
|
|
2488
|
-
mode: "deterministic",
|
|
2489
|
-
evaluate(ctx) {
|
|
2490
|
-
const signals = ctx.signals;
|
|
2491
|
-
if (!signals || signals.length === 0) return [];
|
|
2492
|
-
const issues = [];
|
|
2493
|
-
for (const signal of signals) {
|
|
2494
|
-
const issue = makeIssue(signal);
|
|
2495
|
-
if (issue) issues.push(issue);
|
|
2496
|
-
}
|
|
2497
|
-
return issues;
|
|
2498
|
-
}
|
|
2499
|
-
};
|
|
2500
|
-
function makeIssue(signal) {
|
|
2501
|
-
const resolution = signal.resolution;
|
|
2502
|
-
if (!resolution || resolution.outcome !== "rejected") return null;
|
|
2503
|
-
if (resolution.rejectedBy) {
|
|
2504
|
-
const winner = resolution.rejectedBy;
|
|
2505
|
-
const winnerCandidate = signal.candidates[resolution.winnerIndex ?? 0];
|
|
2506
|
-
const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
|
|
2507
|
-
const winnerRange = `${winner.range.start}-${winner.range.end}`;
|
|
2508
|
-
return {
|
|
2509
|
-
analyzerId: ID18,
|
|
2510
|
-
severity: "warn",
|
|
2511
|
-
nodeIds: [signal.source],
|
|
2512
|
-
message: tx(SIGNAL_COLLISION_TEXTS.message, {
|
|
2513
|
-
loserExtractor: winnerCandidate.extractorId,
|
|
2514
|
-
loserRaw: signal.raw,
|
|
2515
|
-
loserRange,
|
|
2516
|
-
winnerExtractor: winner.extractorId,
|
|
2517
|
-
winnerRange,
|
|
2518
|
-
reason: winner.reason
|
|
2519
|
-
}),
|
|
2520
|
-
data: {
|
|
2521
|
-
loser: {
|
|
2522
|
-
extractorId: winnerCandidate.extractorId,
|
|
2523
|
-
raw: signal.raw,
|
|
2524
|
-
range: signal.range ?? null,
|
|
2525
|
-
candidate: {
|
|
2526
|
-
kind: winnerCandidate.kind,
|
|
2527
|
-
target: winnerCandidate.target,
|
|
2528
|
-
confidence: winnerCandidate.confidence
|
|
2529
|
-
}
|
|
2530
|
-
},
|
|
2531
|
-
winner: {
|
|
2532
|
-
extractorId: winner.extractorId,
|
|
2533
|
-
range: winner.range
|
|
2534
|
-
},
|
|
2535
|
-
reason: winner.reason
|
|
2536
|
-
}
|
|
2537
|
-
};
|
|
2538
|
-
}
|
|
2539
|
-
if (resolution.extractorDisabled) {
|
|
2540
|
-
const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
|
|
2541
|
-
return {
|
|
2542
|
-
analyzerId: ID18,
|
|
2543
|
-
severity: "warn",
|
|
2544
|
-
nodeIds: [signal.source],
|
|
2545
|
-
message: tx(SIGNAL_COLLISION_TEXTS.messageExtractorDisabled, {
|
|
2546
|
-
extractorId: resolution.extractorDisabled.extractorId,
|
|
2547
|
-
loserRaw: signal.raw,
|
|
2548
|
-
loserRange
|
|
2549
|
-
}),
|
|
2550
|
-
data: {
|
|
2551
|
-
extractorDisabled: resolution.extractorDisabled,
|
|
2552
|
-
raw: signal.raw,
|
|
2553
|
-
range: signal.range ?? null
|
|
2554
|
-
}
|
|
2555
|
-
};
|
|
2556
|
-
}
|
|
2557
|
-
if (resolution.belowFloor) {
|
|
2558
|
-
const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
|
|
2559
|
-
const topCandidate = signal.candidates[0];
|
|
2560
|
-
return {
|
|
2561
|
-
analyzerId: ID18,
|
|
2562
|
-
severity: "warn",
|
|
2563
|
-
nodeIds: [signal.source],
|
|
2564
|
-
message: tx(SIGNAL_COLLISION_TEXTS.messageBelowFloor, {
|
|
2565
|
-
loserRaw: signal.raw,
|
|
2566
|
-
loserRange,
|
|
2567
|
-
confidence: topCandidate.confidence,
|
|
2568
|
-
threshold: resolution.belowFloor.threshold
|
|
2569
|
-
}),
|
|
2570
|
-
data: {
|
|
2571
|
-
belowFloor: resolution.belowFloor,
|
|
2572
|
-
raw: signal.raw,
|
|
2573
|
-
range: signal.range ?? null
|
|
2574
|
-
}
|
|
2575
|
-
};
|
|
2576
|
-
}
|
|
2577
|
-
return null;
|
|
2578
|
-
}
|
|
2579
|
-
|
|
2580
|
-
// plugins/core/analyzers/stability/index.ts
|
|
2581
|
-
var ID19 = "stability";
|
|
2319
|
+
// plugins/core/analyzers/node-stability/index.ts
|
|
2320
|
+
var ID17 = "node-stability";
|
|
2582
2321
|
var EXPERIMENTAL_TOOLTIP = "Experimental: API may change";
|
|
2583
2322
|
var DEPRECATED_TOOLTIP = "Deprecated: avoid in new code";
|
|
2584
|
-
var
|
|
2585
|
-
id:
|
|
2323
|
+
var nodeStabilityAnalyzer = {
|
|
2324
|
+
id: ID17,
|
|
2586
2325
|
pluginId: CORE_PLUGIN_ID,
|
|
2587
2326
|
kind: "analyzer",
|
|
2588
2327
|
version: "1.0.0",
|
|
2589
|
-
description: "Reports node
|
|
2328
|
+
description: "Reports a node's stability stage (`experimental`, `deprecated`) on the card.",
|
|
2590
2329
|
mode: "deterministic",
|
|
2591
2330
|
ui: {
|
|
2592
2331
|
experimental: {
|
|
@@ -2614,7 +2353,7 @@ var stabilityAnalyzer = {
|
|
|
2614
2353
|
tooltip: EXPERIMENTAL_TOOLTIP
|
|
2615
2354
|
});
|
|
2616
2355
|
issues.push({
|
|
2617
|
-
analyzerId:
|
|
2356
|
+
analyzerId: ID17,
|
|
2618
2357
|
severity: "info",
|
|
2619
2358
|
nodeIds: [node.path],
|
|
2620
2359
|
message: `Node '${node.path}' is marked experimental: API may change.`,
|
|
@@ -2627,7 +2366,7 @@ var stabilityAnalyzer = {
|
|
|
2627
2366
|
severity: "warn"
|
|
2628
2367
|
});
|
|
2629
2368
|
issues.push({
|
|
2630
|
-
analyzerId:
|
|
2369
|
+
analyzerId: ID17,
|
|
2631
2370
|
severity: "warn",
|
|
2632
2371
|
nodeIds: [node.path],
|
|
2633
2372
|
message: `Node '${node.path}' is marked deprecated: avoid in new code.`,
|
|
@@ -2654,20 +2393,20 @@ function isStability(value) {
|
|
|
2654
2393
|
return value === "experimental" || value === "deprecated" || value === "stable";
|
|
2655
2394
|
}
|
|
2656
2395
|
|
|
2657
|
-
// plugins/core/analyzers/superseded/text.ts
|
|
2658
|
-
var
|
|
2396
|
+
// plugins/core/analyzers/node-superseded/text.ts
|
|
2397
|
+
var NODE_SUPERSEDED_TEXTS = {
|
|
2659
2398
|
/** `<path> is superseded by <supersededBy>` */
|
|
2660
2399
|
message: "{{path}} is superseded by {{supersededBy}}"
|
|
2661
2400
|
};
|
|
2662
2401
|
|
|
2663
|
-
// plugins/core/analyzers/superseded/index.ts
|
|
2664
|
-
var
|
|
2665
|
-
var
|
|
2666
|
-
id:
|
|
2402
|
+
// plugins/core/analyzers/node-superseded/index.ts
|
|
2403
|
+
var ID18 = "node-superseded";
|
|
2404
|
+
var nodeSupersededAnalyzer = {
|
|
2405
|
+
id: ID18,
|
|
2667
2406
|
pluginId: CORE_PLUGIN_ID,
|
|
2668
2407
|
kind: "analyzer",
|
|
2669
2408
|
version: "1.0.0",
|
|
2670
|
-
description: "
|
|
2409
|
+
description: "Marks nodes replaced by a newer one via `supersededBy`.",
|
|
2671
2410
|
mode: "deterministic",
|
|
2672
2411
|
evaluate(ctx) {
|
|
2673
2412
|
const issues = [];
|
|
@@ -2675,10 +2414,10 @@ var supersededAnalyzer = {
|
|
|
2675
2414
|
const supersededBy = pickSupersededBy(node);
|
|
2676
2415
|
if (supersededBy === null) continue;
|
|
2677
2416
|
issues.push({
|
|
2678
|
-
analyzerId:
|
|
2417
|
+
analyzerId: ID18,
|
|
2679
2418
|
severity: "info",
|
|
2680
2419
|
nodeIds: [node.path],
|
|
2681
|
-
message: tx(
|
|
2420
|
+
message: tx(NODE_SUPERSEDED_TEXTS.message, {
|
|
2682
2421
|
path: node.path,
|
|
2683
2422
|
supersededBy
|
|
2684
2423
|
}),
|
|
@@ -2698,353 +2437,336 @@ function pickSupersededBy(node) {
|
|
|
2698
2437
|
return value;
|
|
2699
2438
|
}
|
|
2700
2439
|
|
|
2701
|
-
// plugins/core/analyzers/
|
|
2702
|
-
|
|
2703
|
-
/**
|
|
2704
|
-
* Top-level message when `analyzeTriggerBucket` accumulated exactly one
|
|
2705
|
-
* cause part. Used for the advertiser-ambiguous-only, invocation-
|
|
2706
|
-
* ambiguous-only, and cross-kind-only branches.
|
|
2707
|
-
*/
|
|
2708
|
-
messageOnePart: 'Trigger "{{normalized}}" has {{part}}.',
|
|
2709
|
-
/**
|
|
2710
|
-
* Top-level message when `analyzeTriggerBucket` accumulated two cause
|
|
2711
|
-
* parts (advertiser-ambiguous AND invocation-ambiguous fire together).
|
|
2712
|
-
* The joiner lives inside the template so future locales can adapt it
|
|
2713
|
-
* (e.g. `'; y '` in Spanish) without touching the rule code.
|
|
2714
|
-
*/
|
|
2715
|
-
messageTwoParts: 'Trigger "{{normalized}}" has {{first}}; and {{second}}.',
|
|
2716
|
-
/** `<n> nodes advertise it: <list>` part, fires on the advertiser-ambiguous branch. */
|
|
2717
|
-
partAdvertisers: "{{count}} nodes advertise it: {{paths}}",
|
|
2718
|
-
/** `<n> distinct invocation forms: <list>` part, fires on the invocation-ambiguous branch. */
|
|
2719
|
-
partInvocations: "{{count}} distinct invocation forms: {{forms}}",
|
|
2720
|
-
/** Singular cross-kind cause: `non-canonical invocation <form> against advertiser <path>`. */
|
|
2721
|
-
partNonCanonicalSingular: "non-canonical invocation {{forms}} against advertiser {{advertiser}}",
|
|
2722
|
-
/** Plural cross-kind cause: `non-canonical invocations <forms> against advertiser <path>`. */
|
|
2723
|
-
partNonCanonicalPlural: "non-canonical invocations {{forms}} against advertiser {{advertiser}}"
|
|
2724
|
-
};
|
|
2725
|
-
|
|
2726
|
-
// plugins/core/analyzers/trigger-collision/index.ts
|
|
2727
|
-
var ID21 = "trigger-collision";
|
|
2728
|
-
var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
|
|
2729
|
-
"command",
|
|
2730
|
-
"skill",
|
|
2731
|
-
"agent"
|
|
2732
|
-
]);
|
|
2733
|
-
var triggerCollisionAnalyzer = {
|
|
2734
|
-
id: ID21,
|
|
2735
|
-
pluginId: CORE_PLUGIN_ID,
|
|
2736
|
-
kind: "analyzer",
|
|
2737
|
-
mode: "deterministic",
|
|
2738
|
-
version: "1.0.0",
|
|
2739
|
-
description: "Detects and flags two or more nodes claiming the same `/command` or `@agent` name.",
|
|
2740
|
-
// Two claim-collection passes (advertisement + invocation) feeding
|
|
2741
|
-
// the bucket map. Per-bucket analysis lives in `analyzeTriggerBucket`.
|
|
2742
|
-
// eslint-disable-next-line complexity
|
|
2743
|
-
evaluate(ctx) {
|
|
2744
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
2745
|
-
const push = (key, claim) => {
|
|
2746
|
-
const bucket = buckets.get(key) ?? [];
|
|
2747
|
-
bucket.push(claim);
|
|
2748
|
-
buckets.set(key, bucket);
|
|
2749
|
-
};
|
|
2750
|
-
for (const node of ctx.nodes) {
|
|
2751
|
-
if (!ADVERTISING_KINDS.has(node.kind)) continue;
|
|
2752
|
-
const raw = node.frontmatter?.["name"];
|
|
2753
|
-
if (typeof raw !== "string" || raw.length === 0) continue;
|
|
2754
|
-
const normalized = `/${normalizeTrigger(raw)}`;
|
|
2755
|
-
if (normalized === "/") continue;
|
|
2756
|
-
push(normalized, {
|
|
2757
|
-
kind: "advertiser",
|
|
2758
|
-
token: node.path,
|
|
2759
|
-
nodeId: node.path,
|
|
2760
|
-
canonicalForm: `/${raw}`
|
|
2761
|
-
});
|
|
2762
|
-
}
|
|
2763
|
-
for (const link of ctx.links) {
|
|
2764
|
-
const normalized = link.trigger?.normalizedTrigger;
|
|
2765
|
-
if (!normalized) continue;
|
|
2766
|
-
push(normalized, {
|
|
2767
|
-
kind: "invocation",
|
|
2768
|
-
token: link.target,
|
|
2769
|
-
nodeId: link.source
|
|
2770
|
-
});
|
|
2771
|
-
}
|
|
2772
|
-
const issues = [];
|
|
2773
|
-
for (const [normalized, claims] of buckets) {
|
|
2774
|
-
const issue = analyzeTriggerBucket(normalized, claims);
|
|
2775
|
-
if (issue) issues.push(issue);
|
|
2776
|
-
}
|
|
2777
|
-
return issues;
|
|
2778
|
-
}
|
|
2779
|
-
};
|
|
2780
|
-
function analyzeTriggerBucket(normalized, claims) {
|
|
2781
|
-
const advertiserPaths = [
|
|
2782
|
-
...new Set(claims.filter((c) => c.kind === "advertiser").map((c) => c.token))
|
|
2783
|
-
].sort();
|
|
2784
|
-
const invocationTargets = [
|
|
2785
|
-
...new Set(claims.filter((c) => c.kind === "invocation").map((c) => c.token))
|
|
2786
|
-
].sort();
|
|
2787
|
-
const advertisers = claims.filter(
|
|
2788
|
-
(c) => c.kind === "advertiser"
|
|
2789
|
-
);
|
|
2790
|
-
const advertiserAmbiguous = advertiserPaths.length >= 2;
|
|
2791
|
-
const invocationAmbiguous = invocationTargets.length >= 2;
|
|
2792
|
-
const canonicalForms = new Set(advertisers.map((a) => a.canonicalForm));
|
|
2793
|
-
const nonCanonicalInvocations = invocationTargets.filter((t) => !canonicalForms.has(t));
|
|
2794
|
-
const crossKindAmbiguous = advertiserPaths.length === 1 && nonCanonicalInvocations.length >= 1;
|
|
2795
|
-
if (!advertiserAmbiguous && !invocationAmbiguous && !crossKindAmbiguous) {
|
|
2796
|
-
return null;
|
|
2797
|
-
}
|
|
2798
|
-
const nodeIds = [...new Set(claims.map((c) => c.nodeId))].sort();
|
|
2799
|
-
const parts = [];
|
|
2800
|
-
if (advertiserAmbiguous) {
|
|
2801
|
-
parts.push(
|
|
2802
|
-
tx(TRIGGER_COLLISION_TEXTS.partAdvertisers, {
|
|
2803
|
-
count: advertiserPaths.length,
|
|
2804
|
-
paths: advertiserPaths.join(", ")
|
|
2805
|
-
})
|
|
2806
|
-
);
|
|
2807
|
-
}
|
|
2808
|
-
if (invocationAmbiguous) {
|
|
2809
|
-
parts.push(
|
|
2810
|
-
tx(TRIGGER_COLLISION_TEXTS.partInvocations, {
|
|
2811
|
-
count: invocationTargets.length,
|
|
2812
|
-
forms: invocationTargets.join(", ")
|
|
2813
|
-
})
|
|
2814
|
-
);
|
|
2815
|
-
} else if (crossKindAmbiguous) {
|
|
2816
|
-
const template = nonCanonicalInvocations.length > 1 ? TRIGGER_COLLISION_TEXTS.partNonCanonicalPlural : TRIGGER_COLLISION_TEXTS.partNonCanonicalSingular;
|
|
2817
|
-
parts.push(
|
|
2818
|
-
tx(template, {
|
|
2819
|
-
forms: nonCanonicalInvocations.join(", "),
|
|
2820
|
-
advertiser: advertiserPaths[0]
|
|
2821
|
-
})
|
|
2822
|
-
);
|
|
2823
|
-
}
|
|
2824
|
-
const message = parts.length === 2 ? tx(TRIGGER_COLLISION_TEXTS.messageTwoParts, {
|
|
2825
|
-
normalized,
|
|
2826
|
-
first: parts[0],
|
|
2827
|
-
second: parts[1]
|
|
2828
|
-
}) : tx(TRIGGER_COLLISION_TEXTS.messageOnePart, {
|
|
2829
|
-
normalized,
|
|
2830
|
-
part: parts[0]
|
|
2831
|
-
});
|
|
2832
|
-
return {
|
|
2833
|
-
analyzerId: ID21,
|
|
2834
|
-
severity: "error",
|
|
2835
|
-
nodeIds,
|
|
2836
|
-
message,
|
|
2837
|
-
data: {
|
|
2838
|
-
normalizedTrigger: normalized,
|
|
2839
|
-
invocationTargets,
|
|
2840
|
-
advertiserPaths
|
|
2841
|
-
}
|
|
2842
|
-
};
|
|
2843
|
-
}
|
|
2844
|
-
|
|
2845
|
-
// plugins/core/analyzers/unknown-field/index.ts
|
|
2846
|
-
import { readFileSync } from "fs";
|
|
2847
|
-
import { dirname, resolve as resolve3 } from "path";
|
|
2848
|
-
import { createRequire } from "module";
|
|
2849
|
-
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
2440
|
+
// plugins/core/analyzers/reference-broken/index.ts
|
|
2441
|
+
import { posix as pathPosix4, resolve as resolve3 } from "path";
|
|
2850
2442
|
|
|
2851
|
-
//
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
addFormats(ajv);
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2858
|
-
// plugins/core/analyzers/unknown-field/text.ts
|
|
2859
|
-
var UNKNOWN_FIELD_TEXTS = {
|
|
2860
|
-
/** Key inside `annotations:` is not in the curated catalog. */
|
|
2861
|
-
unknownAnnotationKey: "{{path}}: sidecar annotations contain unknown key '{{key}}' (not in annotations.schema.json catalog).",
|
|
2862
|
-
/** Top-level key is neither reserved, nor a registered plugin namespace, nor a registered root key. */
|
|
2863
|
-
unknownRootKey: "{{path}}: sidecar declares unknown top-level key '{{key}}'; not a reserved block, not a registered plugin namespace, not a registered root contribution.",
|
|
2864
|
-
/** Value under a registered plugin namespace fails the contributed schema. */
|
|
2865
|
-
pluginNamespaceInvalid: "{{path}}: sidecar block '{{pluginId}}.{{key}}' fails the schema contributed by plugin '{{pluginId}}': {{errors}}.",
|
|
2443
|
+
// plugins/core/analyzers/reference-broken/text.ts
|
|
2444
|
+
var REFERENCE_BROKEN_TEXTS = {
|
|
2445
|
+
/** `Broken <kind> reference from <source> → <target>` */
|
|
2446
|
+
message: "Broken {{kind}} reference from {{source}} \u2192 {{target}}",
|
|
2866
2447
|
// Tooltips for the per-node view-contribution badges. Singular vs
|
|
2867
2448
|
// plural keeps the count grammar correct without a sub-template.
|
|
2868
|
-
alertTooltipSingle: "This node has
|
|
2869
|
-
alertTooltipMany: "This node has {{count}}
|
|
2449
|
+
alertTooltipSingle: "This node has a broken reference. Open the inspector for details.",
|
|
2450
|
+
alertTooltipMany: "This node has {{count}} broken references. Open the inspector for details.",
|
|
2451
|
+
// Fix-summary copy when the broken trigger has a same-named file on
|
|
2452
|
+
// disk that does not advertise `name:` in its frontmatter. Two
|
|
2453
|
+
// variants for single vs multiple candidates; same template family
|
|
2454
|
+
// as the alert tooltips above.
|
|
2455
|
+
hintSummarySingle: "Add `name: {{name}}` to the frontmatter of {{candidate}} so this reference resolves.",
|
|
2456
|
+
hintSummaryMany: "Add `name: {{name}}` to the frontmatter of one of these files so this reference resolves: {{candidates}}."
|
|
2870
2457
|
};
|
|
2871
2458
|
|
|
2872
|
-
// plugins/core/analyzers/
|
|
2873
|
-
var
|
|
2874
|
-
var
|
|
2875
|
-
|
|
2876
|
-
id: ID22,
|
|
2459
|
+
// plugins/core/analyzers/reference-broken/index.ts
|
|
2460
|
+
var ID19 = "reference-broken";
|
|
2461
|
+
var referenceBrokenAnalyzer = {
|
|
2462
|
+
id: ID19,
|
|
2877
2463
|
pluginId: CORE_PLUGIN_ID,
|
|
2878
2464
|
kind: "analyzer",
|
|
2879
2465
|
version: "1.0.0",
|
|
2880
|
-
description: "
|
|
2466
|
+
description: "Flags arrows pointing at a node not part of the current scan.",
|
|
2881
2467
|
mode: "deterministic",
|
|
2882
2468
|
ui: {
|
|
2883
2469
|
// Corner badge on the graph card; count omitted when there is a
|
|
2884
|
-
// single
|
|
2470
|
+
// single broken ref (avoids a noisy "icon + 1" chip).
|
|
2885
2471
|
alert: {
|
|
2886
2472
|
slot: "graph.node.alert",
|
|
2887
|
-
|
|
2888
|
-
// alert's "attention-grabbing solid" pattern; the footer chip
|
|
2889
|
-
// below stays outlined for the quieter counter pairing.
|
|
2890
|
-
icon: "fa-solid fa-triangle-exclamation",
|
|
2473
|
+
icon: "fa-solid fa-circle-xmark",
|
|
2891
2474
|
emitWhenEmpty: false
|
|
2892
2475
|
},
|
|
2893
|
-
// Footer chip on the card
|
|
2894
|
-
//
|
|
2895
|
-
//
|
|
2896
|
-
//
|
|
2897
|
-
//
|
|
2898
|
-
// is required: with `value: 0` the slot treats the payload as
|
|
2899
|
-
// empty, so the manifest has to opt in to keep the emission.
|
|
2476
|
+
// Footer chip on the card. `_counter` shape, `value` always shows,
|
|
2477
|
+
// so the operator sees "how many" at a glance. Renders OUTLINED
|
|
2478
|
+
// (`fa-regular`) so the corner alert (filled, attention-grabbing)
|
|
2479
|
+
// and the footer chip (quieter, paired with a number) read as two
|
|
2480
|
+
// beats of the same signal rather than two identical glyphs.
|
|
2900
2481
|
chip: {
|
|
2901
2482
|
slot: "card.footer.right",
|
|
2902
|
-
icon: "
|
|
2903
|
-
emitWhenEmpty:
|
|
2904
|
-
priority:
|
|
2483
|
+
icon: "fa-regular fa-circle-xmark",
|
|
2484
|
+
emitWhenEmpty: false,
|
|
2485
|
+
priority: 40
|
|
2905
2486
|
}
|
|
2906
2487
|
},
|
|
2907
|
-
//
|
|
2908
|
-
//
|
|
2909
|
-
//
|
|
2910
|
-
//
|
|
2911
|
-
// `context/lint.md` category 7 (recursive type-discriminator walkers).
|
|
2488
|
+
// The resolver, the reference-paths escape hatch, the per-source
|
|
2489
|
+
// aggregation, and the dual-slot emit (with single/plural tooltip and
|
|
2490
|
+
// optional count) all live in one flow because they share the per-link
|
|
2491
|
+
// loop. Splitting them would re-walk `ctx.links` three times.
|
|
2912
2492
|
// eslint-disable-next-line complexity
|
|
2913
2493
|
evaluate(ctx) {
|
|
2914
|
-
const
|
|
2915
|
-
|
|
2916
|
-
const
|
|
2917
|
-
const
|
|
2918
|
-
const namespacedByPlugin = indexNamespacedContributions(contributions);
|
|
2919
|
-
const rootKeys = indexRootContributions(contributions);
|
|
2920
|
-
const knownPluginIds = collectPluginIds(contributions);
|
|
2494
|
+
const byPath3 = new Set(ctx.nodes.map((n) => n.path));
|
|
2495
|
+
const byNormalizedName = indexByNormalizedName(ctx.nodes);
|
|
2496
|
+
const byBasenameWithoutName = indexByBasenameWithoutName(ctx.nodes);
|
|
2497
|
+
const refIndex = ctx.referenceablePaths && ctx.referenceablePaths.size > 0 && ctx.cwd ? { paths: ctx.referenceablePaths, cwd: ctx.cwd } : null;
|
|
2921
2498
|
const issues = [];
|
|
2922
2499
|
const perNode = /* @__PURE__ */ new Map();
|
|
2923
|
-
const
|
|
2924
|
-
|
|
2500
|
+
for (const link of ctx.links) {
|
|
2501
|
+
if (isResolved(link, byPath3, byNormalizedName)) continue;
|
|
2502
|
+
if (refIndex && resolvesViaReferencePaths(link, refIndex)) continue;
|
|
2503
|
+
const candidates = findHintCandidates(link, byBasenameWithoutName);
|
|
2504
|
+
issues.push(buildIssue(link, candidates));
|
|
2505
|
+
perNode.set(link.source, (perNode.get(link.source) ?? 0) + 1);
|
|
2506
|
+
}
|
|
2507
|
+
for (const [nodePath, count] of perNode) {
|
|
2508
|
+
const tooltip = count === 1 ? REFERENCE_BROKEN_TEXTS.alertTooltipSingle : tx(REFERENCE_BROKEN_TEXTS.alertTooltipMany, { count });
|
|
2509
|
+
const capped = Math.min(count, 99);
|
|
2510
|
+
ctx.emitContribution(nodePath, "alert", {
|
|
2511
|
+
icon: "fa-solid fa-circle-xmark",
|
|
2512
|
+
severity: "danger",
|
|
2513
|
+
tooltip
|
|
2514
|
+
});
|
|
2515
|
+
ctx.emitContribution(nodePath, "chip", {
|
|
2516
|
+
value: capped,
|
|
2517
|
+
severity: "danger",
|
|
2518
|
+
tooltip
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
return issues;
|
|
2522
|
+
}
|
|
2523
|
+
};
|
|
2524
|
+
function buildIssue(link, hintCandidates = []) {
|
|
2525
|
+
const data = {
|
|
2526
|
+
target: link.target,
|
|
2527
|
+
kind: link.kind,
|
|
2528
|
+
trigger: link.trigger?.normalizedTrigger ?? null
|
|
2529
|
+
};
|
|
2530
|
+
const issue = {
|
|
2531
|
+
analyzerId: ID19,
|
|
2532
|
+
severity: "warn",
|
|
2533
|
+
nodeIds: [link.source],
|
|
2534
|
+
message: tx(REFERENCE_BROKEN_TEXTS.message, {
|
|
2535
|
+
kind: link.kind,
|
|
2536
|
+
source: link.source,
|
|
2537
|
+
target: link.target
|
|
2538
|
+
}),
|
|
2539
|
+
data
|
|
2540
|
+
};
|
|
2541
|
+
if (hintCandidates.length > 0) {
|
|
2542
|
+
const suggestedName = (link.trigger?.normalizedTrigger ?? "").replace(/^[/@]/, "").trim();
|
|
2543
|
+
const candidatePaths = hintCandidates.map((n) => n.path);
|
|
2544
|
+
data["hint"] = {
|
|
2545
|
+
kind: "missing-frontmatter-name",
|
|
2546
|
+
suggestedName,
|
|
2547
|
+
candidates: candidatePaths
|
|
2925
2548
|
};
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2549
|
+
issue.fix = {
|
|
2550
|
+
summary: candidatePaths.length === 1 ? tx(REFERENCE_BROKEN_TEXTS.hintSummarySingle, {
|
|
2551
|
+
name: suggestedName,
|
|
2552
|
+
candidate: candidatePaths[0]
|
|
2553
|
+
}) : tx(REFERENCE_BROKEN_TEXTS.hintSummaryMany, {
|
|
2554
|
+
name: suggestedName,
|
|
2555
|
+
candidates: candidatePaths.join(", ")
|
|
2556
|
+
}),
|
|
2557
|
+
autofixable: false
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
return issue;
|
|
2561
|
+
}
|
|
2562
|
+
function resolvesViaReferencePaths(link, refIndex) {
|
|
2563
|
+
if (!isPathStyleLink(link)) return false;
|
|
2564
|
+
return refIndex.paths.has(resolve3(refIndex.cwd, link.target));
|
|
2565
|
+
}
|
|
2566
|
+
function indexByNormalizedName(nodes) {
|
|
2567
|
+
const out = /* @__PURE__ */ new Map();
|
|
2568
|
+
for (const node of nodes) {
|
|
2569
|
+
const raw = node.frontmatter?.["name"];
|
|
2570
|
+
const name = typeof raw === "string" ? raw : "";
|
|
2571
|
+
if (!name) continue;
|
|
2572
|
+
const key = normalizeTrigger(name);
|
|
2573
|
+
const bucket = out.get(key) ?? [];
|
|
2574
|
+
bucket.push(node);
|
|
2575
|
+
out.set(key, bucket);
|
|
2576
|
+
}
|
|
2577
|
+
return out;
|
|
2578
|
+
}
|
|
2579
|
+
function basenameWithoutExt(path) {
|
|
2580
|
+
const base = pathPosix4.basename(path);
|
|
2581
|
+
const ext = pathPosix4.extname(base);
|
|
2582
|
+
return ext ? base.slice(0, -ext.length) : base;
|
|
2583
|
+
}
|
|
2584
|
+
function indexByBasenameWithoutName(nodes) {
|
|
2585
|
+
const out = /* @__PURE__ */ new Map();
|
|
2586
|
+
for (const node of nodes) {
|
|
2587
|
+
const raw = node.frontmatter?.["name"];
|
|
2588
|
+
const name = typeof raw === "string" ? raw : "";
|
|
2589
|
+
if (name) continue;
|
|
2590
|
+
const bare = basenameWithoutExt(node.path);
|
|
2591
|
+
if (!bare) continue;
|
|
2592
|
+
const key = normalizeTrigger(bare);
|
|
2593
|
+
if (!key) continue;
|
|
2594
|
+
const bucket = out.get(key) ?? [];
|
|
2595
|
+
bucket.push(node);
|
|
2596
|
+
out.set(key, bucket);
|
|
2597
|
+
}
|
|
2598
|
+
return out;
|
|
2599
|
+
}
|
|
2600
|
+
function findHintCandidates(link, idx) {
|
|
2601
|
+
const normalized = link.trigger?.normalizedTrigger;
|
|
2602
|
+
if (!normalized) return [];
|
|
2603
|
+
const sigil = normalized.charAt(0);
|
|
2604
|
+
if (sigil !== "/" && sigil !== "@") return [];
|
|
2605
|
+
const withoutSigil = normalized.slice(1).trim();
|
|
2606
|
+
if (!withoutSigil) return [];
|
|
2607
|
+
return idx.get(withoutSigil) ?? [];
|
|
2608
|
+
}
|
|
2609
|
+
function isResolved(link, byPath3, byNormalizedName) {
|
|
2610
|
+
const normalized = link.trigger?.normalizedTrigger;
|
|
2611
|
+
if (normalized) {
|
|
2612
|
+
const withoutSigil = normalized.replace(/^[/@]/, "").trim();
|
|
2613
|
+
if (byNormalizedName.has(withoutSigil)) return true;
|
|
2614
|
+
}
|
|
2615
|
+
if (byPath3.has(link.target)) return true;
|
|
2616
|
+
return false;
|
|
2617
|
+
}
|
|
2618
|
+
function isPathStyleLink(link) {
|
|
2619
|
+
const sigil = link.trigger?.normalizedTrigger?.charAt(0);
|
|
2620
|
+
if (sigil === "/" || sigil === "@") return false;
|
|
2621
|
+
return true;
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
// plugins/core/analyzers/reference-redundant/text.ts
|
|
2625
|
+
var REFERENCE_REDUNDANT_TEXTS = {
|
|
2626
|
+
/**
|
|
2627
|
+
* Multi-form / multi-occurrence reference message. Lists each
|
|
2628
|
+
* occurrence (trigger + line) so the operator sees the full
|
|
2629
|
+
* authorial surface without having to grep the body.
|
|
2630
|
+
*/
|
|
2631
|
+
message: "{{source}} references {{resolvedTarget}} via {{count}} occurrences: {{occurrences}}. Consider consolidating to a single form to reduce maintenance surface and avoid duplicate inlining at runtime.",
|
|
2632
|
+
/** Inline separator between occurrences in the message. */
|
|
2633
|
+
occurrenceSeparator: ", ",
|
|
2634
|
+
/** Per-occurrence formatting (trigger + line). */
|
|
2635
|
+
occurrence: "`{{trigger}}` ({{kind}}, line {{line}})",
|
|
2636
|
+
/** Per-occurrence formatting when the extractor did not record a line. */
|
|
2637
|
+
occurrenceUnknownLine: "`{{trigger}}` ({{kind}}, unknown line)"
|
|
2638
|
+
};
|
|
2639
|
+
|
|
2640
|
+
// plugins/core/analyzers/reference-redundant/index.ts
|
|
2641
|
+
var ID20 = "reference-redundant";
|
|
2642
|
+
var referenceRedundantAnalyzer = {
|
|
2643
|
+
id: ID20,
|
|
2644
|
+
pluginId: CORE_PLUGIN_ID,
|
|
2645
|
+
kind: "analyzer",
|
|
2646
|
+
version: "1.0.0",
|
|
2647
|
+
description: "Flags when one node references the same target through two or more different links (e.g. a markdown link plus a `references:` entry).",
|
|
2648
|
+
mode: "deterministic",
|
|
2649
|
+
evaluate(ctx) {
|
|
2650
|
+
if (ctx.links.length === 0) return [];
|
|
2651
|
+
const byPath3 = /* @__PURE__ */ new Map();
|
|
2652
|
+
for (const node of ctx.nodes) byPath3.set(node.path, node);
|
|
2653
|
+
const byName = buildNameIndex2(ctx.nodes);
|
|
2654
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2655
|
+
for (const link of ctx.links) {
|
|
2656
|
+
const resolved = resolveTargetPath(link, byPath3, byName);
|
|
2657
|
+
if (!resolved) continue;
|
|
2658
|
+
const key = `${link.source}\0${resolved}`;
|
|
2659
|
+
const bucket = groups.get(key);
|
|
2660
|
+
if (bucket) bucket.push(link);
|
|
2661
|
+
else groups.set(key, [link]);
|
|
2988
2662
|
}
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
value: 0,
|
|
2663
|
+
const issues = [];
|
|
2664
|
+
for (const [key, links] of groups) {
|
|
2665
|
+
const totalOccurrences = links.reduce((acc, l) => acc + (l.occurrences?.length ?? 1), 0);
|
|
2666
|
+
if (totalOccurrences < 2) continue;
|
|
2667
|
+
const [source, resolvedTarget] = key.split("\0");
|
|
2668
|
+
const flat = flattenOccurrences(links);
|
|
2669
|
+
issues.push({
|
|
2670
|
+
analyzerId: ID20,
|
|
2998
2671
|
severity: "warn",
|
|
2999
|
-
|
|
2672
|
+
nodeIds: [source],
|
|
2673
|
+
message: tx(REFERENCE_REDUNDANT_TEXTS.message, {
|
|
2674
|
+
source,
|
|
2675
|
+
resolvedTarget,
|
|
2676
|
+
count: flat.length,
|
|
2677
|
+
occurrences: flat.map(formatOccurrence).join(REFERENCE_REDUNDANT_TEXTS.occurrenceSeparator)
|
|
2678
|
+
}),
|
|
2679
|
+
data: {
|
|
2680
|
+
target: resolvedTarget,
|
|
2681
|
+
resolvedTarget,
|
|
2682
|
+
occurrences: flat.map((o) => ({
|
|
2683
|
+
kind: o.kind,
|
|
2684
|
+
trigger: o.originalTrigger,
|
|
2685
|
+
line: o.line ?? null,
|
|
2686
|
+
extractor: o.extractor
|
|
2687
|
+
}))
|
|
2688
|
+
}
|
|
3000
2689
|
});
|
|
3001
2690
|
}
|
|
3002
2691
|
return issues;
|
|
3003
2692
|
}
|
|
3004
2693
|
};
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
}
|
|
3017
|
-
|
|
3018
|
-
const out = /* @__PURE__ */ new Map();
|
|
3019
|
-
const ajv = new Ajv2020({ strict: false, allErrors: true, allowUnionTypes: true });
|
|
3020
|
-
applyAjvFormats(ajv);
|
|
3021
|
-
for (const entry of contributions) {
|
|
3022
|
-
if (entry.location !== "namespaced") continue;
|
|
3023
|
-
let bucket = out.get(entry.pluginId);
|
|
3024
|
-
if (!bucket) {
|
|
3025
|
-
bucket = /* @__PURE__ */ new Map();
|
|
3026
|
-
out.set(entry.pluginId, bucket);
|
|
3027
|
-
}
|
|
3028
|
-
try {
|
|
3029
|
-
bucket.set(entry.key, ajv.compile(entry.schema));
|
|
3030
|
-
} catch {
|
|
2694
|
+
function flattenOccurrences(links) {
|
|
2695
|
+
const out = [];
|
|
2696
|
+
for (const link of links) {
|
|
2697
|
+
if (link.occurrences && link.occurrences.length > 0) {
|
|
2698
|
+
for (const occ of link.occurrences) {
|
|
2699
|
+
out.push({
|
|
2700
|
+
kind: link.kind,
|
|
2701
|
+
originalTrigger: occ.originalTrigger,
|
|
2702
|
+
extractor: occ.extractor,
|
|
2703
|
+
line: occ.location?.line ?? null
|
|
2704
|
+
});
|
|
2705
|
+
}
|
|
2706
|
+
continue;
|
|
3031
2707
|
}
|
|
2708
|
+
const trigger = link.trigger?.originalTrigger ?? link.target;
|
|
2709
|
+
out.push({
|
|
2710
|
+
kind: link.kind,
|
|
2711
|
+
originalTrigger: trigger,
|
|
2712
|
+
extractor: link.sources[0] ?? "unknown",
|
|
2713
|
+
line: link.location?.line ?? null
|
|
2714
|
+
});
|
|
3032
2715
|
}
|
|
2716
|
+
out.sort((a, b) => {
|
|
2717
|
+
const la = a.line ?? Number.MAX_SAFE_INTEGER;
|
|
2718
|
+
const lb = b.line ?? Number.MAX_SAFE_INTEGER;
|
|
2719
|
+
if (la !== lb) return la - lb;
|
|
2720
|
+
return a.originalTrigger.localeCompare(b.originalTrigger);
|
|
2721
|
+
});
|
|
3033
2722
|
return out;
|
|
3034
2723
|
}
|
|
3035
|
-
function
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
if (entry.location === "root") out.add(entry.key);
|
|
2724
|
+
function formatOccurrence(occ) {
|
|
2725
|
+
if (occ.line === null) {
|
|
2726
|
+
return tx(REFERENCE_REDUNDANT_TEXTS.occurrenceUnknownLine, { trigger: occ.originalTrigger, kind: occ.kind });
|
|
3039
2727
|
}
|
|
3040
|
-
return
|
|
2728
|
+
return tx(REFERENCE_REDUNDANT_TEXTS.occurrence, { trigger: occ.originalTrigger, kind: occ.kind, line: occ.line });
|
|
3041
2729
|
}
|
|
3042
|
-
function
|
|
3043
|
-
const out = /* @__PURE__ */ new
|
|
3044
|
-
for (const
|
|
2730
|
+
function buildNameIndex2(nodes) {
|
|
2731
|
+
const out = /* @__PURE__ */ new Map();
|
|
2732
|
+
for (const node of nodes) {
|
|
2733
|
+
for (const candidate of collectIdentifiers(node)) {
|
|
2734
|
+
const normalised = normalizeTrigger(candidate);
|
|
2735
|
+
if (!normalised) continue;
|
|
2736
|
+
const bucket = out.get(normalised);
|
|
2737
|
+
if (bucket) bucket.push(node.path);
|
|
2738
|
+
else out.set(normalised, [node.path]);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
3045
2741
|
return out;
|
|
3046
2742
|
}
|
|
3047
|
-
|
|
2743
|
+
function collectIdentifiers(node) {
|
|
2744
|
+
const out = [];
|
|
2745
|
+
const fmName = node.frontmatter?.["name"];
|
|
2746
|
+
if (typeof fmName === "string" && fmName.length > 0) out.push(fmName);
|
|
2747
|
+
const segs = node.path.split("/");
|
|
2748
|
+
const last = segs[segs.length - 1] ?? "";
|
|
2749
|
+
if (last) {
|
|
2750
|
+
const stem = last.replace(/\.[^.]+$/, "");
|
|
2751
|
+
if (stem) out.push(stem);
|
|
2752
|
+
}
|
|
2753
|
+
if (segs.length >= 2) {
|
|
2754
|
+
const dirBase = segs[segs.length - 2];
|
|
2755
|
+
if (dirBase) out.push(dirBase);
|
|
2756
|
+
}
|
|
2757
|
+
return out;
|
|
2758
|
+
}
|
|
2759
|
+
function resolveTargetPath(link, byPath3, byName) {
|
|
2760
|
+
if (byPath3.has(link.target)) return link.target;
|
|
2761
|
+
const trigger = link.trigger?.normalizedTrigger;
|
|
2762
|
+
if (!trigger) return null;
|
|
2763
|
+
const stripped = trigger.replace(/^[/@]/, "").trim();
|
|
2764
|
+
if (!stripped) return null;
|
|
2765
|
+
const candidates = byName.get(stripped);
|
|
2766
|
+
if (!candidates || candidates.length === 0) return null;
|
|
2767
|
+
return candidates[0] ?? null;
|
|
2768
|
+
}
|
|
2769
|
+
|
|
3048
2770
|
// kernel/adapters/schema-validators.ts
|
|
3049
2771
|
import { readFileSync as readFileSync2 } from "fs";
|
|
3050
2772
|
import { dirname as dirname2, resolve as resolve4 } from "path";
|
|
@@ -3252,8 +2974,8 @@ function existsSyncSafe(path) {
|
|
|
3252
2974
|
}
|
|
3253
2975
|
}
|
|
3254
2976
|
|
|
3255
|
-
// plugins/core/analyzers/
|
|
3256
|
-
var
|
|
2977
|
+
// plugins/core/analyzers/schema-violation/text.ts
|
|
2978
|
+
var SCHEMA_VIOLATION_TEXTS = {
|
|
3257
2979
|
/** `Node <path> failed schema validation: <errors>` */
|
|
3258
2980
|
nodeFailure: "Node {{path}} failed schema validation: {{errors}}",
|
|
3259
2981
|
/** `Link <source> → <target> failed schema validation: <errors>` */
|
|
@@ -3266,150 +2988,428 @@ var VALIDATE_ALL_TEXTS = {
|
|
|
3266
2988
|
alertTooltipMany: "{{count}} schema validation issues on this node."
|
|
3267
2989
|
};
|
|
3268
2990
|
|
|
3269
|
-
// plugins/core/analyzers/
|
|
3270
|
-
var
|
|
3271
|
-
var
|
|
2991
|
+
// plugins/core/analyzers/schema-violation/index.ts
|
|
2992
|
+
var ID21 = "schema-violation";
|
|
2993
|
+
var schemaViolationAnalyzer = {
|
|
2994
|
+
id: ID21,
|
|
2995
|
+
pluginId: CORE_PLUGIN_ID,
|
|
2996
|
+
kind: "analyzer",
|
|
2997
|
+
version: "1.0.0",
|
|
2998
|
+
description: "Flags nodes or links that violate the project schemas.",
|
|
2999
|
+
mode: "deterministic",
|
|
3000
|
+
ui: {
|
|
3001
|
+
// Corner badge on the graph card; surfaces when the node body /
|
|
3002
|
+
// frontmatter fails schema validation (parse error, missing
|
|
3003
|
+
// `name`/`description`, malformed YAML, etc.). Same visual
|
|
3004
|
+
// chassis as `core/reference-broken`, danger severity.
|
|
3005
|
+
alert: {
|
|
3006
|
+
slot: "graph.node.alert",
|
|
3007
|
+
icon: "fa-solid fa-triangle-exclamation",
|
|
3008
|
+
emitWhenEmpty: false
|
|
3009
|
+
},
|
|
3010
|
+
// Footer chip that mirrors the corner alert with the actual
|
|
3011
|
+
// count so the operator can scan the cards and prioritise.
|
|
3012
|
+
// Outlined (vs the filled corner alert) per the broken-ref
|
|
3013
|
+
// pattern: two beats of the same signal.
|
|
3014
|
+
chip: {
|
|
3015
|
+
slot: "card.footer.right",
|
|
3016
|
+
icon: "fa-regular fa-triangle-exclamation",
|
|
3017
|
+
emitWhenEmpty: false,
|
|
3018
|
+
priority: 35
|
|
3019
|
+
}
|
|
3020
|
+
},
|
|
3021
|
+
evaluate(ctx) {
|
|
3022
|
+
const validators = loadSchemaValidators();
|
|
3023
|
+
const findings = [];
|
|
3024
|
+
const perNode = /* @__PURE__ */ new Map();
|
|
3025
|
+
for (const node of ctx.nodes) {
|
|
3026
|
+
const before = findings.length;
|
|
3027
|
+
collectNodeFindings(validators, node, findings);
|
|
3028
|
+
collectFrontmatterBaseFindings(node, findings);
|
|
3029
|
+
if (findings.length > before) {
|
|
3030
|
+
perNode.set(node.path, (perNode.get(node.path) ?? 0) + (findings.length - before));
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
for (const link of ctx.links) {
|
|
3034
|
+
collectLinkFindings(validators, link, findings);
|
|
3035
|
+
}
|
|
3036
|
+
for (const [nodePath, count] of perNode) {
|
|
3037
|
+
const tooltip = count === 1 ? SCHEMA_VIOLATION_TEXTS.alertTooltipSingle : tx(SCHEMA_VIOLATION_TEXTS.alertTooltipMany, { count });
|
|
3038
|
+
const capped = Math.min(count, 99);
|
|
3039
|
+
ctx.emitContribution(nodePath, "alert", {
|
|
3040
|
+
icon: "fa-solid fa-triangle-exclamation",
|
|
3041
|
+
severity: "danger",
|
|
3042
|
+
tooltip
|
|
3043
|
+
});
|
|
3044
|
+
ctx.emitContribution(nodePath, "chip", {
|
|
3045
|
+
value: capped,
|
|
3046
|
+
severity: "danger",
|
|
3047
|
+
tooltip
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
return findings;
|
|
3051
|
+
}
|
|
3052
|
+
};
|
|
3053
|
+
function collectNodeFindings(v, node, out) {
|
|
3054
|
+
const result = v.validate("node", toNodeForSchema(node));
|
|
3055
|
+
if (result.ok) return;
|
|
3056
|
+
out.push({
|
|
3057
|
+
analyzerId: ID21,
|
|
3058
|
+
severity: "error",
|
|
3059
|
+
nodeIds: [node.path],
|
|
3060
|
+
message: tx(SCHEMA_VIOLATION_TEXTS.nodeFailure, {
|
|
3061
|
+
path: node.path,
|
|
3062
|
+
errors: result.errors
|
|
3063
|
+
}),
|
|
3064
|
+
data: { target: "node", path: node.path }
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
3067
|
+
function collectFrontmatterBaseFindings(node, out) {
|
|
3068
|
+
if (node.provider === "markdown") return;
|
|
3069
|
+
if (node.bytes.frontmatter === 0) return;
|
|
3070
|
+
const fm = node.frontmatter ?? {};
|
|
3071
|
+
const missing = [];
|
|
3072
|
+
if (isMissingStringField(fm, "name")) missing.push("name");
|
|
3073
|
+
if (isMissingStringField(fm, "description")) missing.push("description");
|
|
3074
|
+
if (missing.length === 0) return;
|
|
3075
|
+
out.push({
|
|
3076
|
+
analyzerId: ID21,
|
|
3077
|
+
// `warn` (not `error`) so the default `sm scan` exit code stays
|
|
3078
|
+
// 0 even when nodes are missing frontmatter base fields. Strict
|
|
3079
|
+
// mode (`sm scan --strict`) still escalates to exit 1. Matches
|
|
3080
|
+
// the `frontmatter-invalid` severity policy of the orchestrator.
|
|
3081
|
+
severity: "warn",
|
|
3082
|
+
nodeIds: [node.path],
|
|
3083
|
+
message: tx(SCHEMA_VIOLATION_TEXTS.frontmatterBaseFailure, {
|
|
3084
|
+
path: node.path,
|
|
3085
|
+
missing: missing.join(", ")
|
|
3086
|
+
}),
|
|
3087
|
+
data: { target: "frontmatter", path: node.path, missing }
|
|
3088
|
+
});
|
|
3089
|
+
}
|
|
3090
|
+
function isMissingStringField(fm, field) {
|
|
3091
|
+
const v = fm[field];
|
|
3092
|
+
return typeof v !== "string" || v.length === 0;
|
|
3093
|
+
}
|
|
3094
|
+
function collectLinkFindings(v, link, out) {
|
|
3095
|
+
const result = v.validate("link", toLinkForSchema(link));
|
|
3096
|
+
if (result.ok) return;
|
|
3097
|
+
out.push({
|
|
3098
|
+
analyzerId: ID21,
|
|
3099
|
+
severity: "error",
|
|
3100
|
+
nodeIds: [link.source],
|
|
3101
|
+
message: tx(SCHEMA_VIOLATION_TEXTS.linkFailure, {
|
|
3102
|
+
source: link.source,
|
|
3103
|
+
target: link.target,
|
|
3104
|
+
errors: result.errors
|
|
3105
|
+
}),
|
|
3106
|
+
data: { target: "link", source: link.source, to: link.target }
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
function toNodeForSchema(node) {
|
|
3110
|
+
return {
|
|
3111
|
+
path: node.path,
|
|
3112
|
+
kind: node.kind,
|
|
3113
|
+
provider: node.provider,
|
|
3114
|
+
bodyHash: node.bodyHash,
|
|
3115
|
+
frontmatterHash: node.frontmatterHash,
|
|
3116
|
+
bytes: node.bytes,
|
|
3117
|
+
tokens: node.tokens ?? void 0,
|
|
3118
|
+
linksOutCount: node.linksOutCount,
|
|
3119
|
+
linksInCount: node.linksInCount,
|
|
3120
|
+
externalRefsCount: node.externalRefsCount,
|
|
3121
|
+
frontmatter: node.frontmatter ?? {},
|
|
3122
|
+
sidecar: node.sidecar ?? void 0
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
function toLinkForSchema(link) {
|
|
3126
|
+
return {
|
|
3127
|
+
source: link.source,
|
|
3128
|
+
target: link.target,
|
|
3129
|
+
kind: link.kind,
|
|
3130
|
+
confidence: link.confidence,
|
|
3131
|
+
sources: link.sources,
|
|
3132
|
+
trigger: link.trigger ?? void 0,
|
|
3133
|
+
location: link.location ?? void 0,
|
|
3134
|
+
raw: link.raw ?? void 0
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
// plugins/core/analyzers/signal-collision/text.ts
|
|
3139
|
+
var SIGNAL_COLLISION_TEXTS = {
|
|
3140
|
+
/**
|
|
3141
|
+
* Per-Signal warn issue: two extractors detected something at
|
|
3142
|
+
* overlapping byte ranges within the same node and the resolver
|
|
3143
|
+
* dropped the loser. Surfaces WHO lost, WHO won, and the tiebreak
|
|
3144
|
+
* reason so the operator can understand why a candidate edge did NOT
|
|
3145
|
+
* become a Link.
|
|
3146
|
+
*
|
|
3147
|
+
* Placeholders are deliberately verbose because this is one of the
|
|
3148
|
+
* few diagnostic surfaces where the operator may need to disambiguate
|
|
3149
|
+
* a confusing graph (e.g. a `[link](path)` followed by `@path` inside
|
|
3150
|
+
* the same paragraph, the markdown-link wins and the at-directive
|
|
3151
|
+
* silently disappears without this warning).
|
|
3152
|
+
*/
|
|
3153
|
+
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.",
|
|
3154
|
+
/**
|
|
3155
|
+
* Same warn but for the rare case the resolver rejected a Signal
|
|
3156
|
+
* because the operator disabled its extractor via
|
|
3157
|
+
* `plugins.<id>.extensions.<extId>.enabled`. Phase 4+ stub: today the
|
|
3158
|
+
* filter is not wired so this template is unreachable from the
|
|
3159
|
+
* resolver; documented now so the analyzer stays forward-compatible
|
|
3160
|
+
* with the upcoming filter pass.
|
|
3161
|
+
*/
|
|
3162
|
+
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.",
|
|
3163
|
+
/**
|
|
3164
|
+
* Same warn but for the future confidence floor case. Phase 4+ stub:
|
|
3165
|
+
* today the resolver materialises every winning candidate regardless
|
|
3166
|
+
* of confidence, so this template is unreachable; documented for
|
|
3167
|
+
* forward compatibility.
|
|
3168
|
+
*/
|
|
3169
|
+
messageBelowFloor: "Detection `{{loserRaw}}` (offset {{loserRange}}, confidence {{confidence}}) fell below the configured threshold {{threshold}} and was dropped."
|
|
3170
|
+
};
|
|
3171
|
+
|
|
3172
|
+
// plugins/core/analyzers/signal-collision/index.ts
|
|
3173
|
+
var ID22 = "signal-collision";
|
|
3174
|
+
var signalCollisionAnalyzer = {
|
|
3175
|
+
id: ID22,
|
|
3176
|
+
pluginId: CORE_PLUGIN_ID,
|
|
3177
|
+
kind: "analyzer",
|
|
3178
|
+
version: "1.0.0",
|
|
3179
|
+
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.",
|
|
3180
|
+
mode: "deterministic",
|
|
3181
|
+
evaluate(ctx) {
|
|
3182
|
+
const signals = ctx.signals;
|
|
3183
|
+
if (!signals || signals.length === 0) return [];
|
|
3184
|
+
const issues = [];
|
|
3185
|
+
for (const signal of signals) {
|
|
3186
|
+
const issue = makeIssue(signal);
|
|
3187
|
+
if (issue) issues.push(issue);
|
|
3188
|
+
}
|
|
3189
|
+
return issues;
|
|
3190
|
+
}
|
|
3191
|
+
};
|
|
3192
|
+
function makeIssue(signal) {
|
|
3193
|
+
const resolution = signal.resolution;
|
|
3194
|
+
if (!resolution || resolution.outcome !== "rejected") return null;
|
|
3195
|
+
if (resolution.rejectedBy) {
|
|
3196
|
+
const winner = resolution.rejectedBy;
|
|
3197
|
+
const winnerCandidate = signal.candidates[resolution.winnerIndex ?? 0];
|
|
3198
|
+
const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
|
|
3199
|
+
const winnerRange = `${winner.range.start}-${winner.range.end}`;
|
|
3200
|
+
return {
|
|
3201
|
+
analyzerId: ID22,
|
|
3202
|
+
severity: "warn",
|
|
3203
|
+
nodeIds: [signal.source],
|
|
3204
|
+
message: tx(SIGNAL_COLLISION_TEXTS.message, {
|
|
3205
|
+
loserExtractor: winnerCandidate.extractorId,
|
|
3206
|
+
loserRaw: signal.raw,
|
|
3207
|
+
loserRange,
|
|
3208
|
+
winnerExtractor: winner.extractorId,
|
|
3209
|
+
winnerRange,
|
|
3210
|
+
reason: winner.reason
|
|
3211
|
+
}),
|
|
3212
|
+
data: {
|
|
3213
|
+
loser: {
|
|
3214
|
+
extractorId: winnerCandidate.extractorId,
|
|
3215
|
+
raw: signal.raw,
|
|
3216
|
+
range: signal.range ?? null,
|
|
3217
|
+
candidate: {
|
|
3218
|
+
kind: winnerCandidate.kind,
|
|
3219
|
+
target: winnerCandidate.target,
|
|
3220
|
+
confidence: winnerCandidate.confidence
|
|
3221
|
+
}
|
|
3222
|
+
},
|
|
3223
|
+
winner: {
|
|
3224
|
+
extractorId: winner.extractorId,
|
|
3225
|
+
range: winner.range
|
|
3226
|
+
},
|
|
3227
|
+
reason: winner.reason
|
|
3228
|
+
}
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
if (resolution.extractorDisabled) {
|
|
3232
|
+
const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
|
|
3233
|
+
return {
|
|
3234
|
+
analyzerId: ID22,
|
|
3235
|
+
severity: "warn",
|
|
3236
|
+
nodeIds: [signal.source],
|
|
3237
|
+
message: tx(SIGNAL_COLLISION_TEXTS.messageExtractorDisabled, {
|
|
3238
|
+
extractorId: resolution.extractorDisabled.extractorId,
|
|
3239
|
+
loserRaw: signal.raw,
|
|
3240
|
+
loserRange
|
|
3241
|
+
}),
|
|
3242
|
+
data: {
|
|
3243
|
+
extractorDisabled: resolution.extractorDisabled,
|
|
3244
|
+
raw: signal.raw,
|
|
3245
|
+
range: signal.range ?? null
|
|
3246
|
+
}
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3249
|
+
if (resolution.belowFloor) {
|
|
3250
|
+
const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
|
|
3251
|
+
const topCandidate = signal.candidates[0];
|
|
3252
|
+
return {
|
|
3253
|
+
analyzerId: ID22,
|
|
3254
|
+
severity: "warn",
|
|
3255
|
+
nodeIds: [signal.source],
|
|
3256
|
+
message: tx(SIGNAL_COLLISION_TEXTS.messageBelowFloor, {
|
|
3257
|
+
loserRaw: signal.raw,
|
|
3258
|
+
loserRange,
|
|
3259
|
+
confidence: topCandidate.confidence,
|
|
3260
|
+
threshold: resolution.belowFloor.threshold
|
|
3261
|
+
}),
|
|
3262
|
+
data: {
|
|
3263
|
+
belowFloor: resolution.belowFloor,
|
|
3264
|
+
raw: signal.raw,
|
|
3265
|
+
range: signal.range ?? null
|
|
3266
|
+
}
|
|
3267
|
+
};
|
|
3268
|
+
}
|
|
3269
|
+
return null;
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
// plugins/core/analyzers/trigger-collision/text.ts
|
|
3273
|
+
var TRIGGER_COLLISION_TEXTS = {
|
|
3274
|
+
/**
|
|
3275
|
+
* Top-level message when `analyzeTriggerBucket` accumulated exactly one
|
|
3276
|
+
* cause part. Used for the advertiser-ambiguous-only, invocation-
|
|
3277
|
+
* ambiguous-only, and cross-kind-only branches.
|
|
3278
|
+
*/
|
|
3279
|
+
messageOnePart: 'Trigger "{{normalized}}" has {{part}}.',
|
|
3280
|
+
/**
|
|
3281
|
+
* Top-level message when `analyzeTriggerBucket` accumulated two cause
|
|
3282
|
+
* parts (advertiser-ambiguous AND invocation-ambiguous fire together).
|
|
3283
|
+
* The joiner lives inside the template so future locales can adapt it
|
|
3284
|
+
* (e.g. `'; y '` in Spanish) without touching the rule code.
|
|
3285
|
+
*/
|
|
3286
|
+
messageTwoParts: 'Trigger "{{normalized}}" has {{first}}; and {{second}}.',
|
|
3287
|
+
/** `<n> nodes advertise it: <list>` part, fires on the advertiser-ambiguous branch. */
|
|
3288
|
+
partAdvertisers: "{{count}} nodes advertise it: {{paths}}",
|
|
3289
|
+
/** `<n> distinct invocation forms: <list>` part, fires on the invocation-ambiguous branch. */
|
|
3290
|
+
partInvocations: "{{count}} distinct invocation forms: {{forms}}",
|
|
3291
|
+
/** Singular cross-kind cause: `non-canonical invocation <form> against advertiser <path>`. */
|
|
3292
|
+
partNonCanonicalSingular: "non-canonical invocation {{forms}} against advertiser {{advertiser}}",
|
|
3293
|
+
/** Plural cross-kind cause: `non-canonical invocations <forms> against advertiser <path>`. */
|
|
3294
|
+
partNonCanonicalPlural: "non-canonical invocations {{forms}} against advertiser {{advertiser}}"
|
|
3295
|
+
};
|
|
3296
|
+
|
|
3297
|
+
// plugins/core/analyzers/trigger-collision/index.ts
|
|
3298
|
+
var ID23 = "trigger-collision";
|
|
3299
|
+
var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
|
|
3300
|
+
"command",
|
|
3301
|
+
"skill",
|
|
3302
|
+
"agent"
|
|
3303
|
+
]);
|
|
3304
|
+
var triggerCollisionAnalyzer = {
|
|
3272
3305
|
id: ID23,
|
|
3273
3306
|
pluginId: CORE_PLUGIN_ID,
|
|
3274
3307
|
kind: "analyzer",
|
|
3275
|
-
version: "1.0.0",
|
|
3276
|
-
description: "Detects and flags nodes or links violating the project schemas.",
|
|
3277
3308
|
mode: "deterministic",
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
alert: {
|
|
3284
|
-
slot: "graph.node.alert",
|
|
3285
|
-
icon: "fa-solid fa-triangle-exclamation",
|
|
3286
|
-
emitWhenEmpty: false
|
|
3287
|
-
},
|
|
3288
|
-
// Footer chip that mirrors the corner alert with the actual
|
|
3289
|
-
// count so the operator can scan the cards and prioritise.
|
|
3290
|
-
// Outlined (vs the filled corner alert) per the broken-ref
|
|
3291
|
-
// pattern: two beats of the same signal.
|
|
3292
|
-
chip: {
|
|
3293
|
-
slot: "card.footer.right",
|
|
3294
|
-
icon: "fa-regular fa-triangle-exclamation",
|
|
3295
|
-
emitWhenEmpty: false,
|
|
3296
|
-
priority: 35
|
|
3297
|
-
}
|
|
3298
|
-
},
|
|
3309
|
+
version: "1.0.0",
|
|
3310
|
+
description: "Flags two or more nodes that claim the same `/command` or `@agent` name.",
|
|
3311
|
+
// Two claim-collection passes (advertisement + invocation) feeding
|
|
3312
|
+
// the bucket map. Per-bucket analysis lives in `analyzeTriggerBucket`.
|
|
3313
|
+
// eslint-disable-next-line complexity
|
|
3299
3314
|
evaluate(ctx) {
|
|
3300
|
-
const
|
|
3301
|
-
const
|
|
3302
|
-
|
|
3315
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3316
|
+
const push = (key, claim) => {
|
|
3317
|
+
const bucket = buckets.get(key) ?? [];
|
|
3318
|
+
bucket.push(claim);
|
|
3319
|
+
buckets.set(key, bucket);
|
|
3320
|
+
};
|
|
3303
3321
|
for (const node of ctx.nodes) {
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3322
|
+
if (!ADVERTISING_KINDS.has(node.kind)) continue;
|
|
3323
|
+
const raw = node.frontmatter?.["name"];
|
|
3324
|
+
if (typeof raw !== "string" || raw.length === 0) continue;
|
|
3325
|
+
const normalized = `/${normalizeTrigger(raw)}`;
|
|
3326
|
+
if (normalized === "/") continue;
|
|
3327
|
+
push(normalized, {
|
|
3328
|
+
kind: "advertiser",
|
|
3329
|
+
token: node.path,
|
|
3330
|
+
nodeId: node.path,
|
|
3331
|
+
canonicalForm: `/${raw}`
|
|
3332
|
+
});
|
|
3310
3333
|
}
|
|
3311
3334
|
for (const link of ctx.links) {
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
icon: "fa-solid fa-triangle-exclamation",
|
|
3319
|
-
severity: "danger",
|
|
3320
|
-
tooltip
|
|
3321
|
-
});
|
|
3322
|
-
ctx.emitContribution(nodePath, "chip", {
|
|
3323
|
-
value: capped,
|
|
3324
|
-
severity: "danger",
|
|
3325
|
-
tooltip
|
|
3335
|
+
const normalized = link.trigger?.normalizedTrigger;
|
|
3336
|
+
if (!normalized) continue;
|
|
3337
|
+
push(normalized, {
|
|
3338
|
+
kind: "invocation",
|
|
3339
|
+
token: link.target,
|
|
3340
|
+
nodeId: link.source
|
|
3326
3341
|
});
|
|
3327
3342
|
}
|
|
3328
|
-
|
|
3343
|
+
const issues = [];
|
|
3344
|
+
for (const [normalized, claims] of buckets) {
|
|
3345
|
+
const issue = analyzeTriggerBucket(normalized, claims);
|
|
3346
|
+
if (issue) issues.push(issue);
|
|
3347
|
+
}
|
|
3348
|
+
return issues;
|
|
3329
3349
|
}
|
|
3330
3350
|
};
|
|
3331
|
-
function
|
|
3332
|
-
const
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
if (
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
const
|
|
3350
|
-
|
|
3351
|
-
if (
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3351
|
+
function analyzeTriggerBucket(normalized, claims) {
|
|
3352
|
+
const advertiserPaths = [
|
|
3353
|
+
...new Set(claims.filter((c) => c.kind === "advertiser").map((c) => c.token))
|
|
3354
|
+
].sort();
|
|
3355
|
+
const invocationTargets = [
|
|
3356
|
+
...new Set(claims.filter((c) => c.kind === "invocation").map((c) => c.token))
|
|
3357
|
+
].sort();
|
|
3358
|
+
const advertisers = claims.filter(
|
|
3359
|
+
(c) => c.kind === "advertiser"
|
|
3360
|
+
);
|
|
3361
|
+
const advertiserAmbiguous = advertiserPaths.length >= 2;
|
|
3362
|
+
const invocationAmbiguous = invocationTargets.length >= 2;
|
|
3363
|
+
const canonicalForms = new Set(advertisers.map((a) => a.canonicalForm));
|
|
3364
|
+
const nonCanonicalInvocations = invocationTargets.filter((t) => !canonicalForms.has(t));
|
|
3365
|
+
const crossKindAmbiguous = advertiserPaths.length === 1 && nonCanonicalInvocations.length >= 1;
|
|
3366
|
+
if (!advertiserAmbiguous && !invocationAmbiguous && !crossKindAmbiguous) {
|
|
3367
|
+
return null;
|
|
3368
|
+
}
|
|
3369
|
+
const nodeIds = [...new Set(claims.map((c) => c.nodeId))].sort();
|
|
3370
|
+
const parts = [];
|
|
3371
|
+
if (advertiserAmbiguous) {
|
|
3372
|
+
parts.push(
|
|
3373
|
+
tx(TRIGGER_COLLISION_TEXTS.partAdvertisers, {
|
|
3374
|
+
count: advertiserPaths.length,
|
|
3375
|
+
paths: advertiserPaths.join(", ")
|
|
3376
|
+
})
|
|
3377
|
+
);
|
|
3378
|
+
}
|
|
3379
|
+
if (invocationAmbiguous) {
|
|
3380
|
+
parts.push(
|
|
3381
|
+
tx(TRIGGER_COLLISION_TEXTS.partInvocations, {
|
|
3382
|
+
count: invocationTargets.length,
|
|
3383
|
+
forms: invocationTargets.join(", ")
|
|
3384
|
+
})
|
|
3385
|
+
);
|
|
3386
|
+
} else if (crossKindAmbiguous) {
|
|
3387
|
+
const template = nonCanonicalInvocations.length > 1 ? TRIGGER_COLLISION_TEXTS.partNonCanonicalPlural : TRIGGER_COLLISION_TEXTS.partNonCanonicalSingular;
|
|
3388
|
+
parts.push(
|
|
3389
|
+
tx(template, {
|
|
3390
|
+
forms: nonCanonicalInvocations.join(", "),
|
|
3391
|
+
advertiser: advertiserPaths[0]
|
|
3392
|
+
})
|
|
3393
|
+
);
|
|
3394
|
+
}
|
|
3395
|
+
const message = parts.length === 2 ? tx(TRIGGER_COLLISION_TEXTS.messageTwoParts, {
|
|
3396
|
+
normalized,
|
|
3397
|
+
first: parts[0],
|
|
3398
|
+
second: parts[1]
|
|
3399
|
+
}) : tx(TRIGGER_COLLISION_TEXTS.messageOnePart, {
|
|
3400
|
+
normalized,
|
|
3401
|
+
part: parts[0]
|
|
3366
3402
|
});
|
|
3367
|
-
|
|
3368
|
-
function isMissingStringField(fm, field) {
|
|
3369
|
-
const v = fm[field];
|
|
3370
|
-
return typeof v !== "string" || v.length === 0;
|
|
3371
|
-
}
|
|
3372
|
-
function collectLinkFindings(v, link, out) {
|
|
3373
|
-
const result = v.validate("link", toLinkForSchema(link));
|
|
3374
|
-
if (result.ok) return;
|
|
3375
|
-
out.push({
|
|
3403
|
+
return {
|
|
3376
3404
|
analyzerId: ID23,
|
|
3377
3405
|
severity: "error",
|
|
3378
|
-
nodeIds
|
|
3379
|
-
message
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
});
|
|
3386
|
-
}
|
|
3387
|
-
function toNodeForSchema(node) {
|
|
3388
|
-
return {
|
|
3389
|
-
path: node.path,
|
|
3390
|
-
kind: node.kind,
|
|
3391
|
-
provider: node.provider,
|
|
3392
|
-
bodyHash: node.bodyHash,
|
|
3393
|
-
frontmatterHash: node.frontmatterHash,
|
|
3394
|
-
bytes: node.bytes,
|
|
3395
|
-
tokens: node.tokens ?? void 0,
|
|
3396
|
-
linksOutCount: node.linksOutCount,
|
|
3397
|
-
linksInCount: node.linksInCount,
|
|
3398
|
-
externalRefsCount: node.externalRefsCount,
|
|
3399
|
-
frontmatter: node.frontmatter ?? {},
|
|
3400
|
-
sidecar: node.sidecar ?? void 0
|
|
3401
|
-
};
|
|
3402
|
-
}
|
|
3403
|
-
function toLinkForSchema(link) {
|
|
3404
|
-
return {
|
|
3405
|
-
source: link.source,
|
|
3406
|
-
target: link.target,
|
|
3407
|
-
kind: link.kind,
|
|
3408
|
-
confidence: link.confidence,
|
|
3409
|
-
sources: link.sources,
|
|
3410
|
-
trigger: link.trigger ?? void 0,
|
|
3411
|
-
location: link.location ?? void 0,
|
|
3412
|
-
raw: link.raw ?? void 0
|
|
3406
|
+
nodeIds,
|
|
3407
|
+
message,
|
|
3408
|
+
data: {
|
|
3409
|
+
normalizedTrigger: normalized,
|
|
3410
|
+
invocationTargets,
|
|
3411
|
+
advertiserPaths
|
|
3412
|
+
}
|
|
3413
3413
|
};
|
|
3414
3414
|
}
|
|
3415
3415
|
|
|
@@ -3449,7 +3449,7 @@ var asciiFormatter = {
|
|
|
3449
3449
|
kind: "formatter",
|
|
3450
3450
|
formatId: ID24,
|
|
3451
3451
|
version: "1.0.0",
|
|
3452
|
-
description: "Renders the scan as plain text
|
|
3452
|
+
description: "Renders the scan as plain text in three sections: nodes (grouped by kind), arrows, and issues. Used by `sm scan --format ascii`.",
|
|
3453
3453
|
// ASCII tree formatter, header + per-kind sections + per-issue
|
|
3454
3454
|
// section. Each section iterates and renders; splitting per section
|
|
3455
3455
|
// would multiply the for-loop boilerplate.
|
|
@@ -3548,7 +3548,7 @@ var jsonFormatter = {
|
|
|
3548
3548
|
pluginId: CORE_PLUGIN_ID,
|
|
3549
3549
|
kind: "formatter",
|
|
3550
3550
|
version: "1.0.0",
|
|
3551
|
-
description: "Renders the persisted scan as JSON (conforms to `scan-result.schema.json`
|
|
3551
|
+
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`.",
|
|
3552
3552
|
formatId: ID25,
|
|
3553
3553
|
format(ctx) {
|
|
3554
3554
|
if (ctx.scanResult !== void 0) {
|
|
@@ -3687,14 +3687,14 @@ function resolveSpecRoot2() {
|
|
|
3687
3687
|
}
|
|
3688
3688
|
}
|
|
3689
3689
|
|
|
3690
|
-
// plugins/core/actions/bump/index.ts
|
|
3691
|
-
var ID26 = "bump";
|
|
3692
|
-
var
|
|
3690
|
+
// plugins/core/actions/node-bump/index.ts
|
|
3691
|
+
var ID26 = "node-bump";
|
|
3692
|
+
var nodeBumpAction = {
|
|
3693
3693
|
id: ID26,
|
|
3694
3694
|
pluginId: CORE_PLUGIN_ID,
|
|
3695
3695
|
kind: "action",
|
|
3696
3696
|
version: "1.0.0",
|
|
3697
|
-
description: "Marks a node as updated: bumps version
|
|
3697
|
+
description: "Marks a node as updated: bumps `annotations.version`, refreshes sidecar hashes, and records the timestamp.",
|
|
3698
3698
|
mode: "deterministic",
|
|
3699
3699
|
// The runtime contract uses generic <TInput, TReport>; bump narrows
|
|
3700
3700
|
// both. The cast is the standard pattern for built-ins that want
|
|
@@ -3748,14 +3748,14 @@ function pickCurrentVersion(overlay) {
|
|
|
3748
3748
|
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
3749
3749
|
}
|
|
3750
3750
|
|
|
3751
|
-
// plugins/core/actions/
|
|
3752
|
-
var ID27 = "
|
|
3753
|
-
var
|
|
3751
|
+
// plugins/core/actions/node-supersede/index.ts
|
|
3752
|
+
var ID27 = "node-supersede";
|
|
3753
|
+
var nodeSupersedeAction = {
|
|
3754
3754
|
id: ID27,
|
|
3755
3755
|
pluginId: CORE_PLUGIN_ID,
|
|
3756
3756
|
kind: "action",
|
|
3757
3757
|
version: "0.0.0",
|
|
3758
|
-
description: "Declares the current node as superseded by another (writes `supersededBy` to the sidecar).
|
|
3758
|
+
description: "Declares the current node as superseded by another (writes `supersededBy` to the sidecar).",
|
|
3759
3759
|
mode: "deterministic",
|
|
3760
3760
|
invoke(_input, _ctx) {
|
|
3761
3761
|
const report = { ok: true, noop: true };
|
|
@@ -3861,7 +3861,7 @@ var UPDATE_CHECK_TEXTS = {
|
|
|
3861
3861
|
// package.json
|
|
3862
3862
|
var package_default = {
|
|
3863
3863
|
name: "@skill-map/cli",
|
|
3864
|
-
version: "0.
|
|
3864
|
+
version: "0.39.0",
|
|
3865
3865
|
description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
|
|
3866
3866
|
license: "MIT",
|
|
3867
3867
|
type: "module",
|
|
@@ -4279,7 +4279,7 @@ var updateCheckHook = {
|
|
|
4279
4279
|
pluginId: CORE_PLUGIN_ID,
|
|
4280
4280
|
kind: "hook",
|
|
4281
4281
|
version: "1.0.0",
|
|
4282
|
-
description: "Checks daily for a newer skill-map version on npm. Shows an `update available` banner when one is found.",
|
|
4282
|
+
description: "Checks daily for a newer `skill-map` version on npm. Shows an `update available` banner when one is found.",
|
|
4283
4283
|
triggers: ["boot"],
|
|
4284
4284
|
async on(ctx) {
|
|
4285
4285
|
const payload = ctx.event.data ?? {};
|
|
@@ -4296,7 +4296,7 @@ var updateCheckHook = {
|
|
|
4296
4296
|
// plugins/built-ins.ts
|
|
4297
4297
|
var claudeProvider2 = { ...claudeProvider, pluginId: "claude" };
|
|
4298
4298
|
var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "claude" };
|
|
4299
|
-
var
|
|
4299
|
+
var slashCommandExtractor2 = { ...slashCommandExtractor, pluginId: "claude" };
|
|
4300
4300
|
var antigravityProvider2 = { ...antigravityProvider, pluginId: "antigravity" };
|
|
4301
4301
|
var openaiProvider2 = { ...openaiProvider, pluginId: "openai" };
|
|
4302
4302
|
var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills" };
|
|
@@ -4305,43 +4305,43 @@ var annotationsExtractor2 = { ...annotationsExtractor, pluginId: "core" };
|
|
|
4305
4305
|
var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core" };
|
|
4306
4306
|
var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core" };
|
|
4307
4307
|
var mcpToolsExtractor2 = { ...mcpToolsExtractor, pluginId: "core" };
|
|
4308
|
-
var
|
|
4308
|
+
var toolsCounterExtractor2 = { ...toolsCounterExtractor, pluginId: "core" };
|
|
4309
|
+
var annotationFieldUnknownAnalyzer2 = { ...annotationFieldUnknownAnalyzer, pluginId: "core" };
|
|
4309
4310
|
var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core" };
|
|
4310
4311
|
var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core" };
|
|
4311
|
-
var brokenRefAnalyzer2 = { ...brokenRefAnalyzer, pluginId: "core" };
|
|
4312
4312
|
var contributionOrphanAnalyzer2 = { ...contributionOrphanAnalyzer, pluginId: "core" };
|
|
4313
|
-
var
|
|
4313
|
+
var jobFileOrphanAnalyzer2 = { ...jobFileOrphanAnalyzer, pluginId: "core" };
|
|
4314
4314
|
var linkConflictAnalyzer2 = { ...linkConflictAnalyzer, pluginId: "core" };
|
|
4315
|
-
var
|
|
4316
|
-
var
|
|
4317
|
-
var
|
|
4318
|
-
var
|
|
4315
|
+
var linkCounterAnalyzer2 = { ...linkCounterAnalyzer, pluginId: "core" };
|
|
4316
|
+
var linkSelfLoopAnalyzer2 = { ...linkSelfLoopAnalyzer, pluginId: "core" };
|
|
4317
|
+
var nameReservedAnalyzer2 = { ...nameReservedAnalyzer, pluginId: "core" };
|
|
4318
|
+
var nodeStabilityAnalyzer2 = { ...nodeStabilityAnalyzer, pluginId: "core" };
|
|
4319
|
+
var nodeSupersededAnalyzer2 = { ...nodeSupersededAnalyzer, pluginId: "core" };
|
|
4320
|
+
var referenceBrokenAnalyzer2 = { ...referenceBrokenAnalyzer, pluginId: "core" };
|
|
4321
|
+
var referenceRedundantAnalyzer2 = { ...referenceRedundantAnalyzer, pluginId: "core" };
|
|
4322
|
+
var schemaViolationAnalyzer2 = { ...schemaViolationAnalyzer, pluginId: "core" };
|
|
4319
4323
|
var signalCollisionAnalyzer2 = { ...signalCollisionAnalyzer, pluginId: "core" };
|
|
4320
|
-
var stabilityAnalyzer2 = { ...stabilityAnalyzer, pluginId: "core" };
|
|
4321
|
-
var supersededAnalyzer2 = { ...supersededAnalyzer, pluginId: "core" };
|
|
4322
4324
|
var triggerCollisionAnalyzer2 = { ...triggerCollisionAnalyzer, pluginId: "core" };
|
|
4323
|
-
var unknownFieldAnalyzer2 = { ...unknownFieldAnalyzer, pluginId: "core" };
|
|
4324
|
-
var validateAllAnalyzer2 = { ...validateAllAnalyzer, pluginId: "core" };
|
|
4325
4325
|
var asciiFormatter2 = { ...asciiFormatter, pluginId: "core" };
|
|
4326
4326
|
var jsonFormatter2 = { ...jsonFormatter, pluginId: "core" };
|
|
4327
|
-
var
|
|
4328
|
-
var
|
|
4327
|
+
var nodeBumpAction2 = { ...nodeBumpAction, pluginId: "core" };
|
|
4328
|
+
var nodeSupersedeAction2 = { ...nodeSupersedeAction, pluginId: "core" };
|
|
4329
4329
|
var updateCheckHook2 = { ...updateCheckHook, pluginId: "core" };
|
|
4330
4330
|
var builtInBundles = [
|
|
4331
4331
|
{
|
|
4332
4332
|
id: "claude",
|
|
4333
4333
|
granularity: "bundle",
|
|
4334
|
-
description: "Claude Code platform integration. Classifies files under `.claude/{agents,commands,skills}` and
|
|
4334
|
+
description: "Claude Code platform integration. Classifies files under `.claude/{agents,commands,skills}` and detects Claude-flavored slash commands and at-directives in their bodies.",
|
|
4335
4335
|
extensions: [
|
|
4336
4336
|
claudeProvider2,
|
|
4337
4337
|
atDirectiveExtractor2,
|
|
4338
|
-
|
|
4338
|
+
slashCommandExtractor2
|
|
4339
4339
|
]
|
|
4340
4340
|
},
|
|
4341
4341
|
{
|
|
4342
4342
|
id: "antigravity",
|
|
4343
4343
|
granularity: "bundle",
|
|
4344
|
-
description: "Google Antigravity CLI platform integration (
|
|
4344
|
+
description: "Google Antigravity CLI platform integration (replaces the retired Gemini CLI). Antigravity adopted the open-standard `.agents/` layout, so skills are classified by the neutral `agent-skills` provider; this plugin contributes the Antigravity runtime identity and a seed list of reserved built-in names.",
|
|
4345
4345
|
extensions: [
|
|
4346
4346
|
antigravityProvider2
|
|
4347
4347
|
]
|
|
@@ -4349,7 +4349,7 @@ var builtInBundles = [
|
|
|
4349
4349
|
{
|
|
4350
4350
|
id: "openai",
|
|
4351
4351
|
granularity: "bundle",
|
|
4352
|
-
description: "OpenAI Codex CLI platform integration. Classifies TOML sub-agent definitions under `.codex/agents/*.toml
|
|
4352
|
+
description: "OpenAI Codex CLI platform integration. Classifies TOML sub-agent definitions under `.codex/agents/*.toml`.",
|
|
4353
4353
|
extensions: [
|
|
4354
4354
|
openaiProvider2
|
|
4355
4355
|
]
|
|
@@ -4357,7 +4357,7 @@ var builtInBundles = [
|
|
|
4357
4357
|
{
|
|
4358
4358
|
id: "agent-skills",
|
|
4359
4359
|
granularity: "bundle",
|
|
4360
|
-
description: "Agent Skills
|
|
4360
|
+
description: "Open-standard Agent Skills layout. Classifies skills under the vendor-neutral path `.agents/skills/<name>/SKILL.md` (adopted by Anthropic, OpenAI, Google). See agentskills.io.",
|
|
4361
4361
|
extensions: [
|
|
4362
4362
|
agentSkillsProvider2
|
|
4363
4363
|
]
|
|
@@ -4365,34 +4365,34 @@ var builtInBundles = [
|
|
|
4365
4365
|
{
|
|
4366
4366
|
id: "core",
|
|
4367
4367
|
granularity: "extension",
|
|
4368
|
-
description: "Core extensions shared across providers: extractors, analyzers,
|
|
4368
|
+
description: "Core extensions shared across providers: parsers, extractors, analyzers, actions, hooks, formatters, and the universal `.md` fallback provider.",
|
|
4369
4369
|
extensions: [
|
|
4370
4370
|
coreMarkdownProvider2,
|
|
4371
4371
|
annotationsExtractor2,
|
|
4372
4372
|
externalUrlCounterExtractor2,
|
|
4373
4373
|
markdownLinkExtractor2,
|
|
4374
4374
|
mcpToolsExtractor2,
|
|
4375
|
-
|
|
4375
|
+
toolsCounterExtractor2,
|
|
4376
|
+
annotationFieldUnknownAnalyzer2,
|
|
4376
4377
|
annotationOrphanAnalyzer2,
|
|
4377
4378
|
annotationStaleAnalyzer2,
|
|
4378
|
-
brokenRefAnalyzer2,
|
|
4379
4379
|
contributionOrphanAnalyzer2,
|
|
4380
|
-
|
|
4380
|
+
jobFileOrphanAnalyzer2,
|
|
4381
4381
|
linkConflictAnalyzer2,
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4382
|
+
linkCounterAnalyzer2,
|
|
4383
|
+
linkSelfLoopAnalyzer2,
|
|
4384
|
+
nameReservedAnalyzer2,
|
|
4385
|
+
nodeStabilityAnalyzer2,
|
|
4386
|
+
nodeSupersededAnalyzer2,
|
|
4387
|
+
referenceBrokenAnalyzer2,
|
|
4388
|
+
referenceRedundantAnalyzer2,
|
|
4389
|
+
schemaViolationAnalyzer2,
|
|
4386
4390
|
signalCollisionAnalyzer2,
|
|
4387
|
-
stabilityAnalyzer2,
|
|
4388
|
-
supersededAnalyzer2,
|
|
4389
4391
|
triggerCollisionAnalyzer2,
|
|
4390
|
-
unknownFieldAnalyzer2,
|
|
4391
|
-
validateAllAnalyzer2,
|
|
4392
4392
|
asciiFormatter2,
|
|
4393
4393
|
jsonFormatter2,
|
|
4394
|
-
|
|
4395
|
-
|
|
4394
|
+
nodeBumpAction2,
|
|
4395
|
+
nodeSupersedeAction2,
|
|
4396
4396
|
updateCheckHook2
|
|
4397
4397
|
]
|
|
4398
4398
|
}
|
|
@@ -4519,21 +4519,42 @@ function localTimeFromIso(iso) {
|
|
|
4519
4519
|
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
4520
4520
|
return `${hh}:${mm}:${ss}`;
|
|
4521
4521
|
}
|
|
4522
|
-
|
|
4522
|
+
function paintLevelPrefix(level, ansi) {
|
|
4523
|
+
const label = level.toUpperCase().padEnd(5);
|
|
4524
|
+
switch (level) {
|
|
4525
|
+
case "error":
|
|
4526
|
+
return `${ansi.red("\u2715")} ${ansi.red(label)}`;
|
|
4527
|
+
case "warn":
|
|
4528
|
+
return `${ansi.yellow("\u26A0")} ${ansi.yellow(label)}`;
|
|
4529
|
+
case "info":
|
|
4530
|
+
return `${ansi.cyan("\u2139")} ${ansi.cyan(label)}`;
|
|
4531
|
+
case "debug":
|
|
4532
|
+
case "trace":
|
|
4533
|
+
return `${ansi.dim("\xB7")} ${ansi.dim(label)}`;
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
var defaultFormat = (record, ansi) => {
|
|
4523
4537
|
const time = localTimeFromIso(record.timestamp);
|
|
4524
|
-
const
|
|
4525
|
-
const ctx = record.context && Object.keys(record.context).length > 0 ? ` | ${JSON.stringify(record.context)}` : "";
|
|
4526
|
-
return `${time}
|
|
4538
|
+
const prefix = paintLevelPrefix(record.level, ansi);
|
|
4539
|
+
const ctx = record.context && Object.keys(record.context).length > 0 ? ` ${ansi.dim("|")} ${ansi.dim(JSON.stringify(record.context))}` : "";
|
|
4540
|
+
return `${ansi.dim(time)} ${prefix} ${record.message}${ctx}
|
|
4527
4541
|
`;
|
|
4528
4542
|
};
|
|
4529
4543
|
var Logger = class {
|
|
4530
4544
|
#level;
|
|
4531
4545
|
#stream;
|
|
4532
4546
|
#format;
|
|
4547
|
+
#ansi;
|
|
4533
4548
|
constructor(opts) {
|
|
4534
4549
|
this.#level = opts.level;
|
|
4535
4550
|
this.#stream = opts.stream;
|
|
4536
4551
|
this.#format = opts.format ?? defaultFormat;
|
|
4552
|
+
const streamTty = opts.stream;
|
|
4553
|
+
this.#ansi = ansiFor({
|
|
4554
|
+
isTTY: streamTty.isTTY === true,
|
|
4555
|
+
noColorFlag: opts.noColorFlag === true,
|
|
4556
|
+
...opts.env !== void 0 ? { env: opts.env } : {}
|
|
4557
|
+
});
|
|
4537
4558
|
}
|
|
4538
4559
|
setLevel(level) {
|
|
4539
4560
|
this.#level = level;
|
|
@@ -4564,7 +4585,7 @@ var Logger = class {
|
|
|
4564
4585
|
message,
|
|
4565
4586
|
...context !== void 0 ? { context } : {}
|
|
4566
4587
|
};
|
|
4567
|
-
this.#stream.write(this.#format(record));
|
|
4588
|
+
this.#stream.write(this.#format(record, this.#ansi));
|
|
4568
4589
|
}
|
|
4569
4590
|
};
|
|
4570
4591
|
function resolveLogLevel(opts) {
|
|
@@ -7246,9 +7267,9 @@ async function sweepPerTupleContributions(trx, contributions, freshlyRunTuples)
|
|
|
7246
7267
|
const bufferKeys = buildContributionsBufferKeys(contributions);
|
|
7247
7268
|
const tuplesByPluginExt = groupFreshlyRunTuplesByPluginExt(freshlyRunTuples);
|
|
7248
7269
|
for (const [pe, nodes] of tuplesByPluginExt) {
|
|
7249
|
-
const
|
|
7250
|
-
if (
|
|
7251
|
-
await deleteStaleTupleRows(trx, pe.slice(0,
|
|
7270
|
+
const sep8 = pe.indexOf("\0");
|
|
7271
|
+
if (sep8 < 0) continue;
|
|
7272
|
+
await deleteStaleTupleRows(trx, pe.slice(0, sep8), pe.slice(sep8 + 1), [...nodes], bufferKeys);
|
|
7252
7273
|
}
|
|
7253
7274
|
}
|
|
7254
7275
|
function buildContributionsBufferKeys(contributions) {
|
|
@@ -8406,12 +8427,12 @@ function planOne(node, options) {
|
|
|
8406
8427
|
};
|
|
8407
8428
|
}
|
|
8408
8429
|
function invokeBumpFor(node, absPath, force) {
|
|
8409
|
-
if (!
|
|
8430
|
+
if (!nodeBumpAction.invoke) {
|
|
8410
8431
|
throw new Error("built-in bump action is missing its invoke()");
|
|
8411
8432
|
}
|
|
8412
8433
|
const input = {};
|
|
8413
8434
|
if (force) input.force = true;
|
|
8414
|
-
return
|
|
8435
|
+
return nodeBumpAction.invoke(input, {
|
|
8415
8436
|
node,
|
|
8416
8437
|
nodeAbsolutePath: absPath,
|
|
8417
8438
|
invoker: "cli",
|
|
@@ -8427,7 +8448,7 @@ var BumpCommand = class extends SmCommand {
|
|
|
8427
8448
|
category: "Actions",
|
|
8428
8449
|
description: "Bump a node's sidecar (`<basename>.sm`): increment annotations.version, refresh hashes, stamp audit.",
|
|
8429
8450
|
details: `
|
|
8430
|
-
Wraps the built-in deterministic \`core/bump\` Action. Single-node
|
|
8451
|
+
Wraps the built-in deterministic \`core/node-bump\` Action. Single-node
|
|
8431
8452
|
mode bumps one path; \`--pending\` walks every node whose sidecar
|
|
8432
8453
|
overlay reports drift and bumps them all.
|
|
8433
8454
|
|
|
@@ -9823,14 +9844,14 @@ var LOCKED_PLUGIN_IDS = /* @__PURE__ */ new Set([
|
|
|
9823
9844
|
// unreachable from CLI / BFF / UI. Re-evaluate if a third-party ever
|
|
9824
9845
|
// ships a competing supersession extractor.
|
|
9825
9846
|
"core/annotations",
|
|
9826
|
-
// `core/
|
|
9847
|
+
// `core/schema-violation` validates every scanned Node against
|
|
9827
9848
|
// `node.schema.json` and every Link against `link.schema.json` (the
|
|
9828
9849
|
// authoritative @skill-map/spec). Disabling it makes the system
|
|
9829
9850
|
// persist non-conformant content silently, breaking the spec
|
|
9830
9851
|
// invariant "what reaches the DB conforms to the spec". The check is
|
|
9831
9852
|
// foundational, not advisory; lock it on so the guarantee holds
|
|
9832
9853
|
// regardless of user / DB / settings hand-edits.
|
|
9833
|
-
"core/
|
|
9854
|
+
"core/schema-violation",
|
|
9834
9855
|
// `core/ascii` is the only built-in Formatter today and the default
|
|
9835
9856
|
// for `sm graph` (`--format ascii`). Disabling it breaks the verb
|
|
9836
9857
|
// entirely (`composeFormatters` returns the empty list, the CLI
|
|
@@ -9866,7 +9887,8 @@ function isBuiltInExtensionEnabled(bundle, ext, resolveEnabled) {
|
|
|
9866
9887
|
}
|
|
9867
9888
|
function isBundleEntryEnabled(bundle, extId, resolveEnabled) {
|
|
9868
9889
|
if (bundle.granularity === "bundle") {
|
|
9869
|
-
|
|
9890
|
+
if (!resolveEnabled(bundle.id)) return false;
|
|
9891
|
+
return resolveEnabled(qualifiedExtensionId(bundle.id, extId));
|
|
9870
9892
|
}
|
|
9871
9893
|
return resolveEnabled(qualifiedExtensionId(bundle.id, extId));
|
|
9872
9894
|
}
|
|
@@ -9879,7 +9901,10 @@ function buildGranularityMap(discovered) {
|
|
|
9879
9901
|
}
|
|
9880
9902
|
function isPluginExtensionEnabled(ext, granularityMap, resolveEnabled) {
|
|
9881
9903
|
const granularity = granularityMap.get(ext.pluginId) ?? "bundle";
|
|
9882
|
-
if (granularity === "bundle")
|
|
9904
|
+
if (granularity === "bundle") {
|
|
9905
|
+
if (!resolveEnabled(ext.pluginId)) return false;
|
|
9906
|
+
return resolveEnabled(qualifiedExtensionId(ext.pluginId, ext.id));
|
|
9907
|
+
}
|
|
9883
9908
|
return resolveEnabled(qualifiedExtensionId(ext.pluginId, ext.id));
|
|
9884
9909
|
}
|
|
9885
9910
|
async function buildEnabledResolver(ctx) {
|
|
@@ -10550,7 +10575,7 @@ var CheckCommand = class extends SmCommand {
|
|
|
10550
10575
|
["Print every current issue", "$0 check"],
|
|
10551
10576
|
["Machine-readable issue list", "$0 check --json"],
|
|
10552
10577
|
["Restrict to a single node", "$0 check -n .claude/agents/architect.md"],
|
|
10553
|
-
["Restrict to specific rules", "$0 check --analyzers core/broken
|
|
10578
|
+
["Restrict to specific rules", "$0 check --analyzers core/reference-broken,core/schema-violation"],
|
|
10554
10579
|
["Opt in to probabilistic analyzers (stub until Step 10)", "$0 check --include-prob"],
|
|
10555
10580
|
["Use a non-default DB file", "$0 check --db /path/to/skill-map.db"]
|
|
10556
10581
|
]
|
|
@@ -14297,8 +14322,8 @@ function computePlannedHookContent(existing) {
|
|
|
14297
14322
|
if (existing.includes(SKILL_MAP_MARKER)) {
|
|
14298
14323
|
return { kind: "already-installed", content: existing };
|
|
14299
14324
|
}
|
|
14300
|
-
const
|
|
14301
|
-
return { kind: "chained", content: existing +
|
|
14325
|
+
const sep8 = existing.endsWith("\n") ? "" : "\n";
|
|
14326
|
+
return { kind: "chained", content: existing + sep8 + "\n" + SKILL_MAP_BLOCK };
|
|
14302
14327
|
}
|
|
14303
14328
|
async function ensureExecutableBit(path) {
|
|
14304
14329
|
const mode = (await stat2(path)).mode;
|
|
@@ -14673,7 +14698,8 @@ function recomputeLinkCounts(nodes, links) {
|
|
|
14673
14698
|
for (const link of links) {
|
|
14674
14699
|
const source = byPath3.get(link.source);
|
|
14675
14700
|
if (source) source.linksOutCount += 1;
|
|
14676
|
-
const
|
|
14701
|
+
const targetKey = link.resolvedTarget ?? link.target;
|
|
14702
|
+
const target = byPath3.get(targetKey);
|
|
14677
14703
|
if (target) target.linksInCount += 1;
|
|
14678
14704
|
}
|
|
14679
14705
|
}
|
|
@@ -16409,7 +16435,7 @@ var SCAN_RUNNER_TEXTS = {
|
|
|
16409
16435
|
priorSchemaValidationFailed: "prior scan-result loaded from DB failed schema validation: {{errors}}. Run `sm db backup` then re-scan without --strict to rebuild from disk.",
|
|
16410
16436
|
/**
|
|
16411
16437
|
* Reference-paths walker hit `REFERENCE_WALK_MAX_FILES` and stopped
|
|
16412
|
-
* early. The set may be incomplete for link validation; `core/broken
|
|
16438
|
+
* early. The set may be incomplete for link validation; `core/reference-broken`
|
|
16413
16439
|
* still works against whatever made it in.
|
|
16414
16440
|
*/
|
|
16415
16441
|
referenceWalkTruncated: "scan.referencePaths: walker truncated at the 50000-file safety cap. Some link targets may flag as broken even though they exist on disk. Trim the configured paths to dirs you actually need to validate against.",
|
|
@@ -18975,6 +19001,27 @@ var PLUGINS_TEXTS = {
|
|
|
18975
19001
|
slotsListTipText: "Tip: full spec at spec/view-slots.md and spec/input-types.md."
|
|
18976
19002
|
};
|
|
18977
19003
|
|
|
19004
|
+
// plugins/presentation-order.ts
|
|
19005
|
+
var BUILT_IN_BUNDLE_PRESENTATION_ORDER = [
|
|
19006
|
+
"core",
|
|
19007
|
+
"claude",
|
|
19008
|
+
"antigravity",
|
|
19009
|
+
"openai",
|
|
19010
|
+
"agent-skills"
|
|
19011
|
+
];
|
|
19012
|
+
function sortBundlesForPresentation(bundles) {
|
|
19013
|
+
const orderIndex = (id) => {
|
|
19014
|
+
const idx = BUILT_IN_BUNDLE_PRESENTATION_ORDER.indexOf(id);
|
|
19015
|
+
return idx >= 0 ? idx : BUILT_IN_BUNDLE_PRESENTATION_ORDER.length;
|
|
19016
|
+
};
|
|
19017
|
+
return [...bundles].sort((a, b) => {
|
|
19018
|
+
const ai = orderIndex(a.id);
|
|
19019
|
+
const bi = orderIndex(b.id);
|
|
19020
|
+
if (ai !== bi) return ai - bi;
|
|
19021
|
+
return a.id.localeCompare(b.id);
|
|
19022
|
+
});
|
|
19023
|
+
}
|
|
19024
|
+
|
|
18978
19025
|
// cli/commands/plugins/shared.ts
|
|
18979
19026
|
import { resolve as resolve32 } from "path";
|
|
18980
19027
|
function resolveSearchPaths2(opts, cwd) {
|
|
@@ -19004,7 +19051,7 @@ async function loadAll(opts) {
|
|
|
19004
19051
|
return loader.discoverAndLoadAll();
|
|
19005
19052
|
}
|
|
19006
19053
|
function builtInRows(resolveEnabled) {
|
|
19007
|
-
return builtInBundles.map((bundle) => {
|
|
19054
|
+
return sortBundlesForPresentation(builtInBundles).map((bundle) => {
|
|
19008
19055
|
const bundleEnabled = resolveEnabled(bundle.id);
|
|
19009
19056
|
const extensions = bundle.extensions.map((ext) => extensionRowFromBuiltIn(ext, bundle, bundleEnabled, resolveEnabled));
|
|
19010
19057
|
const manifestSummary = bundle.extensions.map((ext) => `${ext.kind}:${qualifiedExtensionId(bundle.id, ext.id)}@${ext.version}`).join(", ");
|
|
@@ -19019,11 +19066,12 @@ function builtInRows(resolveEnabled) {
|
|
|
19019
19066
|
});
|
|
19020
19067
|
}
|
|
19021
19068
|
function extensionRowFromBuiltIn(ext, bundle, bundleEnabled, resolveEnabled) {
|
|
19069
|
+
const qualifiedEnabled = resolveEnabled(qualifiedExtensionId(bundle.id, ext.id));
|
|
19022
19070
|
const row = {
|
|
19023
19071
|
id: ext.id,
|
|
19024
19072
|
kind: ext.kind,
|
|
19025
19073
|
version: ext.version,
|
|
19026
|
-
enabled: bundle.granularity === "bundle" ? bundleEnabled :
|
|
19074
|
+
enabled: bundle.granularity === "bundle" ? bundleEnabled && qualifiedEnabled : qualifiedEnabled,
|
|
19027
19075
|
description: ext.description ?? ""
|
|
19028
19076
|
};
|
|
19029
19077
|
if (ext.entry !== void 0) row.entry = ext.entry;
|
|
@@ -19139,10 +19187,10 @@ function pluginToListRow(p) {
|
|
|
19139
19187
|
}
|
|
19140
19188
|
function wrapNames(names, indent, maxWidth) {
|
|
19141
19189
|
const out = [];
|
|
19142
|
-
const
|
|
19190
|
+
const sep8 = ", ";
|
|
19143
19191
|
let current = "";
|
|
19144
19192
|
for (const name of names) {
|
|
19145
|
-
const candidate = current === "" ? name : `${current}${
|
|
19193
|
+
const candidate = current === "" ? name : `${current}${sep8}${name}`;
|
|
19146
19194
|
if (indent.length + candidate.length > maxWidth && current !== "") {
|
|
19147
19195
|
out.push(`${current},`);
|
|
19148
19196
|
current = name;
|
|
@@ -19710,10 +19758,13 @@ function extensionInstance(ext) {
|
|
|
19710
19758
|
}
|
|
19711
19759
|
function collectKnownKinds(plugins) {
|
|
19712
19760
|
const known = /* @__PURE__ */ new Set();
|
|
19713
|
-
forEachProviderInstance(plugins, ({ instance }) => {
|
|
19761
|
+
forEachProviderInstance(plugins, ({ pluginId, instance }) => {
|
|
19714
19762
|
const map = instance["kinds"];
|
|
19715
19763
|
if (map === null || typeof map !== "object") return;
|
|
19716
|
-
for (const k of Object.keys(map))
|
|
19764
|
+
for (const k of Object.keys(map)) {
|
|
19765
|
+
known.add(k);
|
|
19766
|
+
known.add(qualifiedExtensionId(pluginId, k));
|
|
19767
|
+
}
|
|
19717
19768
|
});
|
|
19718
19769
|
return known;
|
|
19719
19770
|
}
|
|
@@ -19962,7 +20013,7 @@ var TogglePluginsBase = class extends SmCommand {
|
|
|
19962
20013
|
* the plugin's `scan_contributions` rows immediately (matches the
|
|
19963
20014
|
* BFF route, see `server/routes/plugins.ts:applyChangeToAdapter`).
|
|
19964
20015
|
* `targets` carries either a bare bundle id (e.g. `claude`) or a
|
|
19965
|
-
* qualified `<bundle>/<ext>` (e.g. `core/slash`); the split mirrors
|
|
20016
|
+
* qualified `<bundle>/<ext>` (e.g. `core/slash-command`); the split mirrors
|
|
19966
20017
|
* how the catalog sweep groups rows.
|
|
19967
20018
|
*/
|
|
19968
20019
|
async #persistTargets(targets, enabled) {
|
|
@@ -21919,6 +21970,18 @@ import { spawn as spawn2 } from "child_process";
|
|
|
21919
21970
|
import { existsSync as existsSync28 } from "fs";
|
|
21920
21971
|
import { Command as Command33, Option as Option31 } from "clipanion";
|
|
21921
21972
|
|
|
21973
|
+
// kernel/util/dev-mode.ts
|
|
21974
|
+
import { sep as sep6 } from "path";
|
|
21975
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
21976
|
+
var SELF_PATH = fileURLToPath5(import.meta.url);
|
|
21977
|
+
var IS_DEV_BUILD = isDevBuildFromPath(SELF_PATH, sep6);
|
|
21978
|
+
function isDevBuildFromPath(filePath, separator = sep6) {
|
|
21979
|
+
return !filePath.includes(`${separator}node_modules${separator}`);
|
|
21980
|
+
}
|
|
21981
|
+
function isDevBuild() {
|
|
21982
|
+
return IS_DEV_BUILD;
|
|
21983
|
+
}
|
|
21984
|
+
|
|
21922
21985
|
// cli/util/browser-launch.ts
|
|
21923
21986
|
function validateBrowserUrl(url) {
|
|
21924
21987
|
if (typeof url !== "string" || url.length === 0) return false;
|
|
@@ -22632,6 +22695,7 @@ function contentTypeFor(format) {
|
|
|
22632
22695
|
import { existsSync as existsSync23 } from "fs";
|
|
22633
22696
|
var FALLBACK_SCHEMA_VERSION = "1";
|
|
22634
22697
|
function buildHealth(deps) {
|
|
22698
|
+
const dev = isDevBuild();
|
|
22635
22699
|
return {
|
|
22636
22700
|
ok: true,
|
|
22637
22701
|
schemaVersion: FALLBACK_SCHEMA_VERSION,
|
|
@@ -22639,7 +22703,10 @@ function buildHealth(deps) {
|
|
|
22639
22703
|
implVersion: VERSION,
|
|
22640
22704
|
db: existsSync23(deps.dbPath) ? "present" : "missing",
|
|
22641
22705
|
cwd: deps.cwd,
|
|
22642
|
-
dbPath: deps.dbPath
|
|
22706
|
+
dbPath: deps.dbPath,
|
|
22707
|
+
// Only emit when truthy so a published install keeps the wire
|
|
22708
|
+
// shape lean and consumers branch on presence alone.
|
|
22709
|
+
...dev ? { dev: true } : {}
|
|
22643
22710
|
};
|
|
22644
22711
|
}
|
|
22645
22712
|
var cachedSpecVersion = null;
|
|
@@ -22764,13 +22831,13 @@ import { HTTPException as HTTPException6 } from "hono/http-exception";
|
|
|
22764
22831
|
// server/node-body.ts
|
|
22765
22832
|
import { constants as fsConstants2 } from "fs";
|
|
22766
22833
|
import { open } from "fs/promises";
|
|
22767
|
-
import { isAbsolute as isAbsolute10, resolve as resolvePath2, relative as relativePath, sep as
|
|
22834
|
+
import { isAbsolute as isAbsolute10, resolve as resolvePath2, relative as relativePath, sep as sep7 } from "path";
|
|
22768
22835
|
async function readNodeBody(cwd, relPath) {
|
|
22769
22836
|
if (isAbsolute10(relPath)) return null;
|
|
22770
22837
|
const absRoot = resolvePath2(cwd);
|
|
22771
22838
|
const absFile = resolvePath2(absRoot, relPath);
|
|
22772
22839
|
const rel = relativePath(absRoot, absFile);
|
|
22773
|
-
if (rel.startsWith("..") || rel.startsWith(
|
|
22840
|
+
if (rel.startsWith("..") || rel.startsWith(sep7) || rel.length === 0) {
|
|
22774
22841
|
return null;
|
|
22775
22842
|
}
|
|
22776
22843
|
let raw;
|
|
@@ -23210,7 +23277,7 @@ function listItems(deps, resolveEnabled) {
|
|
|
23210
23277
|
];
|
|
23211
23278
|
}
|
|
23212
23279
|
function buildBuiltInItems(resolveEnabled) {
|
|
23213
|
-
return builtInBundles.map((bundle) => {
|
|
23280
|
+
return sortBundlesForPresentation(builtInBundles).map((bundle) => {
|
|
23214
23281
|
const bundleEnabled = resolveEnabled(bundle.id);
|
|
23215
23282
|
const bundleLocked = isPluginLocked(bundle.id);
|
|
23216
23283
|
const extensions = bundle.extensions.map((ext) => {
|
|
@@ -24365,12 +24432,12 @@ async function loadNode(deps, nodePath) {
|
|
|
24365
24432
|
return node;
|
|
24366
24433
|
}
|
|
24367
24434
|
function invokeBump2(node, absPath, body) {
|
|
24368
|
-
if (!
|
|
24435
|
+
if (!nodeBumpAction.invoke) {
|
|
24369
24436
|
throw new HTTPException14(500, { message: SERVER_TEXTS.sidecarBumpInvokeMissing });
|
|
24370
24437
|
}
|
|
24371
24438
|
const input = {};
|
|
24372
24439
|
if (body.force === true) input.force = true;
|
|
24373
|
-
return
|
|
24440
|
+
return nodeBumpAction.invoke(input, {
|
|
24374
24441
|
node,
|
|
24375
24442
|
nodeAbsolutePath: absPath,
|
|
24376
24443
|
invoker: "ui",
|
|
@@ -25041,7 +25108,7 @@ function validateNoUi(noUi, uiDist) {
|
|
|
25041
25108
|
// server/paths.ts
|
|
25042
25109
|
import { existsSync as existsSync27, statSync as statSync10 } from "fs";
|
|
25043
25110
|
import { dirname as dirname18, isAbsolute as isAbsolute11, join as join20, resolve as resolve37 } from "path";
|
|
25044
|
-
import { fileURLToPath as
|
|
25111
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
25045
25112
|
var DEFAULT_UI_REL = join20("ui", "dist", "ui", "browser");
|
|
25046
25113
|
var PACKAGE_UI_REL = "ui";
|
|
25047
25114
|
var INDEX_HTML2 = "index.html";
|
|
@@ -25065,7 +25132,7 @@ function isUiBundleDir(path) {
|
|
|
25065
25132
|
function resolvePackageBundledUi() {
|
|
25066
25133
|
let here;
|
|
25067
25134
|
try {
|
|
25068
|
-
here = dirname18(
|
|
25135
|
+
here = dirname18(fileURLToPath6(import.meta.url));
|
|
25069
25136
|
} catch {
|
|
25070
25137
|
return null;
|
|
25071
25138
|
}
|
|
@@ -25357,7 +25424,9 @@ var ESC2 = {
|
|
|
25357
25424
|
/** 256-color violet (xterm 141). */
|
|
25358
25425
|
violet: "\x1B[38;5;141m",
|
|
25359
25426
|
/** 256-color green (xterm 42). */
|
|
25360
|
-
green: "\x1B[38;5;42m"
|
|
25427
|
+
green: "\x1B[38;5;42m",
|
|
25428
|
+
/** 256-color yellow (xterm 214), matches `cli/util/ansi.ts:yellow`. */
|
|
25429
|
+
yellow: "\x1B[38;5;214m"
|
|
25361
25430
|
};
|
|
25362
25431
|
var LOGO_LINES = [
|
|
25363
25432
|
" ____ _ _ _ _ __ __ ",
|
|
@@ -25377,7 +25446,8 @@ function renderBanner(input) {
|
|
|
25377
25446
|
host: input.host,
|
|
25378
25447
|
port: input.port,
|
|
25379
25448
|
dbPath: input.dbPath,
|
|
25380
|
-
openBrowser: input.openBrowser
|
|
25449
|
+
openBrowser: input.openBrowser,
|
|
25450
|
+
dev: input.dev === true
|
|
25381
25451
|
});
|
|
25382
25452
|
}
|
|
25383
25453
|
return renderFiglet({
|
|
@@ -25387,7 +25457,8 @@ function renderBanner(input) {
|
|
|
25387
25457
|
pathDisplay: formatCwdPath(input.cwd),
|
|
25388
25458
|
browserLine,
|
|
25389
25459
|
colorEnabled: input.colorEnabled,
|
|
25390
|
-
referencePaths: input.referencePaths ?? []
|
|
25460
|
+
referencePaths: input.referencePaths ?? [],
|
|
25461
|
+
dev: input.dev === true
|
|
25391
25462
|
});
|
|
25392
25463
|
}
|
|
25393
25464
|
function resolveColorEnabled(opts) {
|
|
@@ -25402,8 +25473,9 @@ function renderFlat(input) {
|
|
|
25402
25473
|
const safeHost = sanitizeForTerminal(input.host);
|
|
25403
25474
|
const safeDb = sanitizeForTerminal(input.dbPath);
|
|
25404
25475
|
const url = `http://${safeHost}:${input.port}`;
|
|
25476
|
+
const devSuffix = input.dev ? " [dev]" : "";
|
|
25405
25477
|
const linesOut = [];
|
|
25406
|
-
linesOut.push(`sm serve: listening on ${url} (db=${safeDb})`);
|
|
25478
|
+
linesOut.push(`sm serve${devSuffix}: listening on ${url} (db=${safeDb})`);
|
|
25407
25479
|
if (input.openBrowser) {
|
|
25408
25480
|
linesOut.push(`sm serve: opening ${url}/ in your browser. Press Ctrl+C to stop.`);
|
|
25409
25481
|
} else {
|
|
@@ -25430,12 +25502,16 @@ function renderFiglet(input) {
|
|
|
25430
25502
|
greenUnderline,
|
|
25431
25503
|
greenUnderlineClose,
|
|
25432
25504
|
violetOpen,
|
|
25433
|
-
violetClose
|
|
25505
|
+
violetClose,
|
|
25506
|
+
yellowOpen,
|
|
25507
|
+
yellowClose
|
|
25434
25508
|
} = resolveAnsi(input.colorEnabled);
|
|
25435
25509
|
const logoLines = LOGO_LINES.map((line) => `${violetOpen}${line}${violetClose}`);
|
|
25436
25510
|
const versionText = `v${input.version}`;
|
|
25437
|
-
const
|
|
25438
|
-
const
|
|
25511
|
+
const devText = "[dev]";
|
|
25512
|
+
const versionWidth = input.dev ? devText.length : versionText.length;
|
|
25513
|
+
const versionPad = Math.max(0, LOGO_WIDTH - versionWidth);
|
|
25514
|
+
const versionLine = input.dev ? `${" ".repeat(versionPad)}${yellowOpen}${devText}${yellowClose}` : `${" ".repeat(versionPad)}${dimOpen}${versionText}${dimClose}`;
|
|
25439
25515
|
const lines = [];
|
|
25440
25516
|
lines.push(...logoLines);
|
|
25441
25517
|
lines.push("");
|
|
@@ -25467,7 +25543,9 @@ var EMPTY_ANSI = {
|
|
|
25467
25543
|
greenUnderline: "",
|
|
25468
25544
|
greenUnderlineClose: "",
|
|
25469
25545
|
violetOpen: "",
|
|
25470
|
-
violetClose: ""
|
|
25546
|
+
violetClose: "",
|
|
25547
|
+
yellowOpen: "",
|
|
25548
|
+
yellowClose: ""
|
|
25471
25549
|
};
|
|
25472
25550
|
var ENABLED_ANSI = {
|
|
25473
25551
|
dimOpen: ESC2.dim,
|
|
@@ -25475,7 +25553,9 @@ var ENABLED_ANSI = {
|
|
|
25475
25553
|
greenUnderline: `${ESC2.green}${ESC2.underline}`,
|
|
25476
25554
|
greenUnderlineClose: ESC2.reset,
|
|
25477
25555
|
violetOpen: ESC2.violet,
|
|
25478
|
-
violetClose: ESC2.reset
|
|
25556
|
+
violetClose: ESC2.reset,
|
|
25557
|
+
yellowOpen: ESC2.yellow,
|
|
25558
|
+
yellowClose: ESC2.reset
|
|
25479
25559
|
};
|
|
25480
25560
|
function resolveAnsi(colorEnabled) {
|
|
25481
25561
|
return colorEnabled ? ENABLED_ANSI : EMPTY_ANSI;
|
|
@@ -25696,7 +25776,8 @@ var ServeCommand = class extends SmCommand {
|
|
|
25696
25776
|
openBrowser: validation.options.open,
|
|
25697
25777
|
isTTY,
|
|
25698
25778
|
colorEnabled,
|
|
25699
|
-
referencePaths
|
|
25779
|
+
referencePaths,
|
|
25780
|
+
dev: isDevBuild()
|
|
25700
25781
|
})
|
|
25701
25782
|
);
|
|
25702
25783
|
if (validation.options.open) {
|
|
@@ -26830,7 +26911,7 @@ var STUB_COMMANDS = [
|
|
|
26830
26911
|
// cli/commands/tutorial.ts
|
|
26831
26912
|
import { cpSync as cpSync2, existsSync as existsSync29, mkdirSync as mkdirSync6, rmSync as rmSync2, statSync as statSync11 } from "fs";
|
|
26832
26913
|
import { dirname as dirname19, join as join21, resolve as resolve39 } from "path";
|
|
26833
|
-
import { fileURLToPath as
|
|
26914
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
26834
26915
|
import { Command as Command37, Option as Option35 } from "clipanion";
|
|
26835
26916
|
|
|
26836
26917
|
// cli/i18n/tutorial.texts.ts
|
|
@@ -27000,7 +27081,7 @@ function resolveSkillSourceDir(variant) {
|
|
|
27000
27081
|
const cached = cachedSourceDirs.get(variant);
|
|
27001
27082
|
if (cached !== void 0) return cached;
|
|
27002
27083
|
const spec = VARIANT_SPECS[variant];
|
|
27003
|
-
const here = dirname19(
|
|
27084
|
+
const here = dirname19(fileURLToPath7(import.meta.url));
|
|
27004
27085
|
const candidates = [
|
|
27005
27086
|
// dev: src/cli/commands/ → repo-root .claude/skills/<slug>/
|
|
27006
27087
|
resolve39(here, "../../..", spec.sourceDir),
|
|
@@ -27048,6 +27129,7 @@ var VersionCommand = class extends SmCommand {
|
|
|
27048
27129
|
const kernelVersion = VERSION;
|
|
27049
27130
|
const specVersion = await resolveSpecVersion3();
|
|
27050
27131
|
const dbSchema = await resolveDbSchemaVersion();
|
|
27132
|
+
const dev = isDevBuild();
|
|
27051
27133
|
if (this.json) {
|
|
27052
27134
|
const payload = {
|
|
27053
27135
|
sm: VERSION,
|
|
@@ -27055,17 +27137,19 @@ var VersionCommand = class extends SmCommand {
|
|
|
27055
27137
|
spec: specVersion,
|
|
27056
27138
|
dbSchema
|
|
27057
27139
|
};
|
|
27140
|
+
if (dev) payload["dev"] = true;
|
|
27058
27141
|
this.printer.data(JSON.stringify(payload) + "\n");
|
|
27059
27142
|
return ExitCode.Ok;
|
|
27060
27143
|
}
|
|
27144
|
+
const ansi = this.ansiFor("stdout");
|
|
27145
|
+
const smValue = dev ? `${VERSION} ${ansi.yellow("[dev]")}` : VERSION;
|
|
27061
27146
|
const lines = [
|
|
27062
|
-
["sm",
|
|
27147
|
+
["sm", smValue],
|
|
27063
27148
|
["kernel", kernelVersion],
|
|
27064
27149
|
["spec", specVersion],
|
|
27065
27150
|
["runtime", runtime],
|
|
27066
27151
|
["db-schema", dbSchema]
|
|
27067
27152
|
];
|
|
27068
|
-
const ansi = this.ansiFor("stdout");
|
|
27069
27153
|
const pad = Math.max(...lines.map(([k]) => k.length));
|
|
27070
27154
|
for (const [k, v] of lines) {
|
|
27071
27155
|
this.printer.data(
|