@skill-map/cli 0.38.0 → 0.40.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/tutorial/sm-master/references/tour-authoring.md +0 -4
- package/dist/cli/tutorial/sm-tutorial/SKILL.md +279 -26
- package/dist/cli.js +1779 -1512
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +30 -14
- package/dist/index.js.map +1 -1
- package/dist/kernel/index.d.ts +57 -47
- package/dist/kernel/index.js +30 -14
- package/dist/kernel/index.js.map +1 -1
- package/dist/ui/{chunk-AAR3Y55J.js → chunk-2W6BCESO.js} +30 -29
- package/dist/ui/{chunk-NTM2J2WO.js → chunk-3FPS5OIY.js} +1 -1
- package/dist/ui/chunk-4KQGLZV2.js +123 -0
- package/dist/ui/chunk-ASCTZSBZ.js +107 -0
- package/dist/ui/chunk-GSTYLDCL.js +135 -0
- package/dist/ui/chunk-MHEKCBIR.js +317 -0
- package/dist/ui/chunk-PFA2CVPN.js +61 -0
- package/dist/ui/chunk-PURF7B6R.js +411 -0
- package/dist/ui/chunk-UFCOGNZX.js +1 -0
- package/dist/ui/chunk-WQ42JTZB.js +90 -0
- package/dist/ui/chunk-XEWWXGNE.js +1 -0
- package/dist/ui/chunk-YXFKPYK3.js +965 -0
- package/dist/ui/index.html +1 -1
- package/dist/ui/main-WLCA4YLM.js +3 -0
- package/package.json +2 -2
- package/dist/ui/chunk-47MG5XH2.js +0 -317
- package/dist/ui/chunk-4CDTW64C.js +0 -500
- package/dist/ui/chunk-4XEJUDPL.js +0 -123
- package/dist/ui/chunk-5AZ5S6JB.js +0 -1
- package/dist/ui/chunk-G5CKBDBB.js +0 -107
- package/dist/ui/chunk-KKOZFBXQ.js +0 -135
- package/dist/ui/chunk-O5N7UH37.js +0 -965
- package/dist/ui/chunk-QZM2G474.js +0 -61
- package/dist/ui/chunk-Z4LANJFK.js +0 -1
- package/dist/ui/main-EO5QNLE4.js +0 -2
package/dist/cli.js
CHANGED
|
@@ -441,8 +441,7 @@ var claudeProvider = {
|
|
|
441
441
|
id: "claude",
|
|
442
442
|
pluginId: CLAUDE_PLUGIN_ID,
|
|
443
443
|
kind: "provider",
|
|
444
|
-
|
|
445
|
-
description: "Walks Claude Code scope conventions (.claude/{agents,commands,skills}).",
|
|
444
|
+
description: "Classifies files under `.claude/{agents,commands,skills}` as Claude Code agents, commands, and skills.",
|
|
446
445
|
// Vendor provider: Claude Code only reads its own `.claude/` territory
|
|
447
446
|
// and ignores `.codex/` / Antigravity layouts at runtime. Gating the
|
|
448
447
|
// classifier behind the active lens prevents the walker from inventing
|
|
@@ -726,8 +725,7 @@ var atDirectiveExtractor = {
|
|
|
726
725
|
id: ID,
|
|
727
726
|
pluginId: CLAUDE_PLUGIN_ID,
|
|
728
727
|
kind: "extractor",
|
|
729
|
-
|
|
730
|
-
description: "Detects `@<token>` directives in a node's body using Claude Code interpretation 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. Gated by `precondition.provider: ['claude']` so Antigravity / Cursor / Codex apply their own at-directive flavours via their own extractors.",
|
|
728
|
+
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
729
|
scope: "body",
|
|
732
730
|
precondition: { provider: ["claude"] },
|
|
733
731
|
// eslint-disable-next-line complexity
|
|
@@ -808,15 +806,14 @@ function resolveSourceRelative(sourceDir, bare) {
|
|
|
808
806
|
return pathPosix.normalize(joined);
|
|
809
807
|
}
|
|
810
808
|
|
|
811
|
-
// plugins/claude/extractors/slash/index.ts
|
|
812
|
-
var ID2 = "slash";
|
|
809
|
+
// plugins/claude/extractors/slash-command/index.ts
|
|
810
|
+
var ID2 = "slash-command";
|
|
813
811
|
var SLASH_RE = /(?<![A-Za-z0-9_/.:?#=&])(\/[a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)/gi;
|
|
814
|
-
var
|
|
812
|
+
var slashCommandExtractor = {
|
|
815
813
|
id: ID2,
|
|
816
814
|
pluginId: CLAUDE_PLUGIN_ID,
|
|
817
815
|
kind: "extractor",
|
|
818
|
-
|
|
819
|
-
description: "Detects `/command` invocations in a node's body using Claude Code routing rules and turns each one into an arrow between nodes in the graph. Gated by `precondition.provider: ['claude']` so Antigravity / Cursor / Codex apply their own slash flavours (Antigravity ships subagent and skill panels, Codex deprecated user slash commands, etc.) via their own extractors.",
|
|
816
|
+
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
817
|
scope: "body",
|
|
821
818
|
precondition: { provider: ["claude"] },
|
|
822
819
|
extract(ctx) {
|
|
@@ -865,8 +862,7 @@ var antigravityProvider = {
|
|
|
865
862
|
id: "antigravity",
|
|
866
863
|
pluginId: ANTIGRAVITY_PLUGIN_ID,
|
|
867
864
|
kind: "provider",
|
|
868
|
-
|
|
869
|
-
description: "Google Antigravity CLI. Replaces the retired Gemini CLI; skills route through the neutral `agent-skills` Provider via `.agents/skills/`. This Provider contributes lens identity and a reserved-name seed catalog.",
|
|
865
|
+
description: "Declares the Google Antigravity runtime and its reserved built-in names.",
|
|
870
866
|
// Vendor provider: marked gated for the day Antigravity grows its own
|
|
871
867
|
// on-disk kind beyond the open standard. Today `kinds: {}` and
|
|
872
868
|
// `classify` returns `null` for every path, so the flag is inert; the
|
|
@@ -896,7 +892,7 @@ var antigravityProvider = {
|
|
|
896
892
|
// Gemini CLI's. We mirror the full 38-verb Gemini CLI catalog (plus its
|
|
897
893
|
// four documented aliases: `dir`, `?`, `exit`, `bashes`) so a user file
|
|
898
894
|
// that names a skill / command `help`, `clear`, `mcp`, etc. is flagged
|
|
899
|
-
// immediately by `core/reserved
|
|
895
|
+
// immediately by `core/name-reserved` once the lens activates the catalog.
|
|
900
896
|
//
|
|
901
897
|
// The catalog is INACTIVE today: the analyzer keys on `node.provider`
|
|
902
898
|
// and this Provider's `classify()` returns `null` for every path, so
|
|
@@ -1014,8 +1010,7 @@ var openaiProvider = {
|
|
|
1014
1010
|
id: "openai",
|
|
1015
1011
|
pluginId: OPENAI_PLUGIN_ID,
|
|
1016
1012
|
kind: "provider",
|
|
1017
|
-
|
|
1018
|
-
description: "Walks OpenAI Codex CLI scope conventions (.codex/agents/*.toml).",
|
|
1013
|
+
description: "Classifies files under `.codex/agents/*.toml` as OpenAI Codex CLI sub-agents.",
|
|
1019
1014
|
// Vendor provider: Codex CLI only reads its own `.codex/` territory.
|
|
1020
1015
|
// Gating the classifier behind the active lens keeps the walker from
|
|
1021
1016
|
// claiming Codex agents under a `claude` (or any other) lens, where
|
|
@@ -1072,8 +1067,7 @@ var agentSkillsProvider = {
|
|
|
1072
1067
|
id: "agent-skills",
|
|
1073
1068
|
pluginId: AGENT_SKILLS_PLUGIN_ID,
|
|
1074
1069
|
kind: "provider",
|
|
1075
|
-
|
|
1076
|
-
description: "Agent Skills open standard. Vendor-neutral path `.agents/skills/<name>/SKILL.md` (Anthropic, OpenAI, Google). See agentskills.io.",
|
|
1070
|
+
description: "Classifies files under `.agents/skills/<name>/SKILL.md` as Agent Skills.",
|
|
1077
1071
|
read: { extensions: [".md"], parser: "frontmatter-yaml" },
|
|
1078
1072
|
kinds: {
|
|
1079
1073
|
skill: {
|
|
@@ -1120,8 +1114,7 @@ var coreMarkdownProvider = {
|
|
|
1120
1114
|
id: "markdown",
|
|
1121
1115
|
pluginId: CORE_PLUGIN_ID,
|
|
1122
1116
|
kind: "provider",
|
|
1123
|
-
|
|
1124
|
-
description: "Universal `.md` fallback. Claims any markdown file no vendor-specific Provider classifies.",
|
|
1117
|
+
description: "Universal `.md` fallback. Claims any markdown file that no vendor-specific provider has classified.",
|
|
1125
1118
|
read: { extensions: [".md"], parser: "frontmatter-yaml" },
|
|
1126
1119
|
// Per spec § A.6, defaultRefreshAction values MUST be qualified
|
|
1127
1120
|
// action ids. The summarize-markdown action is not yet implemented
|
|
@@ -1156,10 +1149,14 @@ var coreMarkdownProvider = {
|
|
|
1156
1149
|
},
|
|
1157
1150
|
// No `resolution`: `core/markdown` is the universal fallback Provider,
|
|
1158
1151
|
// it does not declare an invocation surface of its own. Mentions /
|
|
1159
|
-
// slashes sourced from markdown bodies still
|
|
1160
|
-
//
|
|
1161
|
-
//
|
|
1162
|
-
// the
|
|
1152
|
+
// slashes sourced from markdown bodies are still resolved by the
|
|
1153
|
+
// post-walk transform, the lookup keys on the ACTIVE PROVIDER LENS
|
|
1154
|
+
// (per `spec/architecture.md` §Provider · resolution rules), mirroring
|
|
1155
|
+
// the extractor gate that authorised the emission in the first place.
|
|
1156
|
+
// Leaving this field absent therefore has no resolver-side impact
|
|
1157
|
+
// under any lens that DOES declare a resolution map; it would only
|
|
1158
|
+
// matter the day `markdown` itself becomes a lens (which is not on
|
|
1159
|
+
// the roadmap, the format is provider-agnostic by design).
|
|
1163
1160
|
classify() {
|
|
1164
1161
|
return "markdown";
|
|
1165
1162
|
}
|
|
@@ -1171,8 +1168,7 @@ var annotationsExtractor = {
|
|
|
1171
1168
|
id: ID3,
|
|
1172
1169
|
pluginId: CORE_PLUGIN_ID,
|
|
1173
1170
|
kind: "extractor",
|
|
1174
|
-
|
|
1175
|
-
description: "Turns the `supersedes` and `supersededBy` entries you write in a node's `.sm` sidecar into the arrows (edges) shown between nodes in the graph.",
|
|
1171
|
+
description: "Turns the `supersedes` and `supersededBy` entries from a node's `.sm` sidecar into arrows between nodes in the graph.",
|
|
1176
1172
|
scope: "frontmatter",
|
|
1177
1173
|
extract(ctx) {
|
|
1178
1174
|
const sourcePath = ctx.node.path;
|
|
@@ -1233,8 +1229,7 @@ var externalUrlCounterExtractor = {
|
|
|
1233
1229
|
id: ID4,
|
|
1234
1230
|
pluginId: CORE_PLUGIN_ID,
|
|
1235
1231
|
kind: "extractor",
|
|
1236
|
-
|
|
1237
|
-
description: "Counts the distinct external URLs in a node's body and shows the total on the card.",
|
|
1232
|
+
description: "Counts the distinct external URLs in a node's body and shows the count on the card.",
|
|
1238
1233
|
scope: "body",
|
|
1239
1234
|
/**
|
|
1240
1235
|
* Phase 6 / View contribution system, surface the distinct-URL
|
|
@@ -1322,8 +1317,7 @@ var markdownLinkExtractor = {
|
|
|
1322
1317
|
id: ID5,
|
|
1323
1318
|
pluginId: CORE_PLUGIN_ID,
|
|
1324
1319
|
kind: "extractor",
|
|
1325
|
-
|
|
1326
|
-
description: "Detects markdown links (`[text](path)`) in a node's body and turns each one into an arrow between nodes in the graph.",
|
|
1320
|
+
description: "Turns markdown links (`[text](path)`) in a node's body into arrows between nodes in the graph.",
|
|
1327
1321
|
scope: "body",
|
|
1328
1322
|
extract(ctx) {
|
|
1329
1323
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1352,7 +1346,7 @@ var markdownLinkExtractor = {
|
|
|
1352
1346
|
// explicitly designates an out-link via the brackets +
|
|
1353
1347
|
// parentheses pair; there is no inference left to discount.
|
|
1354
1348
|
// Whether the path resolves to a real node is a separate
|
|
1355
|
-
// concern (the `core/broken
|
|
1349
|
+
// concern (the `core/reference-broken` analyzer flags unresolved
|
|
1356
1350
|
// targets), not a confidence question.
|
|
1357
1351
|
confidence: 1,
|
|
1358
1352
|
rationale: "unambiguous markdown link syntax",
|
|
@@ -1384,8 +1378,7 @@ var mcpToolsExtractor = {
|
|
|
1384
1378
|
id: ID6,
|
|
1385
1379
|
pluginId: CORE_PLUGIN_ID,
|
|
1386
1380
|
kind: "extractor",
|
|
1387
|
-
|
|
1388
|
-
description: "Detects `tools: [mcp__<server>__<tool>]` entries in a node's frontmatter and turns each unique server into an MCP node + a reference edge from the source.",
|
|
1381
|
+
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
1382
|
scope: "frontmatter",
|
|
1390
1383
|
extract(ctx) {
|
|
1391
1384
|
const raw = ctx.frontmatter["tools"];
|
|
@@ -1439,15 +1432,14 @@ function collectMcpServers(tools) {
|
|
|
1439
1432
|
return out;
|
|
1440
1433
|
}
|
|
1441
1434
|
|
|
1442
|
-
// plugins/core/extractors/tools-
|
|
1443
|
-
var ID7 = "tools-
|
|
1435
|
+
// plugins/core/extractors/tools-counter/index.ts
|
|
1436
|
+
var ID7 = "tools-counter";
|
|
1444
1437
|
var TOOLTIP_MAX = 255;
|
|
1445
|
-
var
|
|
1438
|
+
var toolsCounterExtractor = {
|
|
1446
1439
|
id: ID7,
|
|
1447
1440
|
pluginId: CORE_PLUGIN_ID,
|
|
1448
1441
|
kind: "extractor",
|
|
1449
|
-
|
|
1450
|
-
description: "Counts the tools an agent declares in its frontmatter and shows the total on the agent card.",
|
|
1442
|
+
description: "Counts the tools an agent declares in its frontmatter and shows the count on the agent card.",
|
|
1451
1443
|
scope: "frontmatter",
|
|
1452
1444
|
precondition: { kind: ["claude/agent"] },
|
|
1453
1445
|
ui: {
|
|
@@ -1479,6 +1471,178 @@ function buildTooltip(names) {
|
|
|
1479
1471
|
return `${joined.slice(0, TOOLTIP_MAX - 1)}\u2026`;
|
|
1480
1472
|
}
|
|
1481
1473
|
|
|
1474
|
+
// plugins/core/analyzers/annotation-field-unknown/index.ts
|
|
1475
|
+
import { readFileSync } from "fs";
|
|
1476
|
+
import { dirname, resolve } from "path";
|
|
1477
|
+
import { createRequire } from "module";
|
|
1478
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
1479
|
+
|
|
1480
|
+
// kernel/util/ajv-interop.ts
|
|
1481
|
+
import addFormatsModule from "ajv-formats";
|
|
1482
|
+
var addFormats = addFormatsModule.default ?? addFormatsModule;
|
|
1483
|
+
function applyAjvFormats(ajv) {
|
|
1484
|
+
addFormats(ajv);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// plugins/core/analyzers/annotation-field-unknown/text.ts
|
|
1488
|
+
var ANNOTATION_FIELD_UNKNOWN_TEXTS = {
|
|
1489
|
+
/** Key inside `annotations:` is not in the curated catalog. */
|
|
1490
|
+
unknownAnnotationKey: "{{path}}: sidecar annotations contain unknown key '{{key}}' (not in annotations.schema.json catalog).",
|
|
1491
|
+
/** Top-level key is neither reserved, nor a registered plugin namespace, nor a registered root key. */
|
|
1492
|
+
unknownRootKey: "{{path}}: sidecar declares unknown top-level key '{{key}}'; not a reserved block, not a registered plugin namespace, not a registered root contribution.",
|
|
1493
|
+
/** Value under a registered plugin namespace fails the contributed schema. */
|
|
1494
|
+
pluginNamespaceInvalid: "{{path}}: sidecar block '{{pluginId}}.{{key}}' fails the schema contributed by plugin '{{pluginId}}': {{errors}}.",
|
|
1495
|
+
// Tooltips for the per-node view-contribution badges. Singular vs
|
|
1496
|
+
// plural keeps the count grammar correct without a sub-template.
|
|
1497
|
+
alertTooltipSingle: "This node has 1 unknown field in its sidecar. Open the inspector for details.",
|
|
1498
|
+
alertTooltipMany: "This node has {{count}} unknown fields in its sidecar. Open the inspector for details."
|
|
1499
|
+
};
|
|
1500
|
+
|
|
1501
|
+
// plugins/core/analyzers/annotation-field-unknown/index.ts
|
|
1502
|
+
var ID8 = "annotation-field-unknown";
|
|
1503
|
+
var RESERVED_ROOT_BLOCKS = /* @__PURE__ */ new Set(["identity", "annotations", "settings", "audit"]);
|
|
1504
|
+
var annotationFieldUnknownAnalyzer = {
|
|
1505
|
+
id: ID8,
|
|
1506
|
+
pluginId: CORE_PLUGIN_ID,
|
|
1507
|
+
kind: "analyzer",
|
|
1508
|
+
description: "Flags typos or unrecognized keys in sidecars (`.sm`).",
|
|
1509
|
+
mode: "deterministic",
|
|
1510
|
+
// No `ui` declaration: the per-node icon-only chip used to surface
|
|
1511
|
+
// "this sidecar declares unknown keys" on `card.footer.right`, but
|
|
1512
|
+
// its severity (`warn`) is now folded into the aggregate counters
|
|
1513
|
+
// emitted by `core/issue-counter`. The detection logic stays, the
|
|
1514
|
+
// findings still ship as `Issue` records (visible in the inspector,
|
|
1515
|
+
// `sm check`, etc.).
|
|
1516
|
+
ui: {},
|
|
1517
|
+
// Analyzer body iterates every sidecar root and classifies each
|
|
1518
|
+
// key against three buckets (catalog / plugin namespace / unknown
|
|
1519
|
+
// root). The per-key branching IS the classification table; factoring
|
|
1520
|
+
// it out would rebuild the discriminator elsewhere. Per
|
|
1521
|
+
// `context/lint.md` category 7 (recursive type-discriminator walkers).
|
|
1522
|
+
// eslint-disable-next-line complexity
|
|
1523
|
+
evaluate(ctx) {
|
|
1524
|
+
const sidecarRoots = ctx.sidecarRoots;
|
|
1525
|
+
if (!sidecarRoots || sidecarRoots.size === 0) return [];
|
|
1526
|
+
const knownAnnotationKeys = getKnownAnnotationKeys();
|
|
1527
|
+
const contributions = ctx.annotationContributions ?? [];
|
|
1528
|
+
const namespacedByPlugin = indexNamespacedContributions(contributions);
|
|
1529
|
+
const rootKeys = indexRootContributions(contributions);
|
|
1530
|
+
const knownPluginIds = collectPluginIds(contributions);
|
|
1531
|
+
const issues = [];
|
|
1532
|
+
const perNode = /* @__PURE__ */ new Map();
|
|
1533
|
+
const bump2 = (nodePath) => {
|
|
1534
|
+
perNode.set(nodePath, (perNode.get(nodePath) ?? 0) + 1);
|
|
1535
|
+
};
|
|
1536
|
+
for (const node of ctx.nodes) {
|
|
1537
|
+
const root = sidecarRoots.get(node.path);
|
|
1538
|
+
if (!root) continue;
|
|
1539
|
+
const annotations = root["annotations"];
|
|
1540
|
+
if (annotations !== void 0 && annotations !== null && typeof annotations === "object" && !Array.isArray(annotations)) {
|
|
1541
|
+
for (const key of Object.keys(annotations)) {
|
|
1542
|
+
if (!knownAnnotationKeys.has(key)) {
|
|
1543
|
+
issues.push({
|
|
1544
|
+
analyzerId: ID8,
|
|
1545
|
+
severity: "warn",
|
|
1546
|
+
nodeIds: [node.path],
|
|
1547
|
+
message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.unknownAnnotationKey, {
|
|
1548
|
+
path: node.path,
|
|
1549
|
+
key
|
|
1550
|
+
}),
|
|
1551
|
+
data: { surface: "annotations", key }
|
|
1552
|
+
});
|
|
1553
|
+
bump2(node.path);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
for (const key of Object.keys(root)) {
|
|
1558
|
+
if (RESERVED_ROOT_BLOCKS.has(key)) continue;
|
|
1559
|
+
if (rootKeys.has(key)) continue;
|
|
1560
|
+
if (knownPluginIds.has(key)) {
|
|
1561
|
+
const block = root[key];
|
|
1562
|
+
if (block === null || typeof block !== "object" || Array.isArray(block)) continue;
|
|
1563
|
+
const contribsForPlugin = namespacedByPlugin.get(key);
|
|
1564
|
+
if (!contribsForPlugin) continue;
|
|
1565
|
+
for (const [contribKey, validator] of contribsForPlugin) {
|
|
1566
|
+
const value = block[contribKey];
|
|
1567
|
+
if (value === void 0) continue;
|
|
1568
|
+
if (validator(value)) continue;
|
|
1569
|
+
const errors = (validator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
|
|
1570
|
+
issues.push({
|
|
1571
|
+
analyzerId: ID8,
|
|
1572
|
+
severity: "warn",
|
|
1573
|
+
nodeIds: [node.path],
|
|
1574
|
+
message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.pluginNamespaceInvalid, {
|
|
1575
|
+
path: node.path,
|
|
1576
|
+
pluginId: key,
|
|
1577
|
+
key: contribKey,
|
|
1578
|
+
errors
|
|
1579
|
+
}),
|
|
1580
|
+
data: { surface: "plugin-namespace", pluginId: key, key: contribKey }
|
|
1581
|
+
});
|
|
1582
|
+
bump2(node.path);
|
|
1583
|
+
}
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
issues.push({
|
|
1587
|
+
analyzerId: ID8,
|
|
1588
|
+
severity: "warn",
|
|
1589
|
+
nodeIds: [node.path],
|
|
1590
|
+
message: tx(ANNOTATION_FIELD_UNKNOWN_TEXTS.unknownRootKey, {
|
|
1591
|
+
path: node.path,
|
|
1592
|
+
key
|
|
1593
|
+
}),
|
|
1594
|
+
data: { surface: "root", key }
|
|
1595
|
+
});
|
|
1596
|
+
bump2(node.path);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
void perNode;
|
|
1600
|
+
return issues;
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
var cachedKnownKeys = null;
|
|
1604
|
+
function getKnownAnnotationKeys() {
|
|
1605
|
+
if (cachedKnownKeys) return cachedKnownKeys;
|
|
1606
|
+
const require2 = createRequire(import.meta.url);
|
|
1607
|
+
const indexPath = require2.resolve("@skill-map/spec/index.json");
|
|
1608
|
+
const specRoot = dirname(indexPath);
|
|
1609
|
+
const schema = JSON.parse(
|
|
1610
|
+
readFileSync(resolve(specRoot, "schemas/annotations.schema.json"), "utf8")
|
|
1611
|
+
);
|
|
1612
|
+
cachedKnownKeys = new Set(Object.keys(schema.properties ?? {}));
|
|
1613
|
+
return cachedKnownKeys;
|
|
1614
|
+
}
|
|
1615
|
+
function indexNamespacedContributions(contributions) {
|
|
1616
|
+
const out = /* @__PURE__ */ new Map();
|
|
1617
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true, allowUnionTypes: true });
|
|
1618
|
+
applyAjvFormats(ajv);
|
|
1619
|
+
for (const entry of contributions) {
|
|
1620
|
+
if (entry.location !== "namespaced") continue;
|
|
1621
|
+
let bucket = out.get(entry.pluginId);
|
|
1622
|
+
if (!bucket) {
|
|
1623
|
+
bucket = /* @__PURE__ */ new Map();
|
|
1624
|
+
out.set(entry.pluginId, bucket);
|
|
1625
|
+
}
|
|
1626
|
+
try {
|
|
1627
|
+
bucket.set(entry.key, ajv.compile(entry.schema));
|
|
1628
|
+
} catch {
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return out;
|
|
1632
|
+
}
|
|
1633
|
+
function indexRootContributions(contributions) {
|
|
1634
|
+
const out = /* @__PURE__ */ new Set();
|
|
1635
|
+
for (const entry of contributions) {
|
|
1636
|
+
if (entry.location === "root") out.add(entry.key);
|
|
1637
|
+
}
|
|
1638
|
+
return out;
|
|
1639
|
+
}
|
|
1640
|
+
function collectPluginIds(contributions) {
|
|
1641
|
+
const out = /* @__PURE__ */ new Set();
|
|
1642
|
+
for (const entry of contributions) out.add(entry.pluginId);
|
|
1643
|
+
return out;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1482
1646
|
// plugins/core/analyzers/annotation-orphan/text.ts
|
|
1483
1647
|
var ANNOTATION_ORPHAN_TEXTS = {
|
|
1484
1648
|
/** Sidecar `<path>.sm` has no matching `<path>.md`. */
|
|
@@ -1486,13 +1650,12 @@ var ANNOTATION_ORPHAN_TEXTS = {
|
|
|
1486
1650
|
};
|
|
1487
1651
|
|
|
1488
1652
|
// plugins/core/analyzers/annotation-orphan/index.ts
|
|
1489
|
-
var
|
|
1653
|
+
var ID9 = "annotation-orphan";
|
|
1490
1654
|
var annotationOrphanAnalyzer = {
|
|
1491
|
-
id:
|
|
1655
|
+
id: ID9,
|
|
1492
1656
|
pluginId: CORE_PLUGIN_ID,
|
|
1493
1657
|
kind: "analyzer",
|
|
1494
|
-
|
|
1495
|
-
description: "Detects and flags sidecars (`.sm`) whose `.md` no longer exists.",
|
|
1658
|
+
description: "Flags sidecars (`.sm`) whose `.md` file no longer exists.",
|
|
1496
1659
|
mode: "deterministic",
|
|
1497
1660
|
evaluate(ctx) {
|
|
1498
1661
|
const orphans = ctx.orphanSidecars;
|
|
@@ -1501,7 +1664,7 @@ var annotationOrphanAnalyzer = {
|
|
|
1501
1664
|
for (const orphan of orphans) {
|
|
1502
1665
|
const expectedMdRelative = orphan.relativePath.endsWith(".sm") ? `${orphan.relativePath.slice(0, -".sm".length)}.md` : `${orphan.relativePath}.md`;
|
|
1503
1666
|
issues.push({
|
|
1504
|
-
analyzerId:
|
|
1667
|
+
analyzerId: ID9,
|
|
1505
1668
|
severity: "warn",
|
|
1506
1669
|
nodeIds: [expectedMdRelative],
|
|
1507
1670
|
message: tx(ANNOTATION_ORPHAN_TEXTS.message, {
|
|
@@ -1538,17 +1701,16 @@ var ANNOTATION_STALE_TEXTS = {
|
|
|
1538
1701
|
};
|
|
1539
1702
|
|
|
1540
1703
|
// plugins/core/analyzers/annotation-stale/index.ts
|
|
1541
|
-
var
|
|
1704
|
+
var ID10 = "annotation-stale";
|
|
1542
1705
|
var annotationStaleAnalyzer = {
|
|
1543
|
-
id:
|
|
1706
|
+
id: ID10,
|
|
1544
1707
|
pluginId: CORE_PLUGIN_ID,
|
|
1545
1708
|
kind: "analyzer",
|
|
1546
|
-
|
|
1547
|
-
description: "Detects and marks sidecars (`.sm`) out of date of their `.md`.",
|
|
1709
|
+
description: "Marks sidecars (`.sm`) that are out of date with their `.md`.",
|
|
1548
1710
|
mode: "deterministic",
|
|
1549
1711
|
// The natural fix is to bump the node: refreshes `for` hashes,
|
|
1550
1712
|
// increments `annotations.version`, and stamps the audit block. The
|
|
1551
|
-
// UI surfaces `core/bump` in the node inspector under "Recommended
|
|
1713
|
+
// UI surfaces `core/node-bump` in the node inspector under "Recommended
|
|
1552
1714
|
// for issues" whenever this analyzer fires.
|
|
1553
1715
|
ui: {
|
|
1554
1716
|
// A `pi-clock` chip in the footer-right cluster so the operator
|
|
@@ -1564,7 +1726,10 @@ var annotationStaleAnalyzer = {
|
|
|
1564
1726
|
slot: "card.footer.right",
|
|
1565
1727
|
icon: "pi-clock",
|
|
1566
1728
|
emitWhenEmpty: true,
|
|
1567
|
-
|
|
1729
|
+
// First in the footer-right cluster: drift is the operator's
|
|
1730
|
+
// entry point for "this node disagrees with its sidecar",
|
|
1731
|
+
// followed by stability, then the severity counters.
|
|
1732
|
+
priority: 10
|
|
1568
1733
|
}
|
|
1569
1734
|
},
|
|
1570
1735
|
evaluate(ctx) {
|
|
@@ -1575,7 +1740,7 @@ var annotationStaleAnalyzer = {
|
|
|
1575
1740
|
if (status === "fresh") continue;
|
|
1576
1741
|
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
1742
|
issues.push({
|
|
1578
|
-
analyzerId:
|
|
1743
|
+
analyzerId: ID10,
|
|
1579
1744
|
severity: "warn",
|
|
1580
1745
|
nodeIds: [node.path],
|
|
1581
1746
|
message,
|
|
@@ -1601,233 +1766,130 @@ function tooltipFor(status) {
|
|
|
1601
1766
|
}
|
|
1602
1767
|
}
|
|
1603
1768
|
|
|
1604
|
-
// plugins/core/analyzers/
|
|
1605
|
-
|
|
1769
|
+
// plugins/core/analyzers/contribution-orphan/index.ts
|
|
1770
|
+
var ID11 = "contribution-orphan";
|
|
1771
|
+
var contributionOrphanAnalyzer = {
|
|
1772
|
+
id: ID11,
|
|
1773
|
+
pluginId: CORE_PLUGIN_ID,
|
|
1774
|
+
kind: "analyzer",
|
|
1775
|
+
description: "Warns about plugin data referencing nodes renamed or deleted in the latest scan.",
|
|
1776
|
+
mode: "deterministic",
|
|
1777
|
+
evaluate(_ctx) {
|
|
1778
|
+
return [];
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1606
1781
|
|
|
1607
|
-
// plugins/core/analyzers/
|
|
1608
|
-
var
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
alertTooltipSingle: "This node has a broken reference. Open the inspector for details.",
|
|
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}}."
|
|
1782
|
+
// plugins/core/analyzers/issue-counter/text.ts
|
|
1783
|
+
var ISSUE_COUNTER_TEXTS = {
|
|
1784
|
+
errorTooltipSingle: "1 error",
|
|
1785
|
+
errorTooltipMany: "{{count}} errors",
|
|
1786
|
+
warnTooltipSingle: "1 warning",
|
|
1787
|
+
warnTooltipMany: "{{count}} warnings"
|
|
1621
1788
|
};
|
|
1622
1789
|
|
|
1623
|
-
// plugins/core/analyzers/
|
|
1624
|
-
var
|
|
1625
|
-
|
|
1626
|
-
|
|
1790
|
+
// plugins/core/analyzers/issue-counter/index.ts
|
|
1791
|
+
var ID12 = "issue-counter";
|
|
1792
|
+
function countByTier(issues) {
|
|
1793
|
+
const errors = /* @__PURE__ */ new Map();
|
|
1794
|
+
const warns = /* @__PURE__ */ new Map();
|
|
1795
|
+
for (const issue of issues) {
|
|
1796
|
+
const bucket = issue.severity === "error" ? errors : issue.severity === "warn" ? warns : null;
|
|
1797
|
+
if (!bucket) continue;
|
|
1798
|
+
for (const nodeId of issue.nodeIds) {
|
|
1799
|
+
bucket.set(nodeId, (bucket.get(nodeId) ?? 0) + 1);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
return { errors, warns };
|
|
1803
|
+
}
|
|
1804
|
+
function emitTierChips(ctx, contributionId, severity, counts, singleTooltip, manyTooltip) {
|
|
1805
|
+
for (const [nodePath, count] of counts) {
|
|
1806
|
+
const capped = Math.min(count, 99);
|
|
1807
|
+
ctx.emitContribution(nodePath, contributionId, {
|
|
1808
|
+
value: capped,
|
|
1809
|
+
severity,
|
|
1810
|
+
tooltip: count === 1 ? singleTooltip : tx(manyTooltip, { count })
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
var issueCounterAnalyzer = {
|
|
1815
|
+
id: ID12,
|
|
1627
1816
|
pluginId: CORE_PLUGIN_ID,
|
|
1628
1817
|
kind: "analyzer",
|
|
1629
|
-
|
|
1630
|
-
description: "Detects and flags arrows pointing at a node not part of the current scan.",
|
|
1818
|
+
description: "Emits one aggregate severity chip per node (error + warn counts) from the live issue accumulator.",
|
|
1631
1819
|
mode: "deterministic",
|
|
1820
|
+
phase: "aggregate",
|
|
1632
1821
|
ui: {
|
|
1633
|
-
//
|
|
1634
|
-
//
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1822
|
+
// Third in the footer-right cluster, after the drift chip
|
|
1823
|
+
// (priority 10) and the stability badge (priority 20). The warn
|
|
1824
|
+
// counter sits before the error counter so the operator reads
|
|
1825
|
+
// "advisory → blocking" left-to-right.
|
|
1826
|
+
warnCount: {
|
|
1827
|
+
slot: "card.footer.right",
|
|
1828
|
+
icon: "pi-exclamation-triangle",
|
|
1829
|
+
emitWhenEmpty: false,
|
|
1830
|
+
priority: 30
|
|
1639
1831
|
},
|
|
1640
|
-
//
|
|
1641
|
-
//
|
|
1642
|
-
|
|
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: {
|
|
1832
|
+
// Last in the cluster, the red chip pins to the right edge so the
|
|
1833
|
+
// most severe signal anchors the row's reading position.
|
|
1834
|
+
errorCount: {
|
|
1646
1835
|
slot: "card.footer.right",
|
|
1647
|
-
icon: "
|
|
1836
|
+
icon: "pi-times-circle",
|
|
1648
1837
|
emitWhenEmpty: false,
|
|
1649
1838
|
priority: 40
|
|
1650
1839
|
}
|
|
1651
1840
|
},
|
|
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
1841
|
evaluate(ctx) {
|
|
1658
|
-
const
|
|
1659
|
-
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1842
|
+
const accumulator = ctx.accumulatedIssues ?? [];
|
|
1843
|
+
if (accumulator.length === 0) return [];
|
|
1844
|
+
const { errors, warns } = countByTier(accumulator);
|
|
1845
|
+
emitTierChips(
|
|
1846
|
+
ctx,
|
|
1847
|
+
"errorCount",
|
|
1848
|
+
"danger",
|
|
1849
|
+
errors,
|
|
1850
|
+
ISSUE_COUNTER_TEXTS.errorTooltipSingle,
|
|
1851
|
+
ISSUE_COUNTER_TEXTS.errorTooltipMany
|
|
1852
|
+
);
|
|
1853
|
+
emitTierChips(
|
|
1854
|
+
ctx,
|
|
1855
|
+
"warnCount",
|
|
1856
|
+
"warn",
|
|
1857
|
+
warns,
|
|
1858
|
+
ISSUE_COUNTER_TEXTS.warnTooltipSingle,
|
|
1859
|
+
ISSUE_COUNTER_TEXTS.warnTooltipMany
|
|
1860
|
+
);
|
|
1861
|
+
return [];
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1864
|
+
|
|
1865
|
+
// plugins/core/analyzers/job-file-orphan/text.ts
|
|
1866
|
+
var JOB_FILE_ORPHAN_TEXTS = {
|
|
1867
|
+
/**
|
|
1868
|
+
* `<path>.md` lives under `.skill-map/jobs/` but no `state_jobs.filePath`
|
|
1869
|
+
* row references it. Run `sm job prune --orphan-files` to remove.
|
|
1870
|
+
*/
|
|
1871
|
+
message: "Orphan job file: {{filePath}} is not referenced by any state_jobs row. Run `sm job prune --orphan-files` to remove it."
|
|
1872
|
+
};
|
|
1873
|
+
|
|
1874
|
+
// plugins/core/analyzers/job-file-orphan/index.ts
|
|
1875
|
+
var ID13 = "job-file-orphan";
|
|
1876
|
+
var jobFileOrphanAnalyzer = {
|
|
1877
|
+
id: ID13,
|
|
1878
|
+
pluginId: CORE_PLUGIN_ID,
|
|
1879
|
+
kind: "analyzer",
|
|
1880
|
+
description: "Flags leftover job result files (no live job references them). Clean up via `sm job prune --orphan-files`.",
|
|
1881
|
+
mode: "deterministic",
|
|
1882
|
+
evaluate(ctx) {
|
|
1883
|
+
const orphans = ctx.orphanJobFiles;
|
|
1884
|
+
if (!orphans || orphans.length === 0) return [];
|
|
1662
1885
|
const issues = [];
|
|
1663
|
-
const
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
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 [];
|
|
1823
|
-
const issues = [];
|
|
1824
|
-
for (const filePath of orphans) {
|
|
1825
|
-
issues.push({
|
|
1826
|
-
analyzerId: ID12,
|
|
1827
|
-
severity: "warn",
|
|
1828
|
-
nodeIds: [filePath],
|
|
1829
|
-
message: tx(JOB_ORPHAN_FILE_TEXTS.message, { filePath }),
|
|
1830
|
-
data: { filePath }
|
|
1886
|
+
for (const filePath of orphans) {
|
|
1887
|
+
issues.push({
|
|
1888
|
+
analyzerId: ID13,
|
|
1889
|
+
severity: "warn",
|
|
1890
|
+
nodeIds: [filePath],
|
|
1891
|
+
message: tx(JOB_FILE_ORPHAN_TEXTS.message, { filePath }),
|
|
1892
|
+
data: { filePath }
|
|
1831
1893
|
});
|
|
1832
1894
|
}
|
|
1833
1895
|
return issues;
|
|
@@ -1841,13 +1903,12 @@ var LINK_CONFLICT_TEXTS = {
|
|
|
1841
1903
|
};
|
|
1842
1904
|
|
|
1843
1905
|
// plugins/core/analyzers/link-conflict/index.ts
|
|
1844
|
-
var
|
|
1906
|
+
var ID14 = "link-conflict";
|
|
1845
1907
|
var linkConflictAnalyzer = {
|
|
1846
|
-
id:
|
|
1908
|
+
id: ID14,
|
|
1847
1909
|
pluginId: "core",
|
|
1848
1910
|
kind: "analyzer",
|
|
1849
|
-
|
|
1850
|
-
description: 'Detects and flags conflicting arrow meanings between extractors (e.g. "references" vs "invokes").',
|
|
1911
|
+
description: "Flags conflicting arrow meanings between extractors (e.g. `references` vs `invokes`).",
|
|
1851
1912
|
mode: "deterministic",
|
|
1852
1913
|
// Bucket links by (source, target), then per-bucket detect distinct
|
|
1853
1914
|
// kinds. The branching is intrinsic to the per-bucket conflict
|
|
@@ -1891,7 +1952,7 @@ var linkConflictAnalyzer = {
|
|
|
1891
1952
|
const [source, target] = key.split("\0");
|
|
1892
1953
|
const kindList = variants.map((v) => v.kind).join(" / ");
|
|
1893
1954
|
issues.push({
|
|
1894
|
-
analyzerId:
|
|
1955
|
+
analyzerId: ID14,
|
|
1895
1956
|
severity: "warn",
|
|
1896
1957
|
nodeIds: [source, target],
|
|
1897
1958
|
message: tx(LINK_CONFLICT_TEXTS.message, {
|
|
@@ -1957,13 +2018,12 @@ function resolveLinkTargetToPath(link, nameIndex) {
|
|
|
1957
2018
|
return resolved ?? raw;
|
|
1958
2019
|
}
|
|
1959
2020
|
|
|
1960
|
-
// plugins/core/analyzers/link-
|
|
1961
|
-
var
|
|
1962
|
-
var
|
|
1963
|
-
id:
|
|
2021
|
+
// plugins/core/analyzers/link-counter/index.ts
|
|
2022
|
+
var ID15 = "link-counter";
|
|
2023
|
+
var linkCounterAnalyzer = {
|
|
2024
|
+
id: ID15,
|
|
1964
2025
|
pluginId: CORE_PLUGIN_ID,
|
|
1965
2026
|
kind: "analyzer",
|
|
1966
|
-
version: "1.0.0",
|
|
1967
2027
|
description: "Counts incoming and outgoing links per node.",
|
|
1968
2028
|
mode: "deterministic",
|
|
1969
2029
|
ui: {
|
|
@@ -2024,154 +2084,63 @@ function formatBreakdown(byKind, direction) {
|
|
|
2024
2084
|
return [direction, ...lines].join("\n");
|
|
2025
2085
|
}
|
|
2026
2086
|
|
|
2027
|
-
// plugins/core/analyzers/
|
|
2028
|
-
var
|
|
2087
|
+
// plugins/core/analyzers/link-self-loop/text.ts
|
|
2088
|
+
var LINK_SELF_LOOP_TEXTS = {
|
|
2029
2089
|
/**
|
|
2030
|
-
*
|
|
2031
|
-
*
|
|
2032
|
-
*
|
|
2090
|
+
* Per-edge warn: a node body references itself via the slash /
|
|
2091
|
+
* at-directive / markdown-link surface (most commonly because the
|
|
2092
|
+
* file's heading IS the invocation token, e.g. `# /deploy` inside
|
|
2093
|
+
* `commands/deploy.md`). The link is structurally valid but rarely
|
|
2094
|
+
* the operator's intent; UI consumers MAY hide it by default and
|
|
2095
|
+
* surface a count.
|
|
2033
2096
|
*/
|
|
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)"
|
|
2097
|
+
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
2098
|
};
|
|
2042
2099
|
|
|
2043
|
-
// plugins/core/analyzers/
|
|
2044
|
-
var
|
|
2045
|
-
var
|
|
2046
|
-
id:
|
|
2100
|
+
// plugins/core/analyzers/link-self-loop/index.ts
|
|
2101
|
+
var ID16 = "link-self-loop";
|
|
2102
|
+
var linkSelfLoopAnalyzer = {
|
|
2103
|
+
id: ID16,
|
|
2047
2104
|
pluginId: CORE_PLUGIN_ID,
|
|
2048
2105
|
kind: "analyzer",
|
|
2049
|
-
|
|
2050
|
-
description: "Flags when one node references the same resolved target via two or more syntactic surfaces (cross-extractor multi-form OR cross-kind multi-edge). Emits a warn on the source listing every occurrence (kind + trigger + line). Helps consolidate authorial redundancy and avoid duplicate runtime inlining.",
|
|
2106
|
+
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
2107
|
mode: "deterministic",
|
|
2052
2108
|
evaluate(ctx) {
|
|
2053
2109
|
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
2110
|
const issues = [];
|
|
2067
|
-
for (const
|
|
2068
|
-
|
|
2069
|
-
if (totalOccurrences < 2) continue;
|
|
2070
|
-
const [source, resolvedTarget] = key.split("\0");
|
|
2071
|
-
const flat = flattenOccurrences(links);
|
|
2111
|
+
for (const link of ctx.links) {
|
|
2112
|
+
if (!isSelfLoop(link)) continue;
|
|
2072
2113
|
issues.push({
|
|
2073
|
-
analyzerId:
|
|
2114
|
+
analyzerId: ID16,
|
|
2074
2115
|
severity: "warn",
|
|
2075
|
-
nodeIds: [source],
|
|
2076
|
-
message: tx(
|
|
2077
|
-
source,
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
occurrences: flat.map(formatOccurrence).join(REDUNDANT_TARGET_REFERENCE_TEXTS.occurrenceSeparator)
|
|
2116
|
+
nodeIds: [link.source],
|
|
2117
|
+
message: tx(LINK_SELF_LOOP_TEXTS.message, {
|
|
2118
|
+
source: link.source,
|
|
2119
|
+
trigger: link.trigger?.originalTrigger ?? link.target,
|
|
2120
|
+
kind: link.kind
|
|
2081
2121
|
}),
|
|
2082
2122
|
data: {
|
|
2083
|
-
target:
|
|
2084
|
-
resolvedTarget,
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
}))
|
|
2123
|
+
target: link.target,
|
|
2124
|
+
resolvedTarget: link.resolvedTarget ?? link.target,
|
|
2125
|
+
kind: link.kind,
|
|
2126
|
+
// Mark explicitly so UI / downstream consumers can read this
|
|
2127
|
+
// single field instead of re-computing the `source === target`
|
|
2128
|
+
// predicate themselves.
|
|
2129
|
+
selfLoop: true
|
|
2091
2130
|
}
|
|
2092
2131
|
});
|
|
2093
2132
|
}
|
|
2094
2133
|
return issues;
|
|
2095
2134
|
}
|
|
2096
2135
|
};
|
|
2097
|
-
function
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
for (const occ of link.occurrences) {
|
|
2102
|
-
out.push({
|
|
2103
|
-
kind: link.kind,
|
|
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
|
-
});
|
|
2118
|
-
}
|
|
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
|
-
return out;
|
|
2126
|
-
}
|
|
2127
|
-
function formatOccurrence(occ) {
|
|
2128
|
-
if (occ.line === null) {
|
|
2129
|
-
return tx(REDUNDANT_TARGET_REFERENCE_TEXTS.occurrenceUnknownLine, { trigger: occ.originalTrigger, kind: occ.kind });
|
|
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;
|
|
2136
|
+
function isSelfLoop(link) {
|
|
2137
|
+
if (link.source === link.target) return true;
|
|
2138
|
+
if (link.resolvedTarget && link.source === link.resolvedTarget) return true;
|
|
2139
|
+
return false;
|
|
2171
2140
|
}
|
|
2172
2141
|
|
|
2173
2142
|
// kernel/orchestrator/node-identifiers.ts
|
|
2174
|
-
import { posix as
|
|
2143
|
+
import { posix as pathPosix3 } from "path";
|
|
2175
2144
|
function deriveNodeIdentifiers(node, kindDescriptor) {
|
|
2176
2145
|
const sources = kindDescriptor?.identifiers;
|
|
2177
2146
|
if (!sources || sources.length === 0) return [];
|
|
@@ -2195,16 +2164,16 @@ function readFrontmatterName(node) {
|
|
|
2195
2164
|
return raw.length > 0 ? raw : null;
|
|
2196
2165
|
}
|
|
2197
2166
|
function readFilenameBasename(node) {
|
|
2198
|
-
const base =
|
|
2167
|
+
const base = pathPosix3.basename(node.path);
|
|
2199
2168
|
if (!base) return null;
|
|
2200
|
-
const ext =
|
|
2169
|
+
const ext = pathPosix3.extname(base);
|
|
2201
2170
|
const stem = ext ? base.slice(0, -ext.length) : base;
|
|
2202
2171
|
return stem.length > 0 ? stem : null;
|
|
2203
2172
|
}
|
|
2204
2173
|
function readDirname(node) {
|
|
2205
|
-
const dir =
|
|
2174
|
+
const dir = pathPosix3.dirname(node.path);
|
|
2206
2175
|
if (!dir || dir === "." || dir === "/") return null;
|
|
2207
|
-
const base =
|
|
2176
|
+
const base = pathPosix3.basename(dir);
|
|
2208
2177
|
return base.length > 0 ? base : null;
|
|
2209
2178
|
}
|
|
2210
2179
|
|
|
@@ -2246,10 +2215,9 @@ function resolveByName(link, indexes, ctx) {
|
|
|
2246
2215
|
const winner = candidates.find((c) => allowedKinds.includes(c.kind));
|
|
2247
2216
|
return winner ? winner.path : "none";
|
|
2248
2217
|
}
|
|
2249
|
-
function lookupAllowedKinds(link,
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
return ctx.providerResolution.get(sourceNode.provider)?.[link.kind];
|
|
2218
|
+
function lookupAllowedKinds(link, _indexes, ctx) {
|
|
2219
|
+
if (ctx.activeProvider === null) return void 0;
|
|
2220
|
+
return ctx.providerResolution.get(ctx.activeProvider)?.[link.kind];
|
|
2253
2221
|
}
|
|
2254
2222
|
function stripTriggerSigil(normalized) {
|
|
2255
2223
|
if (!normalized) return null;
|
|
@@ -2273,8 +2241,8 @@ function kindKey(node) {
|
|
|
2273
2241
|
return `${node.provider}/${node.kind}`;
|
|
2274
2242
|
}
|
|
2275
2243
|
|
|
2276
|
-
// plugins/core/analyzers/reserved
|
|
2277
|
-
var
|
|
2244
|
+
// plugins/core/analyzers/name-reserved/text.ts
|
|
2245
|
+
var NAME_RESERVED_TEXTS = {
|
|
2278
2246
|
/**
|
|
2279
2247
|
* Target-side message: emitted on the user file that collides with
|
|
2280
2248
|
* a runtime built-in. Same wording skill-map shipped before the
|
|
@@ -2291,14 +2259,13 @@ var RESERVED_NAME_TEXTS = {
|
|
|
2291
2259
|
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
2260
|
};
|
|
2293
2261
|
|
|
2294
|
-
// plugins/core/analyzers/reserved
|
|
2295
|
-
var
|
|
2296
|
-
var
|
|
2297
|
-
id:
|
|
2262
|
+
// plugins/core/analyzers/name-reserved/index.ts
|
|
2263
|
+
var ID17 = "name-reserved";
|
|
2264
|
+
var nameReservedAnalyzer = {
|
|
2265
|
+
id: ID17,
|
|
2298
2266
|
pluginId: CORE_PLUGIN_ID,
|
|
2299
2267
|
kind: "analyzer",
|
|
2300
|
-
|
|
2301
|
-
description: "Flags reserved-name collisions on two surfaces. Target side: a user file whose name collides with a Provider runtime's built-in invocable (the runtime shadows the file silently). Source side: a link that resolves to a reserved name, which the post-walk lift transform downgrades to the sentinel `RESERVED_TARGET_CONFIDENCE` (0.1). The two findings share the analyzer id so consumers can group by root cause; the source-side issue carries `data.target` matching the link so UIs can correlate per-row.",
|
|
2268
|
+
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
2269
|
mode: "deterministic",
|
|
2303
2270
|
// eslint-disable-next-line complexity
|
|
2304
2271
|
evaluate(ctx) {
|
|
@@ -2311,10 +2278,10 @@ var reservedNameAnalyzer = {
|
|
|
2311
2278
|
const node = byPath3.get(path);
|
|
2312
2279
|
if (!node) continue;
|
|
2313
2280
|
issues.push({
|
|
2314
|
-
analyzerId:
|
|
2281
|
+
analyzerId: ID17,
|
|
2315
2282
|
severity: "warn",
|
|
2316
2283
|
nodeIds: [node.path],
|
|
2317
|
-
message: tx(
|
|
2284
|
+
message: tx(NAME_RESERVED_TEXTS.message, {
|
|
2318
2285
|
path: node.path,
|
|
2319
2286
|
provider: node.provider,
|
|
2320
2287
|
kind: node.kind
|
|
@@ -2327,10 +2294,10 @@ var reservedNameAnalyzer = {
|
|
|
2327
2294
|
const reservedNode = findReservedNodeForLink(link, reserved, byPath3);
|
|
2328
2295
|
if (!reservedNode) continue;
|
|
2329
2296
|
issues.push({
|
|
2330
|
-
analyzerId:
|
|
2297
|
+
analyzerId: ID17,
|
|
2331
2298
|
severity: "warn",
|
|
2332
2299
|
nodeIds: [link.source],
|
|
2333
|
-
message: tx(
|
|
2300
|
+
message: tx(NAME_RESERVED_TEXTS.linkMessage, {
|
|
2334
2301
|
kind: link.kind,
|
|
2335
2302
|
target: link.target,
|
|
2336
2303
|
provider: reservedNode.provider,
|
|
@@ -2387,221 +2354,33 @@ function normaliseId(raw) {
|
|
|
2387
2354
|
return raw.normalize("NFD").replace(new RegExp("\\p{Mn}+", "gu"), "").toLowerCase().replace(/[-_\s]+/g, " ").replace(/ +/g, " ").trim();
|
|
2388
2355
|
}
|
|
2389
2356
|
|
|
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";
|
|
2357
|
+
// plugins/core/analyzers/node-stability/index.ts
|
|
2358
|
+
var ID18 = "node-stability";
|
|
2582
2359
|
var EXPERIMENTAL_TOOLTIP = "Experimental: API may change";
|
|
2583
2360
|
var DEPRECATED_TOOLTIP = "Deprecated: avoid in new code";
|
|
2584
|
-
var
|
|
2585
|
-
id:
|
|
2361
|
+
var nodeStabilityAnalyzer = {
|
|
2362
|
+
id: ID18,
|
|
2586
2363
|
pluginId: CORE_PLUGIN_ID,
|
|
2587
2364
|
kind: "analyzer",
|
|
2588
|
-
|
|
2589
|
-
description: "Reports node lifecycle stage (`experimental`, `deprecated`) on the card.",
|
|
2365
|
+
description: "Reports a node's stability stage (`experimental`, `deprecated`) on the card.",
|
|
2590
2366
|
mode: "deterministic",
|
|
2591
2367
|
ui: {
|
|
2368
|
+
// Second in the footer-right cluster, after the drift chip and
|
|
2369
|
+
// before the severity counters. Stability is a state badge, not a
|
|
2370
|
+
// count, so its priority sits between the two semantic zones.
|
|
2592
2371
|
experimental: {
|
|
2593
2372
|
slot: "card.footer.right",
|
|
2594
2373
|
icon: "fa-solid fa-flask",
|
|
2595
2374
|
label: "experimental",
|
|
2596
2375
|
emitWhenEmpty: false,
|
|
2597
|
-
priority:
|
|
2376
|
+
priority: 20
|
|
2598
2377
|
},
|
|
2599
2378
|
deprecated: {
|
|
2600
2379
|
slot: "card.footer.right",
|
|
2601
2380
|
icon: "pi-ban",
|
|
2602
2381
|
label: "deprecated",
|
|
2603
2382
|
emitWhenEmpty: false,
|
|
2604
|
-
priority:
|
|
2383
|
+
priority: 20
|
|
2605
2384
|
}
|
|
2606
2385
|
},
|
|
2607
2386
|
evaluate(ctx) {
|
|
@@ -2614,7 +2393,7 @@ var stabilityAnalyzer = {
|
|
|
2614
2393
|
tooltip: EXPERIMENTAL_TOOLTIP
|
|
2615
2394
|
});
|
|
2616
2395
|
issues.push({
|
|
2617
|
-
analyzerId:
|
|
2396
|
+
analyzerId: ID18,
|
|
2618
2397
|
severity: "info",
|
|
2619
2398
|
nodeIds: [node.path],
|
|
2620
2399
|
message: `Node '${node.path}' is marked experimental: API may change.`,
|
|
@@ -2627,7 +2406,7 @@ var stabilityAnalyzer = {
|
|
|
2627
2406
|
severity: "warn"
|
|
2628
2407
|
});
|
|
2629
2408
|
issues.push({
|
|
2630
|
-
analyzerId:
|
|
2409
|
+
analyzerId: ID18,
|
|
2631
2410
|
severity: "warn",
|
|
2632
2411
|
nodeIds: [node.path],
|
|
2633
2412
|
message: `Node '${node.path}' is marked deprecated: avoid in new code.`,
|
|
@@ -2654,20 +2433,19 @@ function isStability(value) {
|
|
|
2654
2433
|
return value === "experimental" || value === "deprecated" || value === "stable";
|
|
2655
2434
|
}
|
|
2656
2435
|
|
|
2657
|
-
// plugins/core/analyzers/superseded/text.ts
|
|
2658
|
-
var
|
|
2436
|
+
// plugins/core/analyzers/node-superseded/text.ts
|
|
2437
|
+
var NODE_SUPERSEDED_TEXTS = {
|
|
2659
2438
|
/** `<path> is superseded by <supersededBy>` */
|
|
2660
2439
|
message: "{{path}} is superseded by {{supersededBy}}"
|
|
2661
2440
|
};
|
|
2662
2441
|
|
|
2663
|
-
// plugins/core/analyzers/superseded/index.ts
|
|
2664
|
-
var
|
|
2665
|
-
var
|
|
2666
|
-
id:
|
|
2442
|
+
// plugins/core/analyzers/node-superseded/index.ts
|
|
2443
|
+
var ID19 = "node-superseded";
|
|
2444
|
+
var nodeSupersededAnalyzer = {
|
|
2445
|
+
id: ID19,
|
|
2667
2446
|
pluginId: CORE_PLUGIN_ID,
|
|
2668
2447
|
kind: "analyzer",
|
|
2669
|
-
|
|
2670
|
-
description: "Detects and marks nodes replaced by a newer one via `supersededBy`.",
|
|
2448
|
+
description: "Marks nodes replaced by a newer one via `supersededBy`.",
|
|
2671
2449
|
mode: "deterministic",
|
|
2672
2450
|
evaluate(ctx) {
|
|
2673
2451
|
const issues = [];
|
|
@@ -2675,10 +2453,10 @@ var supersededAnalyzer = {
|
|
|
2675
2453
|
const supersededBy = pickSupersededBy(node);
|
|
2676
2454
|
if (supersededBy === null) continue;
|
|
2677
2455
|
issues.push({
|
|
2678
|
-
analyzerId:
|
|
2456
|
+
analyzerId: ID19,
|
|
2679
2457
|
severity: "info",
|
|
2680
2458
|
nodeIds: [node.path],
|
|
2681
|
-
message: tx(
|
|
2459
|
+
message: tx(NODE_SUPERSEDED_TEXTS.message, {
|
|
2682
2460
|
path: node.path,
|
|
2683
2461
|
supersededBy
|
|
2684
2462
|
}),
|
|
@@ -2698,351 +2476,307 @@ function pickSupersededBy(node) {
|
|
|
2698
2476
|
return value;
|
|
2699
2477
|
}
|
|
2700
2478
|
|
|
2701
|
-
// plugins/core/analyzers/
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
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}}"
|
|
2479
|
+
// plugins/core/analyzers/reference-broken/index.ts
|
|
2480
|
+
import { posix as pathPosix4, resolve as resolve3 } from "path";
|
|
2481
|
+
|
|
2482
|
+
// plugins/core/analyzers/reference-broken/text.ts
|
|
2483
|
+
var REFERENCE_BROKEN_TEXTS = {
|
|
2484
|
+
/** `Broken <kind> reference from <source> → <target>` */
|
|
2485
|
+
message: "Broken {{kind}} reference from {{source}} \u2192 {{target}}",
|
|
2486
|
+
// Tooltips for the per-node view-contribution badges. Singular vs
|
|
2487
|
+
// plural keeps the count grammar correct without a sub-template.
|
|
2488
|
+
alertTooltipSingle: "This node has a broken reference. Open the inspector for details.",
|
|
2489
|
+
alertTooltipMany: "This node has {{count}} broken references. Open the inspector for details.",
|
|
2490
|
+
// Fix-summary copy when the broken trigger has a same-named file on
|
|
2491
|
+
// disk that does not advertise `name:` in its frontmatter. Two
|
|
2492
|
+
// variants for single vs multiple candidates; same template family
|
|
2493
|
+
// as the alert tooltips above.
|
|
2494
|
+
hintSummarySingle: "Add `name: {{name}}` to the frontmatter of {{candidate}} so this reference resolves.",
|
|
2495
|
+
hintSummaryMany: "Add `name: {{name}}` to the frontmatter of one of these files so this reference resolves: {{candidates}}."
|
|
2724
2496
|
};
|
|
2725
2497
|
|
|
2726
|
-
// plugins/core/analyzers/
|
|
2727
|
-
var
|
|
2728
|
-
var
|
|
2729
|
-
|
|
2730
|
-
"skill",
|
|
2731
|
-
"agent"
|
|
2732
|
-
]);
|
|
2733
|
-
var triggerCollisionAnalyzer = {
|
|
2734
|
-
id: ID21,
|
|
2498
|
+
// plugins/core/analyzers/reference-broken/index.ts
|
|
2499
|
+
var ID20 = "reference-broken";
|
|
2500
|
+
var referenceBrokenAnalyzer = {
|
|
2501
|
+
id: ID20,
|
|
2735
2502
|
pluginId: CORE_PLUGIN_ID,
|
|
2736
2503
|
kind: "analyzer",
|
|
2504
|
+
description: "Flags arrows pointing at a node not part of the current scan.",
|
|
2737
2505
|
mode: "deterministic",
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
//
|
|
2741
|
-
//
|
|
2742
|
-
|
|
2506
|
+
// No `ui` declaration: this analyzer used to emit a per-finding
|
|
2507
|
+
// counter chip on `card.footer.right`, but that chip duplicated the
|
|
2508
|
+
// aggregate severity counters now owned by `core/issue-counter`. The
|
|
2509
|
+
// detection logic stays intact, only the chip emission is gone.
|
|
2510
|
+
ui: {},
|
|
2511
|
+
// The resolver, the reference-paths escape hatch, and the hint
|
|
2512
|
+
// index all share the per-link loop, splitting would re-walk
|
|
2513
|
+
// `ctx.links` once per concern. The per-source aggregation that
|
|
2514
|
+
// historically lived alongside (driving the now-retired chip
|
|
2515
|
+
// emission) moved into `core/issue-counter`.
|
|
2743
2516
|
evaluate(ctx) {
|
|
2744
|
-
const
|
|
2745
|
-
const
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
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
|
-
}
|
|
2517
|
+
const byPath3 = new Set(ctx.nodes.map((n) => n.path));
|
|
2518
|
+
const byNormalizedName = indexByNormalizedName(ctx.nodes);
|
|
2519
|
+
const byBasenameWithoutName = indexByBasenameWithoutName(ctx.nodes);
|
|
2520
|
+
const refIndex = ctx.referenceablePaths && ctx.referenceablePaths.size > 0 && ctx.cwd ? { paths: ctx.referenceablePaths, cwd: ctx.cwd } : null;
|
|
2521
|
+
const issues = [];
|
|
2763
2522
|
for (const link of ctx.links) {
|
|
2764
|
-
|
|
2765
|
-
if (
|
|
2766
|
-
|
|
2767
|
-
|
|
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);
|
|
2523
|
+
if (isResolved(link, byPath3, byNormalizedName)) continue;
|
|
2524
|
+
if (refIndex && resolvesViaReferencePaths(link, refIndex)) continue;
|
|
2525
|
+
const candidates = findHintCandidates(link, byBasenameWithoutName);
|
|
2526
|
+
issues.push(buildIssue(link, candidates));
|
|
2776
2527
|
}
|
|
2777
2528
|
return issues;
|
|
2778
2529
|
}
|
|
2779
2530
|
};
|
|
2780
|
-
function
|
|
2781
|
-
const
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2531
|
+
function buildIssue(link, hintCandidates = []) {
|
|
2532
|
+
const data = {
|
|
2533
|
+
target: link.target,
|
|
2534
|
+
kind: link.kind,
|
|
2535
|
+
trigger: link.trigger?.normalizedTrigger ?? null
|
|
2536
|
+
};
|
|
2537
|
+
const issue = {
|
|
2538
|
+
analyzerId: ID20,
|
|
2539
|
+
// `error`, not `warn`: a link whose target is not in the scan is a
|
|
2540
|
+
// structural defect the operator must notice, and the card chip
|
|
2541
|
+
// paints `danger` (red) to match. Per the chip-vs-issue policy in
|
|
2542
|
+
// `context/view-slots.md`, a `danger` chip MUST be backed by an
|
|
2543
|
+
// `error` Issue so the visual signal lines up with the exit code
|
|
2544
|
+
// and the global error count on the card.
|
|
2545
|
+
severity: "error",
|
|
2546
|
+
nodeIds: [link.source],
|
|
2547
|
+
message: tx(REFERENCE_BROKEN_TEXTS.message, {
|
|
2548
|
+
kind: link.kind,
|
|
2549
|
+
source: link.source,
|
|
2550
|
+
target: link.target
|
|
2551
|
+
}),
|
|
2552
|
+
data
|
|
2553
|
+
};
|
|
2554
|
+
if (hintCandidates.length > 0) {
|
|
2555
|
+
const suggestedName = (link.trigger?.normalizedTrigger ?? "").replace(/^[/@]/, "").trim();
|
|
2556
|
+
const candidatePaths = hintCandidates.map((n) => n.path);
|
|
2557
|
+
data["hint"] = {
|
|
2558
|
+
kind: "missing-frontmatter-name",
|
|
2559
|
+
suggestedName,
|
|
2560
|
+
candidates: candidatePaths
|
|
2561
|
+
};
|
|
2562
|
+
issue.fix = {
|
|
2563
|
+
summary: candidatePaths.length === 1 ? tx(REFERENCE_BROKEN_TEXTS.hintSummarySingle, {
|
|
2564
|
+
name: suggestedName,
|
|
2565
|
+
candidate: candidatePaths[0]
|
|
2566
|
+
}) : tx(REFERENCE_BROKEN_TEXTS.hintSummaryMany, {
|
|
2567
|
+
name: suggestedName,
|
|
2568
|
+
candidates: candidatePaths.join(", ")
|
|
2569
|
+
}),
|
|
2570
|
+
autofixable: false
|
|
2571
|
+
};
|
|
2797
2572
|
}
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2573
|
+
return issue;
|
|
2574
|
+
}
|
|
2575
|
+
function resolvesViaReferencePaths(link, refIndex) {
|
|
2576
|
+
if (!isPathStyleLink(link)) return false;
|
|
2577
|
+
return refIndex.paths.has(resolve3(refIndex.cwd, link.target));
|
|
2578
|
+
}
|
|
2579
|
+
function indexByNormalizedName(nodes) {
|
|
2580
|
+
const out = /* @__PURE__ */ new Map();
|
|
2581
|
+
for (const node of nodes) {
|
|
2582
|
+
const raw = node.frontmatter?.["name"];
|
|
2583
|
+
const name = typeof raw === "string" ? raw : "";
|
|
2584
|
+
if (!name) continue;
|
|
2585
|
+
const key = normalizeTrigger(name);
|
|
2586
|
+
const bucket = out.get(key) ?? [];
|
|
2587
|
+
bucket.push(node);
|
|
2588
|
+
out.set(key, bucket);
|
|
2807
2589
|
}
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
);
|
|
2590
|
+
return out;
|
|
2591
|
+
}
|
|
2592
|
+
function basenameWithoutExt(path) {
|
|
2593
|
+
const base = pathPosix4.basename(path);
|
|
2594
|
+
const ext = pathPosix4.extname(base);
|
|
2595
|
+
return ext ? base.slice(0, -ext.length) : base;
|
|
2596
|
+
}
|
|
2597
|
+
function indexByBasenameWithoutName(nodes) {
|
|
2598
|
+
const out = /* @__PURE__ */ new Map();
|
|
2599
|
+
for (const node of nodes) {
|
|
2600
|
+
const raw = node.frontmatter?.["name"];
|
|
2601
|
+
const name = typeof raw === "string" ? raw : "";
|
|
2602
|
+
if (name) continue;
|
|
2603
|
+
const bare = basenameWithoutExt(node.path);
|
|
2604
|
+
if (!bare) continue;
|
|
2605
|
+
const key = normalizeTrigger(bare);
|
|
2606
|
+
if (!key) continue;
|
|
2607
|
+
const bucket = out.get(key) ?? [];
|
|
2608
|
+
bucket.push(node);
|
|
2609
|
+
out.set(key, bucket);
|
|
2823
2610
|
}
|
|
2824
|
-
|
|
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
|
-
};
|
|
2611
|
+
return out;
|
|
2843
2612
|
}
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2613
|
+
function findHintCandidates(link, idx) {
|
|
2614
|
+
const normalized = link.trigger?.normalizedTrigger;
|
|
2615
|
+
if (!normalized) return [];
|
|
2616
|
+
const sigil = normalized.charAt(0);
|
|
2617
|
+
if (sigil !== "/" && sigil !== "@") return [];
|
|
2618
|
+
const withoutSigil = normalized.slice(1).trim();
|
|
2619
|
+
if (!withoutSigil) return [];
|
|
2620
|
+
return idx.get(withoutSigil) ?? [];
|
|
2621
|
+
}
|
|
2622
|
+
function isResolved(link, byPath3, byNormalizedName) {
|
|
2623
|
+
const normalized = link.trigger?.normalizedTrigger;
|
|
2624
|
+
if (normalized) {
|
|
2625
|
+
const withoutSigil = normalized.replace(/^[/@]/, "").trim();
|
|
2626
|
+
if (byNormalizedName.has(withoutSigil)) return true;
|
|
2627
|
+
}
|
|
2628
|
+
if (byPath3.has(link.target)) return true;
|
|
2629
|
+
return false;
|
|
2630
|
+
}
|
|
2631
|
+
function isPathStyleLink(link) {
|
|
2632
|
+
const sigil = link.trigger?.normalizedTrigger?.charAt(0);
|
|
2633
|
+
if (sigil === "/" || sigil === "@") return false;
|
|
2634
|
+
return true;
|
|
2856
2635
|
}
|
|
2857
2636
|
|
|
2858
|
-
// plugins/core/analyzers/
|
|
2859
|
-
var
|
|
2860
|
-
/**
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2637
|
+
// plugins/core/analyzers/reference-redundant/text.ts
|
|
2638
|
+
var REFERENCE_REDUNDANT_TEXTS = {
|
|
2639
|
+
/**
|
|
2640
|
+
* Multi-form / multi-occurrence reference message. Lists each
|
|
2641
|
+
* occurrence (trigger + line) so the operator sees the full
|
|
2642
|
+
* authorial surface without having to grep the body.
|
|
2643
|
+
*/
|
|
2644
|
+
message: "{{source}} references {{resolvedTarget}} via {{count}} occurrences: {{occurrences}}. Consider consolidating to a single form to reduce maintenance surface and avoid duplicate inlining at runtime.",
|
|
2645
|
+
/** Inline separator between occurrences in the message. */
|
|
2646
|
+
occurrenceSeparator: ", ",
|
|
2647
|
+
/** Per-occurrence formatting (trigger + line). */
|
|
2648
|
+
occurrence: "`{{trigger}}` ({{kind}}, line {{line}})",
|
|
2649
|
+
/** Per-occurrence formatting when the extractor did not record a line. */
|
|
2650
|
+
occurrenceUnknownLine: "`{{trigger}}` ({{kind}}, unknown line)"
|
|
2870
2651
|
};
|
|
2871
2652
|
|
|
2872
|
-
// plugins/core/analyzers/
|
|
2873
|
-
var
|
|
2874
|
-
var
|
|
2875
|
-
|
|
2876
|
-
id: ID22,
|
|
2653
|
+
// plugins/core/analyzers/reference-redundant/index.ts
|
|
2654
|
+
var ID21 = "reference-redundant";
|
|
2655
|
+
var referenceRedundantAnalyzer = {
|
|
2656
|
+
id: ID21,
|
|
2877
2657
|
pluginId: CORE_PLUGIN_ID,
|
|
2878
2658
|
kind: "analyzer",
|
|
2879
|
-
|
|
2880
|
-
description: "Detects and flags typos or unrecognized keys in sidecars (`.sm`).",
|
|
2659
|
+
description: "Flags when one node references the same target through two or more different links (e.g. a markdown link plus a `references:` entry).",
|
|
2881
2660
|
mode: "deterministic",
|
|
2882
|
-
ui: {
|
|
2883
|
-
// Corner badge on the graph card; count omitted when there is a
|
|
2884
|
-
// single unknown field (avoids a noisy "icon + 1" chip).
|
|
2885
|
-
alert: {
|
|
2886
|
-
slot: "graph.node.alert",
|
|
2887
|
-
// Filled warning triangle on the corner, matches the broken-ref
|
|
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",
|
|
2891
|
-
emitWhenEmpty: false
|
|
2892
|
-
},
|
|
2893
|
-
// Footer chip on the card, `_counter` shape but rendered icon-only
|
|
2894
|
-
// (the analyzer emits `value: 0` so NodeCounter hides the number
|
|
2895
|
-
// and only the glyph shows). PrimeIcons `pi-question-circle` so the
|
|
2896
|
-
// visual weight matches `annotation-stale`'s `pi-clock` chip
|
|
2897
|
-
// sitting next to it on the same footer row. `emitWhenEmpty: true`
|
|
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.
|
|
2900
|
-
chip: {
|
|
2901
|
-
slot: "card.footer.right",
|
|
2902
|
-
icon: "pi-question-circle",
|
|
2903
|
-
emitWhenEmpty: true,
|
|
2904
|
-
priority: 30
|
|
2905
|
-
}
|
|
2906
|
-
},
|
|
2907
|
-
// Analyzer body iterates every sidecar root and classifies each
|
|
2908
|
-
// key against three buckets (catalog / plugin namespace / unknown
|
|
2909
|
-
// root). The per-key branching IS the classification table; factoring
|
|
2910
|
-
// it out would rebuild the discriminator elsewhere. Per
|
|
2911
|
-
// `context/lint.md` category 7 (recursive type-discriminator walkers).
|
|
2912
|
-
// eslint-disable-next-line complexity
|
|
2913
2661
|
evaluate(ctx) {
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
const
|
|
2917
|
-
const
|
|
2918
|
-
const
|
|
2919
|
-
const
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
for (const node of ctx.nodes) {
|
|
2927
|
-
const root = sidecarRoots.get(node.path);
|
|
2928
|
-
if (!root) continue;
|
|
2929
|
-
const annotations = root["annotations"];
|
|
2930
|
-
if (annotations !== void 0 && annotations !== null && typeof annotations === "object" && !Array.isArray(annotations)) {
|
|
2931
|
-
for (const key of Object.keys(annotations)) {
|
|
2932
|
-
if (!knownAnnotationKeys.has(key)) {
|
|
2933
|
-
issues.push({
|
|
2934
|
-
analyzerId: ID22,
|
|
2935
|
-
severity: "warn",
|
|
2936
|
-
nodeIds: [node.path],
|
|
2937
|
-
message: tx(UNKNOWN_FIELD_TEXTS.unknownAnnotationKey, {
|
|
2938
|
-
path: node.path,
|
|
2939
|
-
key
|
|
2940
|
-
}),
|
|
2941
|
-
data: { surface: "annotations", key }
|
|
2942
|
-
});
|
|
2943
|
-
bump2(node.path);
|
|
2944
|
-
}
|
|
2945
|
-
}
|
|
2946
|
-
}
|
|
2947
|
-
for (const key of Object.keys(root)) {
|
|
2948
|
-
if (RESERVED_ROOT_BLOCKS.has(key)) continue;
|
|
2949
|
-
if (rootKeys.has(key)) continue;
|
|
2950
|
-
if (knownPluginIds.has(key)) {
|
|
2951
|
-
const block = root[key];
|
|
2952
|
-
if (block === null || typeof block !== "object" || Array.isArray(block)) continue;
|
|
2953
|
-
const contribsForPlugin = namespacedByPlugin.get(key);
|
|
2954
|
-
if (!contribsForPlugin) continue;
|
|
2955
|
-
for (const [contribKey, validator] of contribsForPlugin) {
|
|
2956
|
-
const value = block[contribKey];
|
|
2957
|
-
if (value === void 0) continue;
|
|
2958
|
-
if (validator(value)) continue;
|
|
2959
|
-
const errors = (validator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
|
|
2960
|
-
issues.push({
|
|
2961
|
-
analyzerId: ID22,
|
|
2962
|
-
severity: "warn",
|
|
2963
|
-
nodeIds: [node.path],
|
|
2964
|
-
message: tx(UNKNOWN_FIELD_TEXTS.pluginNamespaceInvalid, {
|
|
2965
|
-
path: node.path,
|
|
2966
|
-
pluginId: key,
|
|
2967
|
-
key: contribKey,
|
|
2968
|
-
errors
|
|
2969
|
-
}),
|
|
2970
|
-
data: { surface: "plugin-namespace", pluginId: key, key: contribKey }
|
|
2971
|
-
});
|
|
2972
|
-
bump2(node.path);
|
|
2973
|
-
}
|
|
2974
|
-
continue;
|
|
2975
|
-
}
|
|
2976
|
-
issues.push({
|
|
2977
|
-
analyzerId: ID22,
|
|
2978
|
-
severity: "warn",
|
|
2979
|
-
nodeIds: [node.path],
|
|
2980
|
-
message: tx(UNKNOWN_FIELD_TEXTS.unknownRootKey, {
|
|
2981
|
-
path: node.path,
|
|
2982
|
-
key
|
|
2983
|
-
}),
|
|
2984
|
-
data: { surface: "root", key }
|
|
2985
|
-
});
|
|
2986
|
-
bump2(node.path);
|
|
2987
|
-
}
|
|
2662
|
+
if (ctx.links.length === 0) return [];
|
|
2663
|
+
const byPath3 = /* @__PURE__ */ new Map();
|
|
2664
|
+
for (const node of ctx.nodes) byPath3.set(node.path, node);
|
|
2665
|
+
const byName = buildNameIndex2(ctx.nodes);
|
|
2666
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2667
|
+
for (const link of ctx.links) {
|
|
2668
|
+
const resolved = resolveTargetPath(link, byPath3, byName);
|
|
2669
|
+
if (!resolved) continue;
|
|
2670
|
+
const key = `${link.source}\0${resolved}`;
|
|
2671
|
+
const bucket = groups.get(key);
|
|
2672
|
+
if (bucket) bucket.push(link);
|
|
2673
|
+
else groups.set(key, [link]);
|
|
2988
2674
|
}
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
value: 0,
|
|
2675
|
+
const issues = [];
|
|
2676
|
+
for (const [key, links] of groups) {
|
|
2677
|
+
const totalOccurrences = links.reduce((acc, l) => acc + (l.occurrences?.length ?? 1), 0);
|
|
2678
|
+
if (totalOccurrences < 2) continue;
|
|
2679
|
+
const [source, resolvedTarget] = key.split("\0");
|
|
2680
|
+
const flat = flattenOccurrences(links);
|
|
2681
|
+
issues.push({
|
|
2682
|
+
analyzerId: ID21,
|
|
2998
2683
|
severity: "warn",
|
|
2999
|
-
|
|
2684
|
+
nodeIds: [source],
|
|
2685
|
+
message: tx(REFERENCE_REDUNDANT_TEXTS.message, {
|
|
2686
|
+
source,
|
|
2687
|
+
resolvedTarget,
|
|
2688
|
+
count: flat.length,
|
|
2689
|
+
occurrences: flat.map(formatOccurrence).join(REFERENCE_REDUNDANT_TEXTS.occurrenceSeparator)
|
|
2690
|
+
}),
|
|
2691
|
+
data: {
|
|
2692
|
+
target: resolvedTarget,
|
|
2693
|
+
resolvedTarget,
|
|
2694
|
+
occurrences: flat.map((o) => ({
|
|
2695
|
+
kind: o.kind,
|
|
2696
|
+
trigger: o.originalTrigger,
|
|
2697
|
+
line: o.line ?? null,
|
|
2698
|
+
extractor: o.extractor
|
|
2699
|
+
}))
|
|
2700
|
+
}
|
|
3000
2701
|
});
|
|
3001
2702
|
}
|
|
3002
2703
|
return issues;
|
|
3003
2704
|
}
|
|
3004
2705
|
};
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
2706
|
+
function flattenOccurrences(links) {
|
|
2707
|
+
const out = [];
|
|
2708
|
+
for (const link of links) {
|
|
2709
|
+
if (link.occurrences && link.occurrences.length > 0) {
|
|
2710
|
+
for (const occ of link.occurrences) {
|
|
2711
|
+
out.push({
|
|
2712
|
+
kind: link.kind,
|
|
2713
|
+
originalTrigger: occ.originalTrigger,
|
|
2714
|
+
extractor: occ.extractor,
|
|
2715
|
+
line: occ.location?.line ?? null
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
continue;
|
|
2719
|
+
}
|
|
2720
|
+
const trigger = link.trigger?.originalTrigger ?? link.target;
|
|
2721
|
+
out.push({
|
|
2722
|
+
kind: link.kind,
|
|
2723
|
+
originalTrigger: trigger,
|
|
2724
|
+
extractor: link.sources[0] ?? "unknown",
|
|
2725
|
+
line: link.location?.line ?? null
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2728
|
+
out.sort((a, b) => {
|
|
2729
|
+
const la = a.line ?? Number.MAX_SAFE_INTEGER;
|
|
2730
|
+
const lb = b.line ?? Number.MAX_SAFE_INTEGER;
|
|
2731
|
+
if (la !== lb) return la - lb;
|
|
2732
|
+
return a.originalTrigger.localeCompare(b.originalTrigger);
|
|
2733
|
+
});
|
|
2734
|
+
return out;
|
|
2735
|
+
}
|
|
2736
|
+
function formatOccurrence(occ) {
|
|
2737
|
+
if (occ.line === null) {
|
|
2738
|
+
return tx(REFERENCE_REDUNDANT_TEXTS.occurrenceUnknownLine, { trigger: occ.originalTrigger, kind: occ.kind });
|
|
2739
|
+
}
|
|
2740
|
+
return tx(REFERENCE_REDUNDANT_TEXTS.occurrence, { trigger: occ.originalTrigger, kind: occ.kind, line: occ.line });
|
|
3016
2741
|
}
|
|
3017
|
-
function
|
|
2742
|
+
function buildNameIndex2(nodes) {
|
|
3018
2743
|
const out = /* @__PURE__ */ new Map();
|
|
3019
|
-
const
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
out.set(entry.pluginId, bucket);
|
|
3027
|
-
}
|
|
3028
|
-
try {
|
|
3029
|
-
bucket.set(entry.key, ajv.compile(entry.schema));
|
|
3030
|
-
} catch {
|
|
2744
|
+
for (const node of nodes) {
|
|
2745
|
+
for (const candidate of collectIdentifiers(node)) {
|
|
2746
|
+
const normalised = normalizeTrigger(candidate);
|
|
2747
|
+
if (!normalised) continue;
|
|
2748
|
+
const bucket = out.get(normalised);
|
|
2749
|
+
if (bucket) bucket.push(node.path);
|
|
2750
|
+
else out.set(normalised, [node.path]);
|
|
3031
2751
|
}
|
|
3032
2752
|
}
|
|
3033
2753
|
return out;
|
|
3034
2754
|
}
|
|
3035
|
-
function
|
|
3036
|
-
const out =
|
|
3037
|
-
|
|
3038
|
-
|
|
2755
|
+
function collectIdentifiers(node) {
|
|
2756
|
+
const out = [];
|
|
2757
|
+
const fmName = node.frontmatter?.["name"];
|
|
2758
|
+
if (typeof fmName === "string" && fmName.length > 0) out.push(fmName);
|
|
2759
|
+
const segs = node.path.split("/");
|
|
2760
|
+
const last = segs[segs.length - 1] ?? "";
|
|
2761
|
+
if (last) {
|
|
2762
|
+
const stem = last.replace(/\.[^.]+$/, "");
|
|
2763
|
+
if (stem) out.push(stem);
|
|
2764
|
+
}
|
|
2765
|
+
if (segs.length >= 2) {
|
|
2766
|
+
const dirBase = segs[segs.length - 2];
|
|
2767
|
+
if (dirBase) out.push(dirBase);
|
|
3039
2768
|
}
|
|
3040
2769
|
return out;
|
|
3041
2770
|
}
|
|
3042
|
-
function
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
return
|
|
2771
|
+
function resolveTargetPath(link, byPath3, byName) {
|
|
2772
|
+
if (byPath3.has(link.target)) return link.target;
|
|
2773
|
+
const trigger = link.trigger?.normalizedTrigger;
|
|
2774
|
+
if (!trigger) return null;
|
|
2775
|
+
const stripped = trigger.replace(/^[/@]/, "").trim();
|
|
2776
|
+
if (!stripped) return null;
|
|
2777
|
+
const candidates = byName.get(stripped);
|
|
2778
|
+
if (!candidates || candidates.length === 0) return null;
|
|
2779
|
+
return candidates[0] ?? null;
|
|
3046
2780
|
}
|
|
3047
2781
|
|
|
3048
2782
|
// kernel/adapters/schema-validators.ts
|
|
@@ -3231,185 +2965,449 @@ function registerProviderAuxiliarySchemas(ajv, providers) {
|
|
|
3231
2965
|
ajv.addSchema(aux);
|
|
3232
2966
|
}
|
|
3233
2967
|
}
|
|
3234
|
-
}
|
|
3235
|
-
function resolveSpecRoot() {
|
|
3236
|
-
const require2 = createRequire2(import.meta.url);
|
|
3237
|
-
try {
|
|
3238
|
-
const indexPath = require2.resolve("@skill-map/spec/index.json");
|
|
3239
|
-
return dirname2(indexPath);
|
|
3240
|
-
} catch {
|
|
3241
|
-
throw new Error(
|
|
3242
|
-
"@skill-map/spec not resolvable: ensure the workspace is linked or the package is installed."
|
|
3243
|
-
);
|
|
2968
|
+
}
|
|
2969
|
+
function resolveSpecRoot() {
|
|
2970
|
+
const require2 = createRequire2(import.meta.url);
|
|
2971
|
+
try {
|
|
2972
|
+
const indexPath = require2.resolve("@skill-map/spec/index.json");
|
|
2973
|
+
return dirname2(indexPath);
|
|
2974
|
+
} catch {
|
|
2975
|
+
throw new Error(
|
|
2976
|
+
"@skill-map/spec not resolvable: ensure the workspace is linked or the package is installed."
|
|
2977
|
+
);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
function existsSyncSafe(path) {
|
|
2981
|
+
try {
|
|
2982
|
+
readFileSync2(path, "utf8");
|
|
2983
|
+
return true;
|
|
2984
|
+
} catch {
|
|
2985
|
+
return false;
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
// plugins/core/analyzers/schema-violation/text.ts
|
|
2990
|
+
var SCHEMA_VIOLATION_TEXTS = {
|
|
2991
|
+
/** `Node <path> failed schema validation: <errors>` */
|
|
2992
|
+
nodeFailure: "Node {{path}} failed schema validation: {{errors}}",
|
|
2993
|
+
/** `Link <source> → <target> failed schema validation: <errors>` */
|
|
2994
|
+
linkFailure: "Link {{source}} \u2192 {{target}} failed schema validation: {{errors}}",
|
|
2995
|
+
/** `Node <path> is missing required frontmatter fields: <missing>` */
|
|
2996
|
+
frontmatterBaseFailure: "Node {{path}} is missing required frontmatter fields: {{missing}}.",
|
|
2997
|
+
/** Singular tooltip on the alert / chip when a node has exactly one validation failure. */
|
|
2998
|
+
alertTooltipSingle: "Frontmatter or schema validation failed.",
|
|
2999
|
+
/** Plural tooltip; `{{count}}` capped at 99 in the chip badge but the tooltip text shows the raw count. */
|
|
3000
|
+
alertTooltipMany: "{{count}} schema validation issues on this node."
|
|
3001
|
+
};
|
|
3002
|
+
|
|
3003
|
+
// plugins/core/analyzers/schema-violation/index.ts
|
|
3004
|
+
var ID22 = "schema-violation";
|
|
3005
|
+
var schemaViolationAnalyzer = {
|
|
3006
|
+
id: ID22,
|
|
3007
|
+
pluginId: CORE_PLUGIN_ID,
|
|
3008
|
+
kind: "analyzer",
|
|
3009
|
+
description: "Flags nodes or links that violate the project schemas.",
|
|
3010
|
+
mode: "deterministic",
|
|
3011
|
+
// No `ui` declaration: the per-node failure-count chip used to live
|
|
3012
|
+
// on `card.footer.right`, but its information is now folded into the
|
|
3013
|
+
// aggregate severity counters emitted by `core/issue-counter`. The
|
|
3014
|
+
// findings still emit as `Issue` records, so `sm check` / inspector
|
|
3015
|
+
// unchanged.
|
|
3016
|
+
ui: {},
|
|
3017
|
+
// Pre-existing complexity: validates every node + every link
|
|
3018
|
+
// against multiple schemas with per-severity aggregation. The
|
|
3019
|
+
// branching mirrors the schema catalog and splitting scatters the
|
|
3020
|
+
// validation contract. Tracked as tech-debt; surfaced when an
|
|
3021
|
+
// unrelated change touched the lint cache.
|
|
3022
|
+
// eslint-disable-next-line complexity
|
|
3023
|
+
evaluate(ctx) {
|
|
3024
|
+
const validators = loadSchemaValidators();
|
|
3025
|
+
const findings = [];
|
|
3026
|
+
const perNode = /* @__PURE__ */ new Map();
|
|
3027
|
+
for (const node of ctx.nodes) {
|
|
3028
|
+
const before = findings.length;
|
|
3029
|
+
collectNodeFindings(validators, node, findings);
|
|
3030
|
+
collectFrontmatterBaseFindings(node, findings);
|
|
3031
|
+
if (findings.length > before) {
|
|
3032
|
+
let worst = "warn";
|
|
3033
|
+
for (let i = before; i < findings.length; i++) {
|
|
3034
|
+
if (findings[i].severity === "error") {
|
|
3035
|
+
worst = "danger";
|
|
3036
|
+
break;
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
const prev = perNode.get(node.path);
|
|
3040
|
+
perNode.set(node.path, {
|
|
3041
|
+
count: (prev?.count ?? 0) + (findings.length - before),
|
|
3042
|
+
worst: prev?.worst === "danger" ? "danger" : worst
|
|
3043
|
+
});
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
for (const link of ctx.links) {
|
|
3047
|
+
collectLinkFindings(validators, link, findings);
|
|
3048
|
+
}
|
|
3049
|
+
void perNode;
|
|
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: ID22,
|
|
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: ID22,
|
|
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: ID22,
|
|
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 ID23 = "signal-collision";
|
|
3174
|
+
var signalCollisionAnalyzer = {
|
|
3175
|
+
id: ID23,
|
|
3176
|
+
pluginId: CORE_PLUGIN_ID,
|
|
3177
|
+
kind: "analyzer",
|
|
3178
|
+
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.",
|
|
3179
|
+
mode: "deterministic",
|
|
3180
|
+
evaluate(ctx) {
|
|
3181
|
+
const signals = ctx.signals;
|
|
3182
|
+
if (!signals || signals.length === 0) return [];
|
|
3183
|
+
const issues = [];
|
|
3184
|
+
for (const signal of signals) {
|
|
3185
|
+
const issue = makeIssue(signal);
|
|
3186
|
+
if (issue) issues.push(issue);
|
|
3187
|
+
}
|
|
3188
|
+
return issues;
|
|
3189
|
+
}
|
|
3190
|
+
};
|
|
3191
|
+
function makeIssue(signal) {
|
|
3192
|
+
const resolution = signal.resolution;
|
|
3193
|
+
if (!resolution || resolution.outcome !== "rejected") return null;
|
|
3194
|
+
if (resolution.rejectedBy) {
|
|
3195
|
+
const winner = resolution.rejectedBy;
|
|
3196
|
+
const winnerCandidate = signal.candidates[resolution.winnerIndex ?? 0];
|
|
3197
|
+
const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
|
|
3198
|
+
const winnerRange = `${winner.range.start}-${winner.range.end}`;
|
|
3199
|
+
return {
|
|
3200
|
+
analyzerId: ID23,
|
|
3201
|
+
severity: "warn",
|
|
3202
|
+
nodeIds: [signal.source],
|
|
3203
|
+
message: tx(SIGNAL_COLLISION_TEXTS.message, {
|
|
3204
|
+
loserExtractor: winnerCandidate.extractorId,
|
|
3205
|
+
loserRaw: signal.raw,
|
|
3206
|
+
loserRange,
|
|
3207
|
+
winnerExtractor: winner.extractorId,
|
|
3208
|
+
winnerRange,
|
|
3209
|
+
reason: winner.reason
|
|
3210
|
+
}),
|
|
3211
|
+
data: {
|
|
3212
|
+
loser: {
|
|
3213
|
+
extractorId: winnerCandidate.extractorId,
|
|
3214
|
+
raw: signal.raw,
|
|
3215
|
+
range: signal.range ?? null,
|
|
3216
|
+
candidate: {
|
|
3217
|
+
kind: winnerCandidate.kind,
|
|
3218
|
+
target: winnerCandidate.target,
|
|
3219
|
+
confidence: winnerCandidate.confidence
|
|
3220
|
+
}
|
|
3221
|
+
},
|
|
3222
|
+
winner: {
|
|
3223
|
+
extractorId: winner.extractorId,
|
|
3224
|
+
range: winner.range
|
|
3225
|
+
},
|
|
3226
|
+
reason: winner.reason
|
|
3227
|
+
}
|
|
3228
|
+
};
|
|
3229
|
+
}
|
|
3230
|
+
if (resolution.extractorDisabled) {
|
|
3231
|
+
const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
|
|
3232
|
+
return {
|
|
3233
|
+
analyzerId: ID23,
|
|
3234
|
+
severity: "warn",
|
|
3235
|
+
nodeIds: [signal.source],
|
|
3236
|
+
message: tx(SIGNAL_COLLISION_TEXTS.messageExtractorDisabled, {
|
|
3237
|
+
extractorId: resolution.extractorDisabled.extractorId,
|
|
3238
|
+
loserRaw: signal.raw,
|
|
3239
|
+
loserRange
|
|
3240
|
+
}),
|
|
3241
|
+
data: {
|
|
3242
|
+
extractorDisabled: resolution.extractorDisabled,
|
|
3243
|
+
raw: signal.raw,
|
|
3244
|
+
range: signal.range ?? null
|
|
3245
|
+
}
|
|
3246
|
+
};
|
|
3244
3247
|
}
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3248
|
+
if (resolution.belowFloor) {
|
|
3249
|
+
const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
|
|
3250
|
+
const topCandidate = signal.candidates[0];
|
|
3251
|
+
return {
|
|
3252
|
+
analyzerId: ID23,
|
|
3253
|
+
severity: "warn",
|
|
3254
|
+
nodeIds: [signal.source],
|
|
3255
|
+
message: tx(SIGNAL_COLLISION_TEXTS.messageBelowFloor, {
|
|
3256
|
+
loserRaw: signal.raw,
|
|
3257
|
+
loserRange,
|
|
3258
|
+
confidence: topCandidate.confidence,
|
|
3259
|
+
threshold: resolution.belowFloor.threshold
|
|
3260
|
+
}),
|
|
3261
|
+
data: {
|
|
3262
|
+
belowFloor: resolution.belowFloor,
|
|
3263
|
+
raw: signal.raw,
|
|
3264
|
+
range: signal.range ?? null
|
|
3265
|
+
}
|
|
3266
|
+
};
|
|
3252
3267
|
}
|
|
3268
|
+
return null;
|
|
3253
3269
|
}
|
|
3254
3270
|
|
|
3255
|
-
// plugins/core/analyzers/
|
|
3256
|
-
var
|
|
3257
|
-
/**
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
/**
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3271
|
+
// plugins/core/analyzers/trigger-collision/text.ts
|
|
3272
|
+
var TRIGGER_COLLISION_TEXTS = {
|
|
3273
|
+
/**
|
|
3274
|
+
* Top-level message when `analyzeTriggerBucket` accumulated exactly one
|
|
3275
|
+
* cause part. Used for the advertiser-ambiguous-only, invocation-
|
|
3276
|
+
* ambiguous-only, and cross-kind-only branches.
|
|
3277
|
+
*/
|
|
3278
|
+
messageOnePart: 'Trigger "{{normalized}}" has {{part}}.',
|
|
3279
|
+
/**
|
|
3280
|
+
* Top-level message when `analyzeTriggerBucket` accumulated two cause
|
|
3281
|
+
* parts (advertiser-ambiguous AND invocation-ambiguous fire together).
|
|
3282
|
+
* The joiner lives inside the template so future locales can adapt it
|
|
3283
|
+
* (e.g. `'; y '` in Spanish) without touching the rule code.
|
|
3284
|
+
*/
|
|
3285
|
+
messageTwoParts: 'Trigger "{{normalized}}" has {{first}}; and {{second}}.',
|
|
3286
|
+
/** `<n> nodes advertise it: <list>` part, fires on the advertiser-ambiguous branch. */
|
|
3287
|
+
partAdvertisers: "{{count}} nodes advertise it: {{paths}}",
|
|
3288
|
+
/** `<n> distinct invocation forms: <list>` part, fires on the invocation-ambiguous branch. */
|
|
3289
|
+
partInvocations: "{{count}} distinct invocation forms: {{forms}}",
|
|
3290
|
+
/** Singular cross-kind cause: `non-canonical invocation <form> against advertiser <path>`. */
|
|
3291
|
+
partNonCanonicalSingular: "non-canonical invocation {{forms}} against advertiser {{advertiser}}",
|
|
3292
|
+
/** Plural cross-kind cause: `non-canonical invocations <forms> against advertiser <path>`. */
|
|
3293
|
+
partNonCanonicalPlural: "non-canonical invocations {{forms}} against advertiser {{advertiser}}"
|
|
3267
3294
|
};
|
|
3268
3295
|
|
|
3269
|
-
// plugins/core/analyzers/
|
|
3270
|
-
var
|
|
3271
|
-
var
|
|
3272
|
-
|
|
3296
|
+
// plugins/core/analyzers/trigger-collision/index.ts
|
|
3297
|
+
var ID24 = "trigger-collision";
|
|
3298
|
+
var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
|
|
3299
|
+
"command",
|
|
3300
|
+
"skill",
|
|
3301
|
+
"agent"
|
|
3302
|
+
]);
|
|
3303
|
+
var triggerCollisionAnalyzer = {
|
|
3304
|
+
id: ID24,
|
|
3273
3305
|
pluginId: CORE_PLUGIN_ID,
|
|
3274
3306
|
kind: "analyzer",
|
|
3275
|
-
version: "1.0.0",
|
|
3276
|
-
description: "Detects and flags nodes or links violating the project schemas.",
|
|
3277
3307
|
mode: "deterministic",
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
// chassis as `core/broken-ref`, danger severity.
|
|
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
|
-
},
|
|
3308
|
+
description: "Flags two or more nodes that claim the same `/command` or `@agent` name.",
|
|
3309
|
+
// Two claim-collection passes (advertisement + invocation) feeding
|
|
3310
|
+
// the bucket map. Per-bucket analysis lives in `analyzeTriggerBucket`.
|
|
3311
|
+
// eslint-disable-next-line complexity
|
|
3299
3312
|
evaluate(ctx) {
|
|
3300
|
-
const
|
|
3301
|
-
const
|
|
3302
|
-
|
|
3313
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3314
|
+
const push = (key, claim) => {
|
|
3315
|
+
const bucket = buckets.get(key) ?? [];
|
|
3316
|
+
bucket.push(claim);
|
|
3317
|
+
buckets.set(key, bucket);
|
|
3318
|
+
};
|
|
3303
3319
|
for (const node of ctx.nodes) {
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3320
|
+
if (!ADVERTISING_KINDS.has(node.kind)) continue;
|
|
3321
|
+
const raw = node.frontmatter?.["name"];
|
|
3322
|
+
if (typeof raw !== "string" || raw.length === 0) continue;
|
|
3323
|
+
const normalized = `/${normalizeTrigger(raw)}`;
|
|
3324
|
+
if (normalized === "/") continue;
|
|
3325
|
+
push(normalized, {
|
|
3326
|
+
kind: "advertiser",
|
|
3327
|
+
token: node.path,
|
|
3328
|
+
nodeId: node.path,
|
|
3329
|
+
canonicalForm: `/${raw}`
|
|
3330
|
+
});
|
|
3310
3331
|
}
|
|
3311
3332
|
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
|
|
3333
|
+
const normalized = link.trigger?.normalizedTrigger;
|
|
3334
|
+
if (!normalized) continue;
|
|
3335
|
+
push(normalized, {
|
|
3336
|
+
kind: "invocation",
|
|
3337
|
+
token: link.target,
|
|
3338
|
+
nodeId: link.source
|
|
3326
3339
|
});
|
|
3327
3340
|
}
|
|
3328
|
-
|
|
3341
|
+
const issues = [];
|
|
3342
|
+
for (const [normalized, claims] of buckets) {
|
|
3343
|
+
const issue = analyzeTriggerBucket(normalized, claims);
|
|
3344
|
+
if (issue) issues.push(issue);
|
|
3345
|
+
}
|
|
3346
|
+
return issues;
|
|
3329
3347
|
}
|
|
3330
3348
|
};
|
|
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
|
-
|
|
3366
|
-
})
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
errors: result.errors
|
|
3383
|
-
}),
|
|
3384
|
-
data: { target: "link", source: link.source, to: link.target }
|
|
3349
|
+
function analyzeTriggerBucket(normalized, claims) {
|
|
3350
|
+
const advertiserPaths = [
|
|
3351
|
+
...new Set(claims.filter((c) => c.kind === "advertiser").map((c) => c.token))
|
|
3352
|
+
].sort();
|
|
3353
|
+
const invocationTargets = [
|
|
3354
|
+
...new Set(claims.filter((c) => c.kind === "invocation").map((c) => c.token))
|
|
3355
|
+
].sort();
|
|
3356
|
+
const advertisers = claims.filter(
|
|
3357
|
+
(c) => c.kind === "advertiser"
|
|
3358
|
+
);
|
|
3359
|
+
const advertiserAmbiguous = advertiserPaths.length >= 2;
|
|
3360
|
+
const invocationAmbiguous = invocationTargets.length >= 2;
|
|
3361
|
+
const canonicalForms = new Set(advertisers.map((a) => a.canonicalForm));
|
|
3362
|
+
const nonCanonicalInvocations = invocationTargets.filter((t) => !canonicalForms.has(t));
|
|
3363
|
+
const crossKindAmbiguous = advertiserPaths.length === 1 && nonCanonicalInvocations.length >= 1;
|
|
3364
|
+
if (!advertiserAmbiguous && !invocationAmbiguous && !crossKindAmbiguous) {
|
|
3365
|
+
return null;
|
|
3366
|
+
}
|
|
3367
|
+
const nodeIds = [...new Set(claims.map((c) => c.nodeId))].sort();
|
|
3368
|
+
const parts = [];
|
|
3369
|
+
if (advertiserAmbiguous) {
|
|
3370
|
+
parts.push(
|
|
3371
|
+
tx(TRIGGER_COLLISION_TEXTS.partAdvertisers, {
|
|
3372
|
+
count: advertiserPaths.length,
|
|
3373
|
+
paths: advertiserPaths.join(", ")
|
|
3374
|
+
})
|
|
3375
|
+
);
|
|
3376
|
+
}
|
|
3377
|
+
if (invocationAmbiguous) {
|
|
3378
|
+
parts.push(
|
|
3379
|
+
tx(TRIGGER_COLLISION_TEXTS.partInvocations, {
|
|
3380
|
+
count: invocationTargets.length,
|
|
3381
|
+
forms: invocationTargets.join(", ")
|
|
3382
|
+
})
|
|
3383
|
+
);
|
|
3384
|
+
} else if (crossKindAmbiguous) {
|
|
3385
|
+
const template = nonCanonicalInvocations.length > 1 ? TRIGGER_COLLISION_TEXTS.partNonCanonicalPlural : TRIGGER_COLLISION_TEXTS.partNonCanonicalSingular;
|
|
3386
|
+
parts.push(
|
|
3387
|
+
tx(template, {
|
|
3388
|
+
forms: nonCanonicalInvocations.join(", "),
|
|
3389
|
+
advertiser: advertiserPaths[0]
|
|
3390
|
+
})
|
|
3391
|
+
);
|
|
3392
|
+
}
|
|
3393
|
+
const message = parts.length === 2 ? tx(TRIGGER_COLLISION_TEXTS.messageTwoParts, {
|
|
3394
|
+
normalized,
|
|
3395
|
+
first: parts[0],
|
|
3396
|
+
second: parts[1]
|
|
3397
|
+
}) : tx(TRIGGER_COLLISION_TEXTS.messageOnePart, {
|
|
3398
|
+
normalized,
|
|
3399
|
+
part: parts[0]
|
|
3385
3400
|
});
|
|
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
3401
|
return {
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3402
|
+
analyzerId: ID24,
|
|
3403
|
+
severity: "error",
|
|
3404
|
+
nodeIds,
|
|
3405
|
+
message,
|
|
3406
|
+
data: {
|
|
3407
|
+
normalizedTrigger: normalized,
|
|
3408
|
+
invocationTargets,
|
|
3409
|
+
advertiserPaths
|
|
3410
|
+
}
|
|
3413
3411
|
};
|
|
3414
3412
|
}
|
|
3415
3413
|
|
|
@@ -3441,15 +3439,14 @@ var ASCII_FORMATTER_TEXTS = {
|
|
|
3441
3439
|
};
|
|
3442
3440
|
|
|
3443
3441
|
// plugins/core/formatters/ascii/index.ts
|
|
3444
|
-
var
|
|
3442
|
+
var ID25 = "ascii";
|
|
3445
3443
|
var KIND_ORDER = ["agent", "command", "skill", "markdown"];
|
|
3446
3444
|
var asciiFormatter = {
|
|
3447
|
-
id:
|
|
3445
|
+
id: ID25,
|
|
3448
3446
|
pluginId: CORE_PLUGIN_ID,
|
|
3449
3447
|
kind: "formatter",
|
|
3450
|
-
formatId:
|
|
3451
|
-
|
|
3452
|
-
description: "Renders the scan as plain text, grouped by kind, arrows, and issues. Used by `sm scan --format=ascii`.",
|
|
3448
|
+
formatId: ID25,
|
|
3449
|
+
description: "Renders the scan as plain text in three sections: nodes (grouped by kind), arrows, and issues. Used by `sm scan --format ascii`.",
|
|
3453
3450
|
// ASCII tree formatter, header + per-kind sections + per-issue
|
|
3454
3451
|
// section. Each section iterates and renders; splitting per section
|
|
3455
3452
|
// would multiply the for-loop boilerplate.
|
|
@@ -3542,14 +3539,13 @@ function renderSection(out, kind, group) {
|
|
|
3542
3539
|
}
|
|
3543
3540
|
|
|
3544
3541
|
// plugins/core/formatters/json/index.ts
|
|
3545
|
-
var
|
|
3542
|
+
var ID26 = "json";
|
|
3546
3543
|
var jsonFormatter = {
|
|
3547
|
-
id:
|
|
3544
|
+
id: ID26,
|
|
3548
3545
|
pluginId: CORE_PLUGIN_ID,
|
|
3549
3546
|
kind: "formatter",
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
formatId: ID25,
|
|
3547
|
+
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`.",
|
|
3548
|
+
formatId: ID26,
|
|
3553
3549
|
format(ctx) {
|
|
3554
3550
|
if (ctx.scanResult !== void 0) {
|
|
3555
3551
|
return JSON.stringify(ctx.scanResult);
|
|
@@ -3687,14 +3683,13 @@ function resolveSpecRoot2() {
|
|
|
3687
3683
|
}
|
|
3688
3684
|
}
|
|
3689
3685
|
|
|
3690
|
-
// plugins/core/actions/bump/index.ts
|
|
3691
|
-
var
|
|
3692
|
-
var
|
|
3693
|
-
id:
|
|
3686
|
+
// plugins/core/actions/node-bump/index.ts
|
|
3687
|
+
var ID27 = "node-bump";
|
|
3688
|
+
var nodeBumpAction = {
|
|
3689
|
+
id: ID27,
|
|
3694
3690
|
pluginId: CORE_PLUGIN_ID,
|
|
3695
3691
|
kind: "action",
|
|
3696
|
-
|
|
3697
|
-
description: "Marks a node as updated: bumps version, refreshes sidecar hashes, records the timestamp.",
|
|
3692
|
+
description: "Marks a node as updated: bumps `annotations.version`, refreshes sidecar hashes, and records the timestamp.",
|
|
3698
3693
|
mode: "deterministic",
|
|
3699
3694
|
// The runtime contract uses generic <TInput, TReport>; bump narrows
|
|
3700
3695
|
// both. The cast is the standard pattern for built-ins that want
|
|
@@ -3748,14 +3743,13 @@ function pickCurrentVersion(overlay) {
|
|
|
3748
3743
|
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
3749
3744
|
}
|
|
3750
3745
|
|
|
3751
|
-
// plugins/core/actions/
|
|
3752
|
-
var
|
|
3753
|
-
var
|
|
3754
|
-
id:
|
|
3746
|
+
// plugins/core/actions/node-supersede/index.ts
|
|
3747
|
+
var ID28 = "node-supersede";
|
|
3748
|
+
var nodeSupersedeAction = {
|
|
3749
|
+
id: ID28,
|
|
3755
3750
|
pluginId: CORE_PLUGIN_ID,
|
|
3756
3751
|
kind: "action",
|
|
3757
|
-
|
|
3758
|
-
description: "Declares the current node as superseded by another (writes `supersededBy` to the sidecar). Paired with the `core/superseded` analyzer.",
|
|
3752
|
+
description: "Declares the current node as superseded by another (writes `supersededBy` to the sidecar).",
|
|
3759
3753
|
mode: "deterministic",
|
|
3760
3754
|
invoke(_input, _ctx) {
|
|
3761
3755
|
const report = { ok: true, noop: true };
|
|
@@ -3861,7 +3855,7 @@ var UPDATE_CHECK_TEXTS = {
|
|
|
3861
3855
|
// package.json
|
|
3862
3856
|
var package_default = {
|
|
3863
3857
|
name: "@skill-map/cli",
|
|
3864
|
-
version: "0.
|
|
3858
|
+
version: "0.40.0",
|
|
3865
3859
|
description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
|
|
3866
3860
|
license: "MIT",
|
|
3867
3861
|
type: "module",
|
|
@@ -4278,8 +4272,7 @@ var updateCheckHook = {
|
|
|
4278
4272
|
id: "update-check",
|
|
4279
4273
|
pluginId: CORE_PLUGIN_ID,
|
|
4280
4274
|
kind: "hook",
|
|
4281
|
-
|
|
4282
|
-
description: "Checks daily for a newer skill-map version on npm. Shows an `update available` banner when one is found.",
|
|
4275
|
+
description: "Checks daily for a newer `skill-map` version on npm. Shows an `update available` banner when one is found.",
|
|
4283
4276
|
triggers: ["boot"],
|
|
4284
4277
|
async on(ctx) {
|
|
4285
4278
|
const payload = ctx.event.data ?? {};
|
|
@@ -4294,105 +4287,102 @@ var updateCheckHook = {
|
|
|
4294
4287
|
};
|
|
4295
4288
|
|
|
4296
4289
|
// plugins/built-ins.ts
|
|
4297
|
-
var claudeProvider2 = { ...claudeProvider, pluginId: "claude" };
|
|
4298
|
-
var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "claude" };
|
|
4299
|
-
var
|
|
4300
|
-
var antigravityProvider2 = { ...antigravityProvider, pluginId: "antigravity" };
|
|
4301
|
-
var openaiProvider2 = { ...openaiProvider, pluginId: "openai" };
|
|
4302
|
-
var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills" };
|
|
4303
|
-
var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core" };
|
|
4304
|
-
var annotationsExtractor2 = { ...annotationsExtractor, pluginId: "core" };
|
|
4305
|
-
var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core" };
|
|
4306
|
-
var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core" };
|
|
4307
|
-
var mcpToolsExtractor2 = { ...mcpToolsExtractor, pluginId: "core" };
|
|
4308
|
-
var
|
|
4309
|
-
var
|
|
4310
|
-
var
|
|
4311
|
-
var
|
|
4312
|
-
var contributionOrphanAnalyzer2 = { ...contributionOrphanAnalyzer, pluginId: "core" };
|
|
4313
|
-
var
|
|
4314
|
-
var
|
|
4315
|
-
var
|
|
4316
|
-
var
|
|
4317
|
-
var
|
|
4318
|
-
var
|
|
4319
|
-
var
|
|
4320
|
-
var
|
|
4321
|
-
var
|
|
4322
|
-
var
|
|
4323
|
-
var
|
|
4324
|
-
var
|
|
4325
|
-
var
|
|
4326
|
-
var
|
|
4327
|
-
var
|
|
4328
|
-
var
|
|
4329
|
-
var
|
|
4290
|
+
var claudeProvider2 = { ...claudeProvider, pluginId: "claude", version: "0.40.0" };
|
|
4291
|
+
var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "claude", version: "0.40.0" };
|
|
4292
|
+
var slashCommandExtractor2 = { ...slashCommandExtractor, pluginId: "claude", version: "0.40.0" };
|
|
4293
|
+
var antigravityProvider2 = { ...antigravityProvider, pluginId: "antigravity", version: "0.40.0" };
|
|
4294
|
+
var openaiProvider2 = { ...openaiProvider, pluginId: "openai", version: "0.40.0" };
|
|
4295
|
+
var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills", version: "0.40.0" };
|
|
4296
|
+
var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core", version: "0.40.0" };
|
|
4297
|
+
var annotationsExtractor2 = { ...annotationsExtractor, pluginId: "core", version: "0.40.0" };
|
|
4298
|
+
var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core", version: "0.40.0" };
|
|
4299
|
+
var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core", version: "0.40.0" };
|
|
4300
|
+
var mcpToolsExtractor2 = { ...mcpToolsExtractor, pluginId: "core", version: "0.40.0" };
|
|
4301
|
+
var toolsCounterExtractor2 = { ...toolsCounterExtractor, pluginId: "core", version: "0.40.0" };
|
|
4302
|
+
var annotationFieldUnknownAnalyzer2 = { ...annotationFieldUnknownAnalyzer, pluginId: "core", version: "0.40.0" };
|
|
4303
|
+
var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core", version: "0.40.0" };
|
|
4304
|
+
var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core", version: "0.40.0" };
|
|
4305
|
+
var contributionOrphanAnalyzer2 = { ...contributionOrphanAnalyzer, pluginId: "core", version: "0.40.0" };
|
|
4306
|
+
var issueCounterAnalyzer2 = { ...issueCounterAnalyzer, pluginId: "core", version: "0.40.0" };
|
|
4307
|
+
var jobFileOrphanAnalyzer2 = { ...jobFileOrphanAnalyzer, pluginId: "core", version: "0.40.0" };
|
|
4308
|
+
var linkConflictAnalyzer2 = { ...linkConflictAnalyzer, pluginId: "core", version: "0.40.0" };
|
|
4309
|
+
var linkCounterAnalyzer2 = { ...linkCounterAnalyzer, pluginId: "core", version: "0.40.0" };
|
|
4310
|
+
var linkSelfLoopAnalyzer2 = { ...linkSelfLoopAnalyzer, pluginId: "core", version: "0.40.0" };
|
|
4311
|
+
var nameReservedAnalyzer2 = { ...nameReservedAnalyzer, pluginId: "core", version: "0.40.0" };
|
|
4312
|
+
var nodeStabilityAnalyzer2 = { ...nodeStabilityAnalyzer, pluginId: "core", version: "0.40.0" };
|
|
4313
|
+
var nodeSupersededAnalyzer2 = { ...nodeSupersededAnalyzer, pluginId: "core", version: "0.40.0" };
|
|
4314
|
+
var referenceBrokenAnalyzer2 = { ...referenceBrokenAnalyzer, pluginId: "core", version: "0.40.0" };
|
|
4315
|
+
var referenceRedundantAnalyzer2 = { ...referenceRedundantAnalyzer, pluginId: "core", version: "0.40.0" };
|
|
4316
|
+
var schemaViolationAnalyzer2 = { ...schemaViolationAnalyzer, pluginId: "core", version: "0.40.0" };
|
|
4317
|
+
var signalCollisionAnalyzer2 = { ...signalCollisionAnalyzer, pluginId: "core", version: "0.40.0" };
|
|
4318
|
+
var triggerCollisionAnalyzer2 = { ...triggerCollisionAnalyzer, pluginId: "core", version: "0.40.0" };
|
|
4319
|
+
var asciiFormatter2 = { ...asciiFormatter, pluginId: "core", version: "0.40.0" };
|
|
4320
|
+
var jsonFormatter2 = { ...jsonFormatter, pluginId: "core", version: "0.40.0" };
|
|
4321
|
+
var nodeBumpAction2 = { ...nodeBumpAction, pluginId: "core", version: "0.40.0" };
|
|
4322
|
+
var nodeSupersedeAction2 = { ...nodeSupersedeAction, pluginId: "core", version: "0.40.0" };
|
|
4323
|
+
var updateCheckHook2 = { ...updateCheckHook, pluginId: "core", version: "0.40.0" };
|
|
4330
4324
|
var builtInBundles = [
|
|
4331
4325
|
{
|
|
4332
4326
|
id: "claude",
|
|
4333
|
-
|
|
4334
|
-
description: "Claude Code platform integration. Classifies files under `.claude/{agents,commands,skills}` and parses Claude-flavored frontmatter.",
|
|
4327
|
+
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
4328
|
extensions: [
|
|
4336
4329
|
claudeProvider2,
|
|
4337
4330
|
atDirectiveExtractor2,
|
|
4338
|
-
|
|
4331
|
+
slashCommandExtractor2
|
|
4339
4332
|
]
|
|
4340
4333
|
},
|
|
4341
4334
|
{
|
|
4342
4335
|
id: "antigravity",
|
|
4343
|
-
|
|
4344
|
-
description: "Google Antigravity CLI platform integration (released 2026-05-19, replaces the retired Gemini CLI). Antigravity adopted the open-standard `.agents/` layout, so skills are classified by the neutral `agent-skills` Provider; this bundle contributes lens identity + a reserved-name seed catalog for future Antigravity built-in invocables.",
|
|
4336
|
+
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
4337
|
extensions: [
|
|
4346
4338
|
antigravityProvider2
|
|
4347
4339
|
]
|
|
4348
4340
|
},
|
|
4349
4341
|
{
|
|
4350
4342
|
id: "openai",
|
|
4351
|
-
|
|
4352
|
-
description: "OpenAI Codex CLI platform integration. Classifies TOML sub-agent definitions under `.codex/agents/*.toml` and (future) walks the hierarchical AGENTS.md cascade. Provider for the active-lens `openai` runtime.",
|
|
4343
|
+
description: "OpenAI Codex CLI platform integration. Classifies TOML sub-agent definitions under `.codex/agents/*.toml`.",
|
|
4353
4344
|
extensions: [
|
|
4354
4345
|
openaiProvider2
|
|
4355
4346
|
]
|
|
4356
4347
|
},
|
|
4357
4348
|
{
|
|
4358
4349
|
id: "agent-skills",
|
|
4359
|
-
|
|
4360
|
-
description: "Agent Skills open standard. Vendor-neutral path `.agents/skills/<name>/SKILL.md` (Anthropic, OpenAI, Google). See agentskills.io.",
|
|
4350
|
+
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
4351
|
extensions: [
|
|
4362
4352
|
agentSkillsProvider2
|
|
4363
4353
|
]
|
|
4364
4354
|
},
|
|
4365
4355
|
{
|
|
4366
4356
|
id: "core",
|
|
4367
|
-
|
|
4368
|
-
description: "Core extensions shared across providers: extractors, analyzers, formatters, the bump action, and the universal `.md` fallback Provider.",
|
|
4357
|
+
description: "Core extensions shared across providers: parsers, extractors, analyzers, actions, hooks, formatters, and the universal `.md` fallback provider.",
|
|
4369
4358
|
extensions: [
|
|
4370
4359
|
coreMarkdownProvider2,
|
|
4371
4360
|
annotationsExtractor2,
|
|
4372
4361
|
externalUrlCounterExtractor2,
|
|
4373
4362
|
markdownLinkExtractor2,
|
|
4374
4363
|
mcpToolsExtractor2,
|
|
4375
|
-
|
|
4364
|
+
toolsCounterExtractor2,
|
|
4365
|
+
annotationFieldUnknownAnalyzer2,
|
|
4376
4366
|
annotationOrphanAnalyzer2,
|
|
4377
4367
|
annotationStaleAnalyzer2,
|
|
4378
|
-
brokenRefAnalyzer2,
|
|
4379
4368
|
contributionOrphanAnalyzer2,
|
|
4380
|
-
|
|
4369
|
+
issueCounterAnalyzer2,
|
|
4370
|
+
jobFileOrphanAnalyzer2,
|
|
4381
4371
|
linkConflictAnalyzer2,
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4372
|
+
linkCounterAnalyzer2,
|
|
4373
|
+
linkSelfLoopAnalyzer2,
|
|
4374
|
+
nameReservedAnalyzer2,
|
|
4375
|
+
nodeStabilityAnalyzer2,
|
|
4376
|
+
nodeSupersededAnalyzer2,
|
|
4377
|
+
referenceBrokenAnalyzer2,
|
|
4378
|
+
referenceRedundantAnalyzer2,
|
|
4379
|
+
schemaViolationAnalyzer2,
|
|
4386
4380
|
signalCollisionAnalyzer2,
|
|
4387
|
-
stabilityAnalyzer2,
|
|
4388
|
-
supersededAnalyzer2,
|
|
4389
4381
|
triggerCollisionAnalyzer2,
|
|
4390
|
-
unknownFieldAnalyzer2,
|
|
4391
|
-
validateAllAnalyzer2,
|
|
4392
4382
|
asciiFormatter2,
|
|
4393
4383
|
jsonFormatter2,
|
|
4394
|
-
|
|
4395
|
-
|
|
4384
|
+
nodeBumpAction2,
|
|
4385
|
+
nodeSupersedeAction2,
|
|
4396
4386
|
updateCheckHook2
|
|
4397
4387
|
]
|
|
4398
4388
|
}
|
|
@@ -4519,21 +4509,42 @@ function localTimeFromIso(iso) {
|
|
|
4519
4509
|
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
4520
4510
|
return `${hh}:${mm}:${ss}`;
|
|
4521
4511
|
}
|
|
4522
|
-
|
|
4512
|
+
function paintLevelPrefix(level, ansi) {
|
|
4513
|
+
const label = level.toUpperCase().padEnd(5);
|
|
4514
|
+
switch (level) {
|
|
4515
|
+
case "error":
|
|
4516
|
+
return `${ansi.red("\u2715")} ${ansi.red(label)}`;
|
|
4517
|
+
case "warn":
|
|
4518
|
+
return `${ansi.yellow("\u26A0")} ${ansi.yellow(label)}`;
|
|
4519
|
+
case "info":
|
|
4520
|
+
return `${ansi.cyan("\u2139")} ${ansi.cyan(label)}`;
|
|
4521
|
+
case "debug":
|
|
4522
|
+
case "trace":
|
|
4523
|
+
return `${ansi.dim("\xB7")} ${ansi.dim(label)}`;
|
|
4524
|
+
}
|
|
4525
|
+
}
|
|
4526
|
+
var defaultFormat = (record, ansi) => {
|
|
4523
4527
|
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}
|
|
4528
|
+
const prefix = paintLevelPrefix(record.level, ansi);
|
|
4529
|
+
const ctx = record.context && Object.keys(record.context).length > 0 ? ` ${ansi.dim("|")} ${ansi.dim(JSON.stringify(record.context))}` : "";
|
|
4530
|
+
return `${ansi.dim(time)} ${prefix} ${record.message}${ctx}
|
|
4527
4531
|
`;
|
|
4528
4532
|
};
|
|
4529
4533
|
var Logger = class {
|
|
4530
4534
|
#level;
|
|
4531
4535
|
#stream;
|
|
4532
4536
|
#format;
|
|
4537
|
+
#ansi;
|
|
4533
4538
|
constructor(opts) {
|
|
4534
4539
|
this.#level = opts.level;
|
|
4535
4540
|
this.#stream = opts.stream;
|
|
4536
4541
|
this.#format = opts.format ?? defaultFormat;
|
|
4542
|
+
const streamTty = opts.stream;
|
|
4543
|
+
this.#ansi = ansiFor({
|
|
4544
|
+
isTTY: streamTty.isTTY === true,
|
|
4545
|
+
noColorFlag: opts.noColorFlag === true,
|
|
4546
|
+
...opts.env !== void 0 ? { env: opts.env } : {}
|
|
4547
|
+
});
|
|
4537
4548
|
}
|
|
4538
4549
|
setLevel(level) {
|
|
4539
4550
|
this.#level = level;
|
|
@@ -4564,7 +4575,7 @@ var Logger = class {
|
|
|
4564
4575
|
message,
|
|
4565
4576
|
...context !== void 0 ? { context } : {}
|
|
4566
4577
|
};
|
|
4567
|
-
this.#stream.write(this.#format(record));
|
|
4578
|
+
this.#stream.write(this.#format(record, this.#ansi));
|
|
4568
4579
|
}
|
|
4569
4580
|
};
|
|
4570
4581
|
function resolveLogLevel(opts) {
|
|
@@ -7246,9 +7257,9 @@ async function sweepPerTupleContributions(trx, contributions, freshlyRunTuples)
|
|
|
7246
7257
|
const bufferKeys = buildContributionsBufferKeys(contributions);
|
|
7247
7258
|
const tuplesByPluginExt = groupFreshlyRunTuplesByPluginExt(freshlyRunTuples);
|
|
7248
7259
|
for (const [pe, nodes] of tuplesByPluginExt) {
|
|
7249
|
-
const
|
|
7250
|
-
if (
|
|
7251
|
-
await deleteStaleTupleRows(trx, pe.slice(0,
|
|
7260
|
+
const sep8 = pe.indexOf("\0");
|
|
7261
|
+
if (sep8 < 0) continue;
|
|
7262
|
+
await deleteStaleTupleRows(trx, pe.slice(0, sep8), pe.slice(sep8 + 1), [...nodes], bufferKeys);
|
|
7252
7263
|
}
|
|
7253
7264
|
}
|
|
7254
7265
|
function buildContributionsBufferKeys(contributions) {
|
|
@@ -8406,12 +8417,12 @@ function planOne(node, options) {
|
|
|
8406
8417
|
};
|
|
8407
8418
|
}
|
|
8408
8419
|
function invokeBumpFor(node, absPath, force) {
|
|
8409
|
-
if (!
|
|
8420
|
+
if (!nodeBumpAction.invoke) {
|
|
8410
8421
|
throw new Error("built-in bump action is missing its invoke()");
|
|
8411
8422
|
}
|
|
8412
8423
|
const input = {};
|
|
8413
8424
|
if (force) input.force = true;
|
|
8414
|
-
return
|
|
8425
|
+
return nodeBumpAction.invoke(input, {
|
|
8415
8426
|
node,
|
|
8416
8427
|
nodeAbsolutePath: absPath,
|
|
8417
8428
|
invoker: "cli",
|
|
@@ -8427,7 +8438,7 @@ var BumpCommand = class extends SmCommand {
|
|
|
8427
8438
|
category: "Actions",
|
|
8428
8439
|
description: "Bump a node's sidecar (`<basename>.sm`): increment annotations.version, refresh hashes, stamp audit.",
|
|
8429
8440
|
details: `
|
|
8430
|
-
Wraps the built-in deterministic \`core/bump\` Action. Single-node
|
|
8441
|
+
Wraps the built-in deterministic \`core/node-bump\` Action. Single-node
|
|
8431
8442
|
mode bumps one path; \`--pending\` walks every node whose sidecar
|
|
8432
8443
|
overlay reports drift and bumps them all.
|
|
8433
8444
|
|
|
@@ -8912,7 +8923,16 @@ var CHECK_TEXTS = {
|
|
|
8912
8923
|
tipLine: "\nTip: `sm refresh <node>` to revalidate a file after fixes.\n",
|
|
8913
8924
|
// --- prob stub advisory ---------------------------------------------------
|
|
8914
8925
|
probStubAdvisory: "sm check --include-prob: probabilistic Analyzer dispatch requires the job subsystem (Step 10). Stub: skipped {{count}} probabilistic analyzer(s): {{analyzerIds}}. Deterministic analyzers ran as usual; full dispatch lands when the job subsystem ships.\n",
|
|
8915
|
-
probStubAdvisoryAsync: "sm check --include-prob --async: probabilistic Analyzer dispatch requires the job subsystem (Step 10). Stub: skipped {{count}} probabilistic analyzer(s): {{analyzerIds}}. The --async flag is reserved for future encoding (returns job ids without waiting once jobs land); today it is a no-op. Deterministic analyzers ran as usual.\n"
|
|
8926
|
+
probStubAdvisoryAsync: "sm check --include-prob --async: probabilistic Analyzer dispatch requires the job subsystem (Step 10). Stub: skipped {{count}} probabilistic analyzer(s): {{analyzerIds}}. The --async flag is reserved for future encoding (returns job ids without waiting once jobs land); today it is a no-op. Deterministic analyzers ran as usual.\n",
|
|
8927
|
+
/**
|
|
8928
|
+
* Emitted on stderr when `--analyzers` lists one or more ids the
|
|
8929
|
+
* loaded analyzer registry does not know. The user almost always
|
|
8930
|
+
* mistyped (e.g. `broken-ref` instead of `reference-broken`); listing
|
|
8931
|
+
* the valid ids inline lets them fix the call without a second round
|
|
8932
|
+
* trip through `sm plugins list`. Exit code is `ExitCode.Error` (2):
|
|
8933
|
+
* bad usage, not "no issues found".
|
|
8934
|
+
*/
|
|
8935
|
+
unknownAnalyzerIds: "sm check: unknown analyzer id(s) in --analyzers: {{unknown}}.\nValid ids (qualified or short form accepted):\n{{known}}\n"
|
|
8916
8936
|
};
|
|
8917
8937
|
|
|
8918
8938
|
// cli/util/conformance-env.ts
|
|
@@ -9453,20 +9473,17 @@ var PluginLoader = class {
|
|
|
9453
9473
|
* into a helper would scatter the return-on-failure pattern without
|
|
9454
9474
|
* making the orchestration clearer.
|
|
9455
9475
|
*/
|
|
9456
|
-
// eslint-disable-next-line complexity
|
|
9457
9476
|
async loadOne(pluginPath) {
|
|
9458
9477
|
const pluginId = pathId(pluginPath);
|
|
9459
9478
|
const manifestResult = this.#parseAndValidateManifest(pluginPath, pluginId);
|
|
9460
9479
|
if (!manifestResult.ok) return manifestResult.failure;
|
|
9461
9480
|
const manifest = manifestResult.manifest;
|
|
9462
|
-
const granularity = manifest.granularity ?? "extension";
|
|
9463
9481
|
if (this.#options.resolveEnabled && !this.#options.resolveEnabled(pluginId)) {
|
|
9464
9482
|
return {
|
|
9465
9483
|
path: pluginPath,
|
|
9466
9484
|
id: pluginId,
|
|
9467
9485
|
status: "disabled",
|
|
9468
9486
|
manifest,
|
|
9469
|
-
granularity,
|
|
9470
9487
|
reason: PLUGIN_LOADER_TEXTS.disabledByConfig
|
|
9471
9488
|
};
|
|
9472
9489
|
}
|
|
@@ -9488,7 +9505,6 @@ var PluginLoader = class {
|
|
|
9488
9505
|
id: pluginId,
|
|
9489
9506
|
status: "enabled",
|
|
9490
9507
|
manifest,
|
|
9491
|
-
granularity,
|
|
9492
9508
|
extensions: loaded,
|
|
9493
9509
|
...storageSchemasResult.schemas ? { storageSchemas: storageSchemasResult.schemas } : {}
|
|
9494
9510
|
};
|
|
@@ -9546,7 +9562,6 @@ var PluginLoader = class {
|
|
|
9546
9562
|
id: pluginId,
|
|
9547
9563
|
status: "incompatible-spec",
|
|
9548
9564
|
manifest,
|
|
9549
|
-
granularity: manifest.granularity ?? "extension",
|
|
9550
9565
|
reason: tx(PLUGIN_LOADER_TEXTS.incompatibleSpec, {
|
|
9551
9566
|
installedSpecVersion: this.#options.specVersion,
|
|
9552
9567
|
specCompat: manifest.specCompat
|
|
@@ -9823,14 +9838,14 @@ var LOCKED_PLUGIN_IDS = /* @__PURE__ */ new Set([
|
|
|
9823
9838
|
// unreachable from CLI / BFF / UI. Re-evaluate if a third-party ever
|
|
9824
9839
|
// ships a competing supersession extractor.
|
|
9825
9840
|
"core/annotations",
|
|
9826
|
-
// `core/
|
|
9841
|
+
// `core/schema-violation` validates every scanned Node against
|
|
9827
9842
|
// `node.schema.json` and every Link against `link.schema.json` (the
|
|
9828
9843
|
// authoritative @skill-map/spec). Disabling it makes the system
|
|
9829
9844
|
// persist non-conformant content silently, breaking the spec
|
|
9830
9845
|
// invariant "what reaches the DB conforms to the spec". The check is
|
|
9831
9846
|
// foundational, not advisory; lock it on so the guarantee holds
|
|
9832
9847
|
// regardless of user / DB / settings hand-edits.
|
|
9833
|
-
"core/
|
|
9848
|
+
"core/schema-violation",
|
|
9834
9849
|
// `core/ascii` is the only built-in Formatter today and the default
|
|
9835
9850
|
// for `sm graph` (`--format ascii`). Disabling it breaks the verb
|
|
9836
9851
|
// entirely (`composeFormatters` returns the empty list, the CLI
|
|
@@ -9865,21 +9880,9 @@ function isBuiltInExtensionEnabled(bundle, ext, resolveEnabled) {
|
|
|
9865
9880
|
return isBundleEntryEnabled(bundle, ext.id, resolveEnabled);
|
|
9866
9881
|
}
|
|
9867
9882
|
function isBundleEntryEnabled(bundle, extId, resolveEnabled) {
|
|
9868
|
-
if (bundle.granularity === "bundle") {
|
|
9869
|
-
return resolveEnabled(bundle.id);
|
|
9870
|
-
}
|
|
9871
9883
|
return resolveEnabled(qualifiedExtensionId(bundle.id, extId));
|
|
9872
9884
|
}
|
|
9873
|
-
function
|
|
9874
|
-
const out = /* @__PURE__ */ new Map();
|
|
9875
|
-
for (const plugin of discovered) {
|
|
9876
|
-
out.set(plugin.id, plugin.granularity ?? "bundle");
|
|
9877
|
-
}
|
|
9878
|
-
return out;
|
|
9879
|
-
}
|
|
9880
|
-
function isPluginExtensionEnabled(ext, granularityMap, resolveEnabled) {
|
|
9881
|
-
const granularity = granularityMap.get(ext.pluginId) ?? "bundle";
|
|
9882
|
-
if (granularity === "bundle") return resolveEnabled(ext.pluginId);
|
|
9885
|
+
function isPluginExtensionEnabled(ext, resolveEnabled) {
|
|
9883
9886
|
return resolveEnabled(qualifiedExtensionId(ext.pluginId, ext.id));
|
|
9884
9887
|
}
|
|
9885
9888
|
async function buildEnabledResolver(ctx) {
|
|
@@ -10398,7 +10401,6 @@ function filterBuiltInManifests(manifests, resolveEnabled) {
|
|
|
10398
10401
|
// core/runtime/plugin-runtime/composer.ts
|
|
10399
10402
|
function composeScanExtensions(opts) {
|
|
10400
10403
|
const resolveEnabled = opts.resolveEnabled ?? opts.pluginRuntime.resolveEnabled;
|
|
10401
|
-
const granularityMap = buildGranularityMap(opts.pluginRuntime.discovered);
|
|
10402
10404
|
const providers = [];
|
|
10403
10405
|
const extractors = [];
|
|
10404
10406
|
const analyzers = [];
|
|
@@ -10410,16 +10412,16 @@ function composeScanExtensions(opts) {
|
|
|
10410
10412
|
);
|
|
10411
10413
|
}
|
|
10412
10414
|
for (const ext of opts.pluginRuntime.extensions.providers) {
|
|
10413
|
-
if (isPluginExtensionEnabled(ext,
|
|
10415
|
+
if (isPluginExtensionEnabled(ext, resolveEnabled)) providers.push(ext);
|
|
10414
10416
|
}
|
|
10415
10417
|
for (const ext of opts.pluginRuntime.extensions.extractors) {
|
|
10416
|
-
if (isPluginExtensionEnabled(ext,
|
|
10418
|
+
if (isPluginExtensionEnabled(ext, resolveEnabled)) extractors.push(ext);
|
|
10417
10419
|
}
|
|
10418
10420
|
for (const ext of opts.pluginRuntime.extensions.analyzers) {
|
|
10419
|
-
if (isPluginExtensionEnabled(ext,
|
|
10421
|
+
if (isPluginExtensionEnabled(ext, resolveEnabled)) analyzers.push(ext);
|
|
10420
10422
|
}
|
|
10421
10423
|
for (const ext of opts.pluginRuntime.extensions.hooks) {
|
|
10422
|
-
if (isPluginExtensionEnabled(ext,
|
|
10424
|
+
if (isPluginExtensionEnabled(ext, resolveEnabled)) hooks.push(ext);
|
|
10423
10425
|
}
|
|
10424
10426
|
const finalProviders = opts.killSwitches?.providers === true ? [] : providers;
|
|
10425
10427
|
const finalExtractors = opts.killSwitches?.extractors === true ? [] : extractors;
|
|
@@ -10466,7 +10468,6 @@ function accumulateBuiltInScanExtensions(buckets, resolveEnabled) {
|
|
|
10466
10468
|
function composeFormatters(opts) {
|
|
10467
10469
|
const noBuiltIns = opts.noBuiltIns ?? false;
|
|
10468
10470
|
const resolveEnabled = opts.resolveEnabled ?? opts.pluginRuntime.resolveEnabled;
|
|
10469
|
-
const granularityMap = buildGranularityMap(opts.pluginRuntime.discovered);
|
|
10470
10471
|
const out = [];
|
|
10471
10472
|
if (!noBuiltIns) {
|
|
10472
10473
|
for (const bundle of builtInBundles) {
|
|
@@ -10478,32 +10479,24 @@ function composeFormatters(opts) {
|
|
|
10478
10479
|
}
|
|
10479
10480
|
}
|
|
10480
10481
|
for (const ext of opts.pluginRuntime.extensions.formatters) {
|
|
10481
|
-
if (isPluginExtensionEnabled(ext,
|
|
10482
|
+
if (isPluginExtensionEnabled(ext, resolveEnabled)) out.push(ext);
|
|
10482
10483
|
}
|
|
10483
10484
|
return out;
|
|
10484
10485
|
}
|
|
10485
10486
|
function registerEnabledExtensions(kernel, pluginRuntime, options = {}) {
|
|
10486
10487
|
const noBuiltIns = options.noBuiltIns === true;
|
|
10487
10488
|
const resolveEnabled = options.resolveEnabled ?? pluginRuntime.resolveEnabled;
|
|
10488
|
-
const granularityMap = buildGranularityMap(pluginRuntime.discovered);
|
|
10489
10489
|
if (!noBuiltIns) {
|
|
10490
10490
|
const enabledBuiltIns = filterBuiltInManifests(listBuiltIns(), resolveEnabled);
|
|
10491
10491
|
for (const manifest of enabledBuiltIns) kernel.registry.register(manifest);
|
|
10492
10492
|
}
|
|
10493
10493
|
for (const manifest of pluginRuntime.manifests) {
|
|
10494
|
-
if (!isPluginExtensionEnabled(manifest,
|
|
10494
|
+
if (!isPluginExtensionEnabled(manifest, resolveEnabled)) continue;
|
|
10495
10495
|
kernel.registry.register(manifest);
|
|
10496
10496
|
}
|
|
10497
10497
|
if (kernel.setRegisteredAnnotationKeys) {
|
|
10498
10498
|
const filteredAnnotations = pluginRuntime.annotationContributions.filter(
|
|
10499
|
-
(entry) => (
|
|
10500
|
-
// Annotation contributions live at plugin-id granularity (the
|
|
10501
|
-
// catalog row carries `pluginId`, not `extensionId`), so the
|
|
10502
|
-
// bundle-level toggle gates the entire row. Extension
|
|
10503
|
-
// granularity falls through to the manifest-level filter above
|
|
10504
|
-
// this surface is bundle-scoped by design.
|
|
10505
|
-
resolveEnabled(entry.pluginId)
|
|
10506
|
-
)
|
|
10499
|
+
(entry) => resolveEnabled(entry.pluginId)
|
|
10507
10500
|
);
|
|
10508
10501
|
kernel.setRegisteredAnnotationKeys(filteredAnnotations);
|
|
10509
10502
|
}
|
|
@@ -10511,7 +10504,6 @@ function registerEnabledExtensions(kernel, pluginRuntime, options = {}) {
|
|
|
10511
10504
|
const userContribs = pluginRuntime.viewContributions.filter(
|
|
10512
10505
|
(entry) => isPluginExtensionEnabled(
|
|
10513
10506
|
{ pluginId: entry.pluginId, id: entry.extensionId },
|
|
10514
|
-
granularityMap,
|
|
10515
10507
|
resolveEnabled
|
|
10516
10508
|
)
|
|
10517
10509
|
);
|
|
@@ -10550,7 +10542,7 @@ var CheckCommand = class extends SmCommand {
|
|
|
10550
10542
|
["Print every current issue", "$0 check"],
|
|
10551
10543
|
["Machine-readable issue list", "$0 check --json"],
|
|
10552
10544
|
["Restrict to a single node", "$0 check -n .claude/agents/architect.md"],
|
|
10553
|
-
["Restrict to specific rules", "$0 check --analyzers core/broken
|
|
10545
|
+
["Restrict to specific rules", "$0 check --analyzers core/reference-broken,core/schema-violation"],
|
|
10554
10546
|
["Opt in to probabilistic analyzers (stub until Step 10)", "$0 check --include-prob"],
|
|
10555
10547
|
["Use a non-default DB file", "$0 check --db /path/to/skill-map.db"]
|
|
10556
10548
|
]
|
|
@@ -10577,22 +10569,8 @@ var CheckCommand = class extends SmCommand {
|
|
|
10577
10569
|
const exit = requireDbOrExit(dbPath, this.context.stderr);
|
|
10578
10570
|
if (exit !== null) return exit;
|
|
10579
10571
|
const analyzerFilter = parseAnalyzersFlag(this.analyzers);
|
|
10580
|
-
|
|
10581
|
-
|
|
10582
|
-
noPlugins: this.noPlugins,
|
|
10583
|
-
analyzerFilter,
|
|
10584
|
-
printer: this.printer
|
|
10585
|
-
});
|
|
10586
|
-
if (probAnalyzerIds.length > 0) {
|
|
10587
|
-
const template = this.async ? CHECK_TEXTS.probStubAdvisoryAsync : CHECK_TEXTS.probStubAdvisory;
|
|
10588
|
-
this.printer.info(
|
|
10589
|
-
tx(template, {
|
|
10590
|
-
count: probAnalyzerIds.length,
|
|
10591
|
-
analyzerIds: probAnalyzerIds.join(", ")
|
|
10592
|
-
})
|
|
10593
|
-
);
|
|
10594
|
-
}
|
|
10595
|
-
}
|
|
10572
|
+
const preflight = await this.#preflightAnalyzerCatalog(analyzerFilter);
|
|
10573
|
+
if (preflight.exit !== null) return preflight.exit;
|
|
10596
10574
|
return withSqlite({ databasePath: dbPath, autoBackup: false }, async (adapter) => {
|
|
10597
10575
|
let issues = await adapter.issues.listAll();
|
|
10598
10576
|
if (this.node !== void 0) {
|
|
@@ -10615,6 +10593,53 @@ var CheckCommand = class extends SmCommand {
|
|
|
10615
10593
|
return issues.some((i) => i.severity === "error") ? ExitCode.Issues : ExitCode.Ok;
|
|
10616
10594
|
});
|
|
10617
10595
|
}
|
|
10596
|
+
/**
|
|
10597
|
+
* Either an explicit `--analyzers` list or `--include-prob` forces a
|
|
10598
|
+
* load of the live Analyzer catalog: the first needs it to validate
|
|
10599
|
+
* the user-supplied ids against the registry, the second to enumerate
|
|
10600
|
+
* registered probabilistic analyzers. Sharing a single load keeps
|
|
10601
|
+
* `sm check` from paying for two passes when both flags are present.
|
|
10602
|
+
*
|
|
10603
|
+
* Returns `{ exit: <code> }` to short-circuit `run()` when the
|
|
10604
|
+
* validation rejects an unknown id (the only path that aborts before
|
|
10605
|
+
* the DB read). Successful runs return `{ exit: null }`.
|
|
10606
|
+
*/
|
|
10607
|
+
async #preflightAnalyzerCatalog(analyzerFilter) {
|
|
10608
|
+
const needsCatalog = analyzerFilter !== void 0 || this.includeProb;
|
|
10609
|
+
if (!needsCatalog) return { exit: null };
|
|
10610
|
+
const analyzers = await loadAnalyzerCatalog({
|
|
10611
|
+
noPlugins: this.noPlugins,
|
|
10612
|
+
printer: this.printer
|
|
10613
|
+
});
|
|
10614
|
+
if (analyzerFilter !== void 0) {
|
|
10615
|
+
const validation = validateAnalyzerFilter(analyzerFilter, analyzers);
|
|
10616
|
+
if (validation !== null) {
|
|
10617
|
+
this.printer.error(validation);
|
|
10618
|
+
return { exit: ExitCode.Error };
|
|
10619
|
+
}
|
|
10620
|
+
}
|
|
10621
|
+
if (this.includeProb) {
|
|
10622
|
+
this.#emitProbAdvisory(analyzers, analyzerFilter);
|
|
10623
|
+
}
|
|
10624
|
+
return { exit: null };
|
|
10625
|
+
}
|
|
10626
|
+
/**
|
|
10627
|
+
* Walk the loaded catalog for probabilistic analyzers honouring the
|
|
10628
|
+
* `--analyzers` filter and, when any survive, emit the stub stderr
|
|
10629
|
+
* advisory naming them. Extracted so `run()` does not carry the
|
|
10630
|
+
* branching for the `--include-prob` / `--async` advisory shapes.
|
|
10631
|
+
*/
|
|
10632
|
+
#emitProbAdvisory(analyzers, analyzerFilter) {
|
|
10633
|
+
const probAnalyzerIds = detectProbAnalyzerIds(analyzers, analyzerFilter);
|
|
10634
|
+
if (probAnalyzerIds.length === 0) return;
|
|
10635
|
+
const template = this.async ? CHECK_TEXTS.probStubAdvisoryAsync : CHECK_TEXTS.probStubAdvisory;
|
|
10636
|
+
this.printer.info(
|
|
10637
|
+
tx(template, {
|
|
10638
|
+
count: probAnalyzerIds.length,
|
|
10639
|
+
analyzerIds: probAnalyzerIds.join(", ")
|
|
10640
|
+
})
|
|
10641
|
+
);
|
|
10642
|
+
}
|
|
10618
10643
|
};
|
|
10619
10644
|
function parseAnalyzersFlag(raw) {
|
|
10620
10645
|
if (raw === void 0) return void 0;
|
|
@@ -10622,7 +10647,7 @@ function parseAnalyzersFlag(raw) {
|
|
|
10622
10647
|
if (ids.length === 0) return void 0;
|
|
10623
10648
|
return ids;
|
|
10624
10649
|
}
|
|
10625
|
-
async function
|
|
10650
|
+
async function loadAnalyzerCatalog(opts) {
|
|
10626
10651
|
const pluginRuntime = opts.noPlugins ? emptyPluginRuntime() : await loadPluginRuntime();
|
|
10627
10652
|
pluginRuntime.emitWarnings(opts.printer);
|
|
10628
10653
|
const composed = composeScanExtensions({
|
|
@@ -10630,12 +10655,30 @@ async function detectProbAnalyzerIds(opts) {
|
|
|
10630
10655
|
pluginRuntime,
|
|
10631
10656
|
killSwitches: readConformanceKillSwitches()
|
|
10632
10657
|
});
|
|
10633
|
-
|
|
10658
|
+
return composed?.analyzers ?? [];
|
|
10659
|
+
}
|
|
10660
|
+
function validateAnalyzerFilter(filter, analyzers) {
|
|
10661
|
+
const knownQualified = /* @__PURE__ */ new Set();
|
|
10662
|
+
const knownShort = /* @__PURE__ */ new Set();
|
|
10663
|
+
for (const analyzer of analyzers) {
|
|
10664
|
+
const qualified = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
|
|
10665
|
+
knownQualified.add(qualified);
|
|
10666
|
+
knownShort.add(analyzer.id);
|
|
10667
|
+
}
|
|
10668
|
+
const unknown = filter.filter((id) => !knownQualified.has(id) && !knownShort.has(id));
|
|
10669
|
+
if (unknown.length === 0) return null;
|
|
10670
|
+
const knownList = [...knownQualified].sort().map((id) => ` ${id}`).join("\n");
|
|
10671
|
+
return tx(CHECK_TEXTS.unknownAnalyzerIds, {
|
|
10672
|
+
unknown: unknown.join(", "),
|
|
10673
|
+
known: knownList
|
|
10674
|
+
});
|
|
10675
|
+
}
|
|
10676
|
+
function detectProbAnalyzerIds(analyzers, analyzerFilter) {
|
|
10634
10677
|
const probIds = [];
|
|
10635
10678
|
for (const analyzer of analyzers) {
|
|
10636
10679
|
if (analyzer.mode !== "probabilistic") continue;
|
|
10637
10680
|
const qualified = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
|
|
10638
|
-
if (
|
|
10681
|
+
if (analyzerFilter && !matchesAnalyzerFilter(qualified, analyzerFilter)) continue;
|
|
10639
10682
|
probIds.push(qualified);
|
|
10640
10683
|
}
|
|
10641
10684
|
probIds.sort();
|
|
@@ -14297,8 +14340,8 @@ function computePlannedHookContent(existing) {
|
|
|
14297
14340
|
if (existing.includes(SKILL_MAP_MARKER)) {
|
|
14298
14341
|
return { kind: "already-installed", content: existing };
|
|
14299
14342
|
}
|
|
14300
|
-
const
|
|
14301
|
-
return { kind: "chained", content: existing +
|
|
14343
|
+
const sep8 = existing.endsWith("\n") ? "" : "\n";
|
|
14344
|
+
return { kind: "chained", content: existing + sep8 + "\n" + SKILL_MAP_BLOCK };
|
|
14302
14345
|
}
|
|
14303
14346
|
async function ensureExecutableBit(path) {
|
|
14304
14347
|
const mode = (await stat2(path)).mode;
|
|
@@ -14673,7 +14716,8 @@ function recomputeLinkCounts(nodes, links) {
|
|
|
14673
14716
|
for (const link of links) {
|
|
14674
14717
|
const source = byPath3.get(link.source);
|
|
14675
14718
|
if (source) source.linksOutCount += 1;
|
|
14676
|
-
const
|
|
14719
|
+
const targetKey = link.resolvedTarget ?? link.target;
|
|
14720
|
+
const target = byPath3.get(targetKey);
|
|
14677
14721
|
if (target) target.linksInCount += 1;
|
|
14678
14722
|
}
|
|
14679
14723
|
}
|
|
@@ -14707,8 +14751,8 @@ function isExternalUrlLink(link) {
|
|
|
14707
14751
|
}
|
|
14708
14752
|
|
|
14709
14753
|
// kernel/orchestrator/analyzers.ts
|
|
14710
|
-
async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher, reservedNodePaths, signals) {
|
|
14711
|
-
const issues = [];
|
|
14754
|
+
async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher, reservedNodePaths, signals, seedIssues = []) {
|
|
14755
|
+
const issues = [...seedIssues];
|
|
14712
14756
|
const contributions = [];
|
|
14713
14757
|
const validators = loadSchemaValidators();
|
|
14714
14758
|
void registeredActionIds;
|
|
@@ -14716,7 +14760,8 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
|
|
|
14716
14760
|
relativePath: o.relativePath,
|
|
14717
14761
|
expectedMdPath: o.expectedMdPath
|
|
14718
14762
|
}));
|
|
14719
|
-
|
|
14763
|
+
const scheduled = orderAnalyzersByPhase(analyzers);
|
|
14764
|
+
for (const analyzer of scheduled) {
|
|
14720
14765
|
const qualifiedId2 = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
|
|
14721
14766
|
const declaredContributions = readDeclaredContributions(analyzer);
|
|
14722
14767
|
const emitContribution = (nodePath, contributionId, payload) => {
|
|
@@ -14769,6 +14814,11 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
|
|
|
14769
14814
|
annotationContributions,
|
|
14770
14815
|
viewContributions,
|
|
14771
14816
|
orphanJobFiles,
|
|
14817
|
+
// `issues` is the live accumulator, mutated by `issues.push(...)`
|
|
14818
|
+
// below as each analyzer's emission lands. Late-phase analyzers
|
|
14819
|
+
// (`core/issue-counter`) read it to compute cross-analyzer
|
|
14820
|
+
// aggregates. Treat as read-only on the analyzer side.
|
|
14821
|
+
accumulatedIssues: issues,
|
|
14772
14822
|
...referenceablePaths ? { referenceablePaths } : {},
|
|
14773
14823
|
...cwd ? { cwd } : {},
|
|
14774
14824
|
...reservedNodePaths ? { reservedNodePaths } : {},
|
|
@@ -14785,6 +14835,12 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
|
|
|
14785
14835
|
}
|
|
14786
14836
|
return { issues, contributions };
|
|
14787
14837
|
}
|
|
14838
|
+
function orderAnalyzersByPhase(analyzers) {
|
|
14839
|
+
return analyzers.slice().sort((a, b) => phaseRank(a) - phaseRank(b));
|
|
14840
|
+
}
|
|
14841
|
+
function phaseRank(a) {
|
|
14842
|
+
return a.phase === "aggregate" ? 1 : 0;
|
|
14843
|
+
}
|
|
14788
14844
|
function validateIssue(analyzer, issue, emitter) {
|
|
14789
14845
|
const severity = issue.severity;
|
|
14790
14846
|
if (severity !== "error" && severity !== "warn" && severity !== "info") {
|
|
@@ -15929,7 +15985,7 @@ async function runScanInternal(_kernel, options) {
|
|
|
15929
15985
|
else walked.internalLinks.push(link);
|
|
15930
15986
|
}
|
|
15931
15987
|
walked.signals = resolved.resolvedSignals;
|
|
15932
|
-
const postWalkCtx = buildPostWalkTransformCtx(exts.providers, walked.nodes);
|
|
15988
|
+
const postWalkCtx = buildPostWalkTransformCtx(exts.providers, walked.nodes, activeProviderId);
|
|
15933
15989
|
walked.internalLinks = applyPostWalkTransforms(walked.internalLinks, walked.nodes, postWalkCtx);
|
|
15934
15990
|
recomputeLinkCounts(walked.nodes, walked.internalLinks);
|
|
15935
15991
|
recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);
|
|
@@ -15952,11 +16008,15 @@ async function runScanInternal(_kernel, options) {
|
|
|
15952
16008
|
emitter,
|
|
15953
16009
|
hookDispatcher,
|
|
15954
16010
|
postWalkCtx.reservedNodePaths,
|
|
15955
|
-
walked.signals
|
|
16011
|
+
walked.signals,
|
|
16012
|
+
// Seed the accumulator with orchestrator-emitted frontmatter
|
|
16013
|
+
// issues so the aggregate phase (`core/issue-counter`) counts
|
|
16014
|
+
// them on the per-node chip. The seeds are echoed back on
|
|
16015
|
+
// `analyzerResult.issues`, no explicit push is needed below.
|
|
16016
|
+
walked.frontmatterIssues
|
|
15956
16017
|
);
|
|
15957
16018
|
mergeAnalyzerEmissions(walked, analyzerResult, exts.analyzers);
|
|
15958
16019
|
const issues = analyzerResult.issues;
|
|
15959
|
-
for (const issue of walked.frontmatterIssues) issues.push(issue);
|
|
15960
16020
|
const silenced = options.ignoreFilter ? (path) => options.ignoreFilter.ignores(path) : void 0;
|
|
15961
16021
|
const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues, silenced) : [];
|
|
15962
16022
|
const stats = buildScanStats(walked, issues, start);
|
|
@@ -15965,14 +16025,14 @@ async function runScanInternal(_kernel, options) {
|
|
|
15965
16025
|
await hookDispatcher.dispatch("scan.completed", scanCompletedEvent);
|
|
15966
16026
|
return buildScanReturn(walked, issues, renameOps, stats, options, setup);
|
|
15967
16027
|
}
|
|
15968
|
-
function buildPostWalkTransformCtx(providers, nodes) {
|
|
16028
|
+
function buildPostWalkTransformCtx(providers, nodes, activeProvider) {
|
|
15969
16029
|
const { kindRegistry, providerResolution, reservedNamesByProviderKind } = buildProviderIndexes(providers);
|
|
15970
16030
|
const reservedNodePaths = buildReservedNodePaths(
|
|
15971
16031
|
nodes,
|
|
15972
16032
|
kindRegistry,
|
|
15973
16033
|
reservedNamesByProviderKind
|
|
15974
16034
|
);
|
|
15975
|
-
return { kindRegistry, providerResolution, reservedNodePaths };
|
|
16035
|
+
return { kindRegistry, providerResolution, activeProvider, reservedNodePaths };
|
|
15976
16036
|
}
|
|
15977
16037
|
function buildProviderIndexes(providers) {
|
|
15978
16038
|
const kindRegistry = /* @__PURE__ */ new Map();
|
|
@@ -16409,7 +16469,7 @@ var SCAN_RUNNER_TEXTS = {
|
|
|
16409
16469
|
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
16470
|
/**
|
|
16411
16471
|
* Reference-paths walker hit `REFERENCE_WALK_MAX_FILES` and stopped
|
|
16412
|
-
* early. The set may be incomplete for link validation; `core/broken
|
|
16472
|
+
* early. The set may be incomplete for link validation; `core/reference-broken`
|
|
16413
16473
|
* still works against whatever made it in.
|
|
16414
16474
|
*/
|
|
16415
16475
|
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.",
|
|
@@ -16424,9 +16484,18 @@ var SCAN_RUNNER_TEXTS = {
|
|
|
16424
16484
|
* markers (`.claude/`, `.codex/`, `AGENTS.md`, `.cursor/`) anywhere
|
|
16425
16485
|
* under cwd or the effective scan roots. Plain-markdown projects
|
|
16426
16486
|
* keep scanning fine; provider-specific extractors silently no-op
|
|
16427
|
-
* for this scan.
|
|
16487
|
+
* for this scan. Follows `context/cli-output-style.md` §3.1b
|
|
16488
|
+
* (two-line block, glyph + dim hint):
|
|
16489
|
+
* - line 1: `{{glyph}}` (yellow `⚠`) + headline naming the
|
|
16490
|
+
* missing markers,
|
|
16491
|
+
* - line 2 (indent 3): `{{hint}}`, dim, names the consequence
|
|
16492
|
+
* and the actionable next step.
|
|
16493
|
+
* Both the full block AND the bare hint are catalog-side so the
|
|
16494
|
+
* caller can wrap the hint in `ansi.dim(...)` without splitting
|
|
16495
|
+
* the template manually.
|
|
16428
16496
|
*/
|
|
16429
|
-
activeProviderNoMarkerWarning: "No provider markers detected (.claude/, .codex/, AGENTS.md, .cursor/)
|
|
16497
|
+
activeProviderNoMarkerWarning: "{{glyph}} No provider markers detected (.claude/, .codex/, AGENTS.md, .cursor/).\n {{hint}}\n",
|
|
16498
|
+
activeProviderNoMarkerWarningHint: "Scanning as universal markdown only; provider-specific link types (e.g. claude @-directives, /-commands) will not appear. Set `activeProvider` in .skill-map/settings.json or install a provider plugin to enable them.",
|
|
16430
16499
|
/**
|
|
16431
16500
|
* Active-provider bootstrap: filesystem auto-detect found exactly
|
|
16432
16501
|
* one marker and persisted the detected id to project settings.
|
|
@@ -16579,7 +16648,14 @@ async function bootstrapActiveProvider(opts) {
|
|
|
16579
16648
|
}
|
|
16580
16649
|
const detected = aggregateDetected(opts.cwd, opts.effectiveRoots, fromCwd.detected);
|
|
16581
16650
|
if (detected.length === 0) {
|
|
16582
|
-
opts.
|
|
16651
|
+
const warnGlyph = opts.style?.warnGlyph ?? "\u26A0";
|
|
16652
|
+
const dim = opts.style?.dim ?? ((s) => s);
|
|
16653
|
+
opts.printer.warn(
|
|
16654
|
+
tx(SCAN_RUNNER_TEXTS.activeProviderNoMarkerWarning, {
|
|
16655
|
+
glyph: warnGlyph,
|
|
16656
|
+
hint: dim(SCAN_RUNNER_TEXTS.activeProviderNoMarkerWarningHint)
|
|
16657
|
+
})
|
|
16658
|
+
);
|
|
16583
16659
|
return { kind: "ok", activeProvider: null, source: "none" };
|
|
16584
16660
|
}
|
|
16585
16661
|
if (detected.length === 1) {
|
|
@@ -18788,21 +18864,6 @@ import { Command as Command22, Option as Option21 } from "clipanion";
|
|
|
18788
18864
|
// cli/i18n/plugins.texts.ts
|
|
18789
18865
|
var PLUGINS_TEXTS = {
|
|
18790
18866
|
// --- enable / disable error guidance --------------------------------
|
|
18791
|
-
// Spec § A.7, granularity validation. The CLI rejects mismatched ids
|
|
18792
|
-
// up front (instead of silently writing a config_plugins row that the
|
|
18793
|
-
// runtime would later ignore) so the user learns the model immediately.
|
|
18794
|
-
/**
|
|
18795
|
-
* Granularity-mismatch errors share a structured shape:
|
|
18796
|
-
* ✕ <headline>
|
|
18797
|
-
* <fix-line>
|
|
18798
|
-
* <hint-line>
|
|
18799
|
-
* Glyph + indent + dim hint applied at the call site so all four
|
|
18800
|
-
* "wrong shape" advisories read the same way.
|
|
18801
|
-
*/
|
|
18802
|
-
granularityBundleRejectsQualified: "{{glyph}} '{{bundleId}}' has granularity=bundle.\n Use `sm plugins {{verb}} {{bundleId}}` to {{verb}} the whole bundle.\n {{hint}}\n",
|
|
18803
|
-
granularityBundleRejectsQualifiedHint: "Individual extensions inside a bundle-granularity plugin cannot be toggled.",
|
|
18804
|
-
granularityExtensionRejectsBundleId: "{{glyph}} '{{bundleId}}' has granularity=extension.\n Use `sm plugins {{verb}} {{bundleId}}/<ext-id>` to {{verb}} a single extension.\n {{hint}}\n",
|
|
18805
|
-
granularityExtensionRejectsBundleIdHint: "Run `sm plugins list` for the per-extension qualified ids.",
|
|
18806
18867
|
pluginNotFound: "{{glyph}} Plugin not found: {{id}}\n {{hint}}\n",
|
|
18807
18868
|
pluginNotFoundHint: "Run `sm plugins list` for discovered ids and the qualified extension ids.",
|
|
18808
18869
|
pluginLocked: '{{glyph}} Plugin "{{id}}" is locked by the host and cannot be toggled.\n {{hint}}\n',
|
|
@@ -18833,8 +18894,14 @@ var PLUGINS_TEXTS = {
|
|
|
18833
18894
|
// --- list verb -------------------------------------------------------
|
|
18834
18895
|
listEmpty: "No plugins discovered.\n",
|
|
18835
18896
|
// --- doctor verb -----------------------------------------------------
|
|
18836
|
-
/**
|
|
18837
|
-
|
|
18897
|
+
/**
|
|
18898
|
+
* One-line summary that opens the human doctor output. `enabled` is
|
|
18899
|
+
* the count of enabled extensions across every bundle (every
|
|
18900
|
+
* extension is independently toggle-able by its qualified id); the
|
|
18901
|
+
* value matches the row count rendered by `sm plugins list` once
|
|
18902
|
+
* disabled extensions are filtered out.
|
|
18903
|
+
*/
|
|
18904
|
+
doctorSummary: "plugins doctor: {{enabled}} enabled extension{{enabledPlural}} \xB7 {{issues}} issue{{issuesPlural}} \xB7 {{warnings}} warning{{warningsPlural}}\n\n",
|
|
18838
18905
|
/** Source breakdown row (built-in vs user). Indented 4 to match the status rows. */
|
|
18839
18906
|
doctorSourceRow: " {{label}} {{count}}\n",
|
|
18840
18907
|
/** Status breakdown table heading. */
|
|
@@ -18868,8 +18935,35 @@ var PLUGINS_TEXTS = {
|
|
|
18868
18935
|
toggleNeitherIdNorAllHint: "Examples: `sm plugins {{verb}} <id1> <id2>` (explicit set), `sm plugins {{verb}} --all` (every discovered plugin).",
|
|
18869
18936
|
toggleResolveError: "{{error}}",
|
|
18870
18937
|
toggleAppliedSingle: "{{verbPast}}: {{id}}\n",
|
|
18871
|
-
toggleAppliedManyHeader: "{{verbPast}}: {{count}}
|
|
18938
|
+
toggleAppliedManyHeader: "{{verbPast}}: {{count}} extension(s)\n",
|
|
18872
18939
|
toggleAppliedManyRow: " - {{id}}\n",
|
|
18940
|
+
/**
|
|
18941
|
+
* Macro expansion summary printed on stderr before the confirm
|
|
18942
|
+
* prompt (or before the `--yes` rejection). The block lists every
|
|
18943
|
+
* qualified extension id the bare bundle id resolves to, so the
|
|
18944
|
+
* user sees the exact set that would flip.
|
|
18945
|
+
*/
|
|
18946
|
+
bundleMacroHeader: "sm plugins {{verb}} {{bundleId}}: this will affect {{count}} extensions:\n",
|
|
18947
|
+
bundleMacroRow: " - {{id}}\n",
|
|
18948
|
+
/**
|
|
18949
|
+
* Interactive prompt rendered to a TTY by the macro path. The
|
|
18950
|
+
* `confirm` helper appends the `[y/N]` suffix from UTIL_TEXTS.
|
|
18951
|
+
*/
|
|
18952
|
+
bundleMacroConfirmPrompt: "Apply this {{verb}} to every listed extension?",
|
|
18953
|
+
/**
|
|
18954
|
+
* Stderr advisory when a TTY user answers no to the macro prompt.
|
|
18955
|
+
* The verb exits non-zero (ExitCode.Error) so callers can detect
|
|
18956
|
+
* the cancellation.
|
|
18957
|
+
*/
|
|
18958
|
+
bundleMacroCancelled: "Cancelled.\n",
|
|
18959
|
+
/**
|
|
18960
|
+
* Non-TTY rejection path: pipes / CI cannot prompt, so the verb
|
|
18961
|
+
* refuses unless `--yes` is set. The body lines come from
|
|
18962
|
+
* `bundleMacroHeader` / `bundleMacroRow` above; this template adds
|
|
18963
|
+
* the directed re-run hint.
|
|
18964
|
+
*/
|
|
18965
|
+
bundleMacroRequiresYes: "{{glyph}} Refusing to {{verb}} multiple extensions without confirmation.\n {{hint}}\n",
|
|
18966
|
+
bundleMacroRequiresYesHint: "Re-run with --yes to apply, or pass a qualified id `<bundle>/<extension>` for a single extension.",
|
|
18873
18967
|
// --- list / show renderers ------------------------------------------
|
|
18874
18968
|
rowStatusOk: "ok",
|
|
18875
18969
|
rowStatusOff: "off",
|
|
@@ -18915,17 +19009,15 @@ var PLUGINS_TEXTS = {
|
|
|
18915
19009
|
/** Extensions block heading, separated from the header by a blank line. */
|
|
18916
19010
|
detailExtensionsBlock: "\n",
|
|
18917
19011
|
/**
|
|
18918
|
-
* Extension row
|
|
18919
|
-
*
|
|
18920
|
-
* for {{kind}} and {{name}} is computed at render
|
|
18921
|
-
* align inside the block.
|
|
18922
|
-
|
|
18923
|
-
|
|
18924
|
-
|
|
18925
|
-
* Extension row WITHOUT per-extension glyph (granularity=bundle).
|
|
18926
|
-
* The bundle is the only toggle; per-extension status is implicit.
|
|
19012
|
+
* Extension row inside the bundle detail. Every extension is
|
|
19013
|
+
* independently toggle-able, so every row carries its own glyph
|
|
19014
|
+
* (✓ / ✕). Padding for {{kind}} and {{name}} is computed at render
|
|
19015
|
+
* time so columns align inside the block. `{{versionSuffix}}` is
|
|
19016
|
+
* either ` v<x.y.z>` (user plugins) or empty (built-in bundles,
|
|
19017
|
+
* which inherit the CLI version and do not maintain per-extension
|
|
19018
|
+
* versions of their own).
|
|
18927
19019
|
*/
|
|
18928
|
-
|
|
19020
|
+
detailExtensionRowGlyph: " {{glyph}} {{kind}} {{name}}{{versionSuffix}}\n",
|
|
18929
19021
|
detailVersionUnknown: "?",
|
|
18930
19022
|
detailCompatUnknown: "?",
|
|
18931
19023
|
/**
|
|
@@ -18975,6 +19067,27 @@ var PLUGINS_TEXTS = {
|
|
|
18975
19067
|
slotsListTipText: "Tip: full spec at spec/view-slots.md and spec/input-types.md."
|
|
18976
19068
|
};
|
|
18977
19069
|
|
|
19070
|
+
// plugins/presentation-order.ts
|
|
19071
|
+
var BUILT_IN_BUNDLE_PRESENTATION_ORDER = [
|
|
19072
|
+
"core",
|
|
19073
|
+
"claude",
|
|
19074
|
+
"antigravity",
|
|
19075
|
+
"openai",
|
|
19076
|
+
"agent-skills"
|
|
19077
|
+
];
|
|
19078
|
+
function sortBundlesForPresentation(bundles) {
|
|
19079
|
+
const orderIndex = (id) => {
|
|
19080
|
+
const idx = BUILT_IN_BUNDLE_PRESENTATION_ORDER.indexOf(id);
|
|
19081
|
+
return idx >= 0 ? idx : BUILT_IN_BUNDLE_PRESENTATION_ORDER.length;
|
|
19082
|
+
};
|
|
19083
|
+
return [...bundles].sort((a, b) => {
|
|
19084
|
+
const ai = orderIndex(a.id);
|
|
19085
|
+
const bi = orderIndex(b.id);
|
|
19086
|
+
if (ai !== bi) return ai - bi;
|
|
19087
|
+
return a.id.localeCompare(b.id);
|
|
19088
|
+
});
|
|
19089
|
+
}
|
|
19090
|
+
|
|
18978
19091
|
// cli/commands/plugins/shared.ts
|
|
18979
19092
|
import { resolve as resolve32 } from "path";
|
|
18980
19093
|
function resolveSearchPaths2(opts, cwd) {
|
|
@@ -19004,26 +19117,24 @@ async function loadAll(opts) {
|
|
|
19004
19117
|
return loader.discoverAndLoadAll();
|
|
19005
19118
|
}
|
|
19006
19119
|
function builtInRows(resolveEnabled) {
|
|
19007
|
-
return builtInBundles.map((bundle) => {
|
|
19008
|
-
const
|
|
19009
|
-
const extensions = bundle.extensions.map((ext) => extensionRowFromBuiltIn(ext, bundle, bundleEnabled, resolveEnabled));
|
|
19120
|
+
return sortBundlesForPresentation(builtInBundles).map((bundle) => {
|
|
19121
|
+
const extensions = bundle.extensions.map((ext) => extensionRowFromBuiltIn(ext, bundle, resolveEnabled));
|
|
19010
19122
|
const manifestSummary = bundle.extensions.map((ext) => `${ext.kind}:${qualifiedExtensionId(bundle.id, ext.id)}@${ext.version}`).join(", ");
|
|
19011
19123
|
return {
|
|
19012
19124
|
id: bundle.id,
|
|
19013
|
-
|
|
19014
|
-
enabled: bundleEnabled,
|
|
19125
|
+
enabled: extensions.some((e) => e.enabled),
|
|
19015
19126
|
description: bundle.description,
|
|
19016
19127
|
extensions,
|
|
19017
19128
|
manifestSummary
|
|
19018
19129
|
};
|
|
19019
19130
|
});
|
|
19020
19131
|
}
|
|
19021
|
-
function extensionRowFromBuiltIn(ext, bundle,
|
|
19132
|
+
function extensionRowFromBuiltIn(ext, bundle, resolveEnabled) {
|
|
19022
19133
|
const row = {
|
|
19023
19134
|
id: ext.id,
|
|
19024
19135
|
kind: ext.kind,
|
|
19025
19136
|
version: ext.version,
|
|
19026
|
-
enabled:
|
|
19137
|
+
enabled: resolveEnabled(qualifiedExtensionId(bundle.id, ext.id)),
|
|
19027
19138
|
description: ext.description ?? ""
|
|
19028
19139
|
};
|
|
19029
19140
|
if (ext.entry !== void 0) row.entry = ext.entry;
|
|
@@ -19077,14 +19188,14 @@ var PluginsListCommand = class extends SmCommand {
|
|
|
19077
19188
|
return ExitCode.Ok;
|
|
19078
19189
|
}
|
|
19079
19190
|
const ansi = this.ansiFor("stdout");
|
|
19080
|
-
this.printer.data(renderListHuman(builtIns2, plugins, ansi));
|
|
19191
|
+
this.printer.data(renderListHuman(builtIns2, plugins, resolveEnabled, ansi));
|
|
19081
19192
|
return ExitCode.Ok;
|
|
19082
19193
|
}
|
|
19083
19194
|
};
|
|
19084
|
-
function renderListHuman(builtIns2, plugins, ansi) {
|
|
19195
|
+
function renderListHuman(builtIns2, plugins, resolveEnabled, ansi) {
|
|
19085
19196
|
const rows = [
|
|
19086
19197
|
...builtIns2.map(builtInToListRow),
|
|
19087
|
-
...plugins.map(pluginToListRow)
|
|
19198
|
+
...plugins.map((p) => pluginToListRow(p, resolveEnabled))
|
|
19088
19199
|
];
|
|
19089
19200
|
const idWidth = Math.max(...rows.map((r) => r.id.length));
|
|
19090
19201
|
const countWidth = Math.max(
|
|
@@ -19115,9 +19226,9 @@ function renderListHuman(builtIns2, plugins, ansi) {
|
|
|
19115
19226
|
return lines.join("\n") + "\n" + PLUGINS_TEXTS.listTipShow;
|
|
19116
19227
|
}
|
|
19117
19228
|
function builtInToListRow(b) {
|
|
19118
|
-
const names = b.
|
|
19229
|
+
const names = b.extensions.map(
|
|
19119
19230
|
(e) => e.enabled ? e.id : `${PLUGINS_TEXTS.rowGlyphOff} ${e.id}`
|
|
19120
|
-
)
|
|
19231
|
+
);
|
|
19121
19232
|
return {
|
|
19122
19233
|
id: b.id,
|
|
19123
19234
|
enabled: b.enabled,
|
|
@@ -19125,9 +19236,14 @@ function builtInToListRow(b) {
|
|
|
19125
19236
|
names
|
|
19126
19237
|
};
|
|
19127
19238
|
}
|
|
19128
|
-
function pluginToListRow(p) {
|
|
19129
|
-
const
|
|
19130
|
-
const
|
|
19239
|
+
function pluginToListRow(p, resolveEnabled) {
|
|
19240
|
+
const isLoaded = p.status === "enabled";
|
|
19241
|
+
const extensions = p.extensions ?? [];
|
|
19242
|
+
const enabled = isLoaded ? extensions.length === 0 || extensions.some((e) => resolveEnabled(qualifiedExtensionId(p.id, e.id))) : false;
|
|
19243
|
+
const names = extensions.map((e) => {
|
|
19244
|
+
const safeId = sanitizeForTerminal(e.id);
|
|
19245
|
+
return resolveEnabled(qualifiedExtensionId(p.id, e.id)) ? safeId : `${PLUGINS_TEXTS.rowGlyphOff} ${safeId}`;
|
|
19246
|
+
});
|
|
19131
19247
|
const reason = p.status === "enabled" ? void 0 : sanitizeForTerminal(p.reason ?? "") || void 0;
|
|
19132
19248
|
return {
|
|
19133
19249
|
id: sanitizeForTerminal(p.id),
|
|
@@ -19139,10 +19255,10 @@ function pluginToListRow(p) {
|
|
|
19139
19255
|
}
|
|
19140
19256
|
function wrapNames(names, indent, maxWidth) {
|
|
19141
19257
|
const out = [];
|
|
19142
|
-
const
|
|
19258
|
+
const sep8 = ", ";
|
|
19143
19259
|
let current = "";
|
|
19144
19260
|
for (const name of names) {
|
|
19145
|
-
const candidate = current === "" ? name : `${current}${
|
|
19261
|
+
const candidate = current === "" ? name : `${current}${sep8}${name}`;
|
|
19146
19262
|
if (indent.length + candidate.length > maxWidth && current !== "") {
|
|
19147
19263
|
out.push(`${current},`);
|
|
19148
19264
|
current = name;
|
|
@@ -19165,11 +19281,10 @@ var PluginsShowCommand = class extends SmCommand {
|
|
|
19165
19281
|
Accepts a bundle / plugin id (\`core\`, \`claude\`, \`my-plugin\`)
|
|
19166
19282
|
or a qualified extension id (\`core/<ext-id>\`,
|
|
19167
19283
|
\`<plugin>/<ext-id>\`). When given a qualified id, validates the
|
|
19168
|
-
extension exists and renders
|
|
19169
|
-
|
|
19170
|
-
|
|
19171
|
-
\`sm plugins
|
|
19172
|
-
cleanly here too.
|
|
19284
|
+
extension exists and renders a single-extension detail block.
|
|
19285
|
+
The bare form renders the parent bundle's detail with per-extension
|
|
19286
|
+
status. The same id shapes \`sm plugins enable\` and
|
|
19287
|
+
\`sm plugins disable\` accept resolve cleanly here too.
|
|
19173
19288
|
`
|
|
19174
19289
|
});
|
|
19175
19290
|
id = Option22.String({ required: true });
|
|
@@ -19300,16 +19415,13 @@ function sortExtensionsCanonical(exts) {
|
|
|
19300
19415
|
});
|
|
19301
19416
|
}
|
|
19302
19417
|
function renderBuiltInDetail(b, ansi) {
|
|
19303
|
-
const
|
|
19304
|
-
const glyph = enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff);
|
|
19418
|
+
const glyph = b.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff);
|
|
19305
19419
|
const count = b.extensions.length;
|
|
19306
|
-
const qualify = b.granularity === "extension";
|
|
19307
19420
|
const sorted = sortExtensionsCanonical(b.extensions);
|
|
19308
19421
|
const items = sorted.map((ext) => ({
|
|
19309
|
-
glyph:
|
|
19422
|
+
glyph: ext.enabled ? ansi.green(PLUGINS_TEXTS.rowGlyphOk) : ansi.red(PLUGINS_TEXTS.rowGlyphOff),
|
|
19310
19423
|
kind: ext.kind,
|
|
19311
|
-
name:
|
|
19312
|
-
version: ext.version
|
|
19424
|
+
name: `${b.id}/${ext.id}`
|
|
19313
19425
|
}));
|
|
19314
19426
|
return tx(PLUGINS_TEXTS.detailHeaderBuiltIn, {
|
|
19315
19427
|
glyph,
|
|
@@ -19377,15 +19489,18 @@ function renderPluginDetailFields(match) {
|
|
|
19377
19489
|
function collectPluginExtensionItems(match, ansi) {
|
|
19378
19490
|
const enabled = match.status === "enabled";
|
|
19379
19491
|
if (!enabled || !match.extensions) return [];
|
|
19380
|
-
const qualify = match.granularity === "extension";
|
|
19381
19492
|
const safeBundleId = sanitizeForTerminal(match.id);
|
|
19382
19493
|
const sorted = sortExtensionsCanonical(match.extensions);
|
|
19383
19494
|
return sorted.map((ext) => {
|
|
19384
19495
|
const safeExtId = sanitizeForTerminal(ext.id);
|
|
19385
19496
|
return {
|
|
19386
|
-
|
|
19497
|
+
// User plugins surfaced via `loadAll` already filter on the
|
|
19498
|
+
// resolver, so a reachable extension on this surface is enabled
|
|
19499
|
+
// by construction. The disabled path goes through the bundle
|
|
19500
|
+
// status header above (✕ on the row).
|
|
19501
|
+
glyph: ansi.green(PLUGINS_TEXTS.rowGlyphOk),
|
|
19387
19502
|
kind: sanitizeForTerminal(ext.kind),
|
|
19388
|
-
name:
|
|
19503
|
+
name: `${safeBundleId}/${safeExtId}`,
|
|
19389
19504
|
version: sanitizeForTerminal(ext.version)
|
|
19390
19505
|
};
|
|
19391
19506
|
});
|
|
@@ -19393,29 +19508,21 @@ function collectPluginExtensionItems(match, ansi) {
|
|
|
19393
19508
|
function renderExtensionItems(items) {
|
|
19394
19509
|
if (items.length === 0) return "";
|
|
19395
19510
|
const kindWidth = Math.max(...items.map((i) => i.kind.length));
|
|
19396
|
-
const
|
|
19511
|
+
const anyVersion = items.some((i) => i.version !== void 0);
|
|
19512
|
+
const nameWidth = anyVersion ? Math.max(...items.map((i) => i.name.length)) : 0;
|
|
19397
19513
|
const out = [];
|
|
19398
19514
|
for (const item of items) {
|
|
19399
19515
|
const kind = item.kind.padEnd(kindWidth);
|
|
19400
|
-
const name = item.name.padEnd(nameWidth);
|
|
19401
|
-
|
|
19402
|
-
|
|
19403
|
-
|
|
19404
|
-
|
|
19405
|
-
|
|
19406
|
-
|
|
19407
|
-
|
|
19408
|
-
|
|
19409
|
-
|
|
19410
|
-
} else {
|
|
19411
|
-
out.push(
|
|
19412
|
-
tx(PLUGINS_TEXTS.detailExtensionRowBare, {
|
|
19413
|
-
kind,
|
|
19414
|
-
name,
|
|
19415
|
-
version: item.version
|
|
19416
|
-
})
|
|
19417
|
-
);
|
|
19418
|
-
}
|
|
19516
|
+
const name = anyVersion ? item.name.padEnd(nameWidth) : item.name;
|
|
19517
|
+
const versionSuffix = item.version ? ` v${item.version}` : "";
|
|
19518
|
+
out.push(
|
|
19519
|
+
tx(PLUGINS_TEXTS.detailExtensionRowGlyph, {
|
|
19520
|
+
glyph: item.glyph,
|
|
19521
|
+
kind,
|
|
19522
|
+
name,
|
|
19523
|
+
versionSuffix
|
|
19524
|
+
})
|
|
19525
|
+
);
|
|
19419
19526
|
}
|
|
19420
19527
|
return out.join("");
|
|
19421
19528
|
}
|
|
@@ -19426,7 +19533,7 @@ function renderBuiltInExtensionDetail(bundleId, ext, ansi) {
|
|
|
19426
19533
|
qualifiedId: sanitizeForTerminal(`${bundleId}/${ext.id}`),
|
|
19427
19534
|
source: ansi.dim(PLUGINS_TEXTS.sourceBuiltIn)
|
|
19428
19535
|
});
|
|
19429
|
-
const meta = { kind: ext.kind
|
|
19536
|
+
const meta = { kind: ext.kind };
|
|
19430
19537
|
if (ext.description) meta.description = ext.description;
|
|
19431
19538
|
if (ext.entry !== void 0) meta.entry = ext.entry;
|
|
19432
19539
|
return header + "\n" + renderExtensionFields(meta);
|
|
@@ -19465,7 +19572,12 @@ function readInstanceMeta(instance) {
|
|
|
19465
19572
|
function renderExtensionFields(meta) {
|
|
19466
19573
|
const fields = [];
|
|
19467
19574
|
fields.push({ label: PLUGINS_TEXTS.detailFieldKind, value: sanitizeForTerminal(meta.kind) });
|
|
19468
|
-
|
|
19575
|
+
if (meta.version) {
|
|
19576
|
+
fields.push({
|
|
19577
|
+
label: PLUGINS_TEXTS.detailFieldVersion,
|
|
19578
|
+
value: sanitizeForTerminal(meta.version)
|
|
19579
|
+
});
|
|
19580
|
+
}
|
|
19469
19581
|
if (meta.stability) {
|
|
19470
19582
|
fields.push({
|
|
19471
19583
|
label: PLUGINS_TEXTS.detailFieldStability,
|
|
@@ -19519,7 +19631,7 @@ var PluginsDoctorCommand = class extends SmCommand {
|
|
|
19519
19631
|
const plugins = await loadAll({ pluginDir: this.pluginDir });
|
|
19520
19632
|
const resolveEnabled = await buildResolver();
|
|
19521
19633
|
const builtIns2 = builtInRows(resolveEnabled);
|
|
19522
|
-
const counts = countByStatus(builtIns2, plugins);
|
|
19634
|
+
const counts = countByStatus(builtIns2, plugins, resolveEnabled);
|
|
19523
19635
|
const knownKinds = collectKnownKinds(plugins);
|
|
19524
19636
|
const applicableKindWarnings = collectApplicableKindWarnings(plugins, knownKinds);
|
|
19525
19637
|
const unknownSlotWarnings = collectUnknownSlotWarnings(plugins, KNOWN_SLOT_NAMES);
|
|
@@ -19554,6 +19666,7 @@ var PluginsDoctorCommand = class extends SmCommand {
|
|
|
19554
19666
|
this.printer.data(
|
|
19555
19667
|
tx(PLUGINS_TEXTS.doctorSummary, {
|
|
19556
19668
|
enabled,
|
|
19669
|
+
enabledPlural: enabled === 1 ? "" : "s",
|
|
19557
19670
|
issues: badCount,
|
|
19558
19671
|
issuesPlural: badCount === 1 ? "" : "s",
|
|
19559
19672
|
warnings,
|
|
@@ -19651,7 +19764,7 @@ var PluginsDoctorCommand = class extends SmCommand {
|
|
|
19651
19764
|
}
|
|
19652
19765
|
}
|
|
19653
19766
|
};
|
|
19654
|
-
function countByStatus(builtIns2, plugins) {
|
|
19767
|
+
function countByStatus(builtIns2, plugins, resolveEnabled) {
|
|
19655
19768
|
const counts = {
|
|
19656
19769
|
enabled: 0,
|
|
19657
19770
|
disabled: 0,
|
|
@@ -19662,15 +19775,20 @@ function countByStatus(builtIns2, plugins) {
|
|
|
19662
19775
|
"id-collision": 0
|
|
19663
19776
|
};
|
|
19664
19777
|
for (const b of builtIns2) {
|
|
19665
|
-
|
|
19666
|
-
counts[
|
|
19667
|
-
}
|
|
19668
|
-
|
|
19669
|
-
|
|
19670
|
-
|
|
19778
|
+
for (const ext of b.extensions) {
|
|
19779
|
+
counts[ext.enabled ? "enabled" : "disabled"]++;
|
|
19780
|
+
}
|
|
19781
|
+
}
|
|
19782
|
+
for (const p of plugins) {
|
|
19783
|
+
if (p.status !== "enabled" || !p.extensions) {
|
|
19784
|
+
counts[p.status]++;
|
|
19785
|
+
continue;
|
|
19786
|
+
}
|
|
19787
|
+
for (const ext of p.extensions) {
|
|
19788
|
+
const enabled = resolveEnabled(`${p.id}/${ext.id}`);
|
|
19789
|
+
counts[enabled ? "enabled" : "disabled"]++;
|
|
19671
19790
|
}
|
|
19672
19791
|
}
|
|
19673
|
-
for (const p of plugins) counts[p.status]++;
|
|
19674
19792
|
return counts;
|
|
19675
19793
|
}
|
|
19676
19794
|
function forEachProviderInstance(plugins, callback) {
|
|
@@ -19710,10 +19828,13 @@ function extensionInstance(ext) {
|
|
|
19710
19828
|
}
|
|
19711
19829
|
function collectKnownKinds(plugins) {
|
|
19712
19830
|
const known = /* @__PURE__ */ new Set();
|
|
19713
|
-
forEachProviderInstance(plugins, ({ instance }) => {
|
|
19831
|
+
forEachProviderInstance(plugins, ({ pluginId, instance }) => {
|
|
19714
19832
|
const map = instance["kinds"];
|
|
19715
19833
|
if (map === null || typeof map !== "object") return;
|
|
19716
|
-
for (const k of Object.keys(map))
|
|
19834
|
+
for (const k of Object.keys(map)) {
|
|
19835
|
+
known.add(k);
|
|
19836
|
+
known.add(qualifiedExtensionId(pluginId, k));
|
|
19837
|
+
}
|
|
19717
19838
|
});
|
|
19718
19839
|
return known;
|
|
19719
19840
|
}
|
|
@@ -19860,6 +19981,9 @@ function buildDoctorJsonEnvelope(args2) {
|
|
|
19860
19981
|
import { Command as Command25, Option as Option24 } from "clipanion";
|
|
19861
19982
|
var TogglePluginsBase = class extends SmCommand {
|
|
19862
19983
|
all = Option24.Boolean("--all", false);
|
|
19984
|
+
yes = Option24.Boolean("--yes,-y", false, {
|
|
19985
|
+
description: "Skip the interactive confirm when a bare bundle id (or --all) fans the toggle out across multiple extensions."
|
|
19986
|
+
});
|
|
19863
19987
|
ids = Option24.Rest({ name: "ids" });
|
|
19864
19988
|
async toggle(enabled) {
|
|
19865
19989
|
const verb = enabled ? "enable" : "disable";
|
|
@@ -19868,14 +19992,17 @@ var TogglePluginsBase = class extends SmCommand {
|
|
|
19868
19992
|
if (argError !== null) return argError;
|
|
19869
19993
|
const plugins = await loadAll({ pluginDir: void 0 });
|
|
19870
19994
|
const catalogue = bundleCatalogue(plugins);
|
|
19871
|
-
const targetsResult = this.#pickTargets(catalogue,
|
|
19995
|
+
const targetsResult = this.#pickTargets(catalogue, stderrAnsi);
|
|
19872
19996
|
if (typeof targetsResult === "number") return targetsResult;
|
|
19873
19997
|
let targets = targetsResult;
|
|
19998
|
+
const macroOk = await this.#confirmMacroIfNeeded(targets, verb, stderrAnsi);
|
|
19999
|
+
if (!macroOk) return ExitCode.Error;
|
|
19874
20000
|
const lockError = this.#applyLockGate(targets, stderrAnsi);
|
|
19875
20001
|
if (typeof lockError === "number") return lockError;
|
|
19876
20002
|
targets = lockError;
|
|
19877
|
-
|
|
19878
|
-
this.#
|
|
20003
|
+
const keys = expandToKeys(targets);
|
|
20004
|
+
await this.#persistKeys(keys, enabled);
|
|
20005
|
+
this.#renderSuccess(keys, enabled);
|
|
19879
20006
|
return ExitCode.Ok;
|
|
19880
20007
|
}
|
|
19881
20008
|
/**
|
|
@@ -19908,87 +20035,166 @@ var TogglePluginsBase = class extends SmCommand {
|
|
|
19908
20035
|
/**
|
|
19909
20036
|
* Resolve `<id>...` against the catalogue or fan out via `--all`.
|
|
19910
20037
|
* Returns the target list on success, or the exit code on a
|
|
19911
|
-
* directed-error path (unknown id,
|
|
19912
|
-
*
|
|
19913
|
-
* `--all` is a macro on bundle ids: every plugin / bundle the user
|
|
19914
|
-
* can see. We deliberately do NOT expand to qualified
|
|
19915
|
-
* <bundle>/<ext> keys, that would silently flip a granularity
|
|
19916
|
-
* policy. For granularity=extension bundles the user already hits
|
|
19917
|
-
* the directed error message when they try the bundle id directly,
|
|
19918
|
-
* so `--all` skips them here too and the real "disable every core
|
|
19919
|
-
* extension" intent is served by `--no-built-ins` on `sm scan`.
|
|
20038
|
+
* directed-error path (unknown id, malformed qualified id).
|
|
19920
20039
|
*
|
|
19921
|
-
*
|
|
19922
|
-
*
|
|
19923
|
-
*
|
|
20040
|
+
* Repeated ids in the same call are deduped at the target level
|
|
20041
|
+
* (`origin === 'bare'` and `origin === 'qualified'` rows stay
|
|
20042
|
+
* distinct so the macro-confirm path can address each correctly).
|
|
20043
|
+
* The first unknown id aborts the batch before any DB write so the
|
|
20044
|
+
* user never lands in a partial state.
|
|
19924
20045
|
*/
|
|
19925
|
-
#pickTargets(catalogue,
|
|
20046
|
+
#pickTargets(catalogue, ansi) {
|
|
19926
20047
|
if (this.all) {
|
|
19927
|
-
return catalogue.
|
|
20048
|
+
return catalogue.map((b) => ({
|
|
20049
|
+
origin: "bare",
|
|
20050
|
+
bundleId: b.id,
|
|
20051
|
+
keys: b.extensionIds.map((extId) => qualifiedExtensionId(b.id, extId))
|
|
20052
|
+
}));
|
|
19928
20053
|
}
|
|
19929
|
-
const
|
|
20054
|
+
const out = [];
|
|
20055
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19930
20056
|
for (const rawId of this.ids) {
|
|
19931
|
-
const resolved = resolveToggleTarget(rawId, catalogue,
|
|
20057
|
+
const resolved = resolveToggleTarget(rawId, catalogue, ansi);
|
|
19932
20058
|
if ("error" in resolved) {
|
|
19933
20059
|
this.printer.error(tx(PLUGINS_TEXTS.toggleResolveError, { error: resolved.error }));
|
|
19934
20060
|
return ExitCode.NotFound;
|
|
19935
20061
|
}
|
|
19936
|
-
keys.
|
|
20062
|
+
const novelKeys = resolved.keys.filter((k) => !seen.has(k));
|
|
20063
|
+
if (novelKeys.length === 0) continue;
|
|
20064
|
+
for (const k of novelKeys) seen.add(k);
|
|
20065
|
+
out.push({ ...resolved, keys: novelKeys });
|
|
20066
|
+
}
|
|
20067
|
+
return out;
|
|
20068
|
+
}
|
|
20069
|
+
/**
|
|
20070
|
+
* Macro gate: when the request would fan a single user input out
|
|
20071
|
+
* across more than one extension (either `--all` or a bare bundle
|
|
20072
|
+
* id whose bundle holds ≥2 extensions), confirm the intent.
|
|
20073
|
+
*
|
|
20074
|
+
* Resolution order:
|
|
20075
|
+
* 1. `--yes` flag: skip the prompt entirely.
|
|
20076
|
+
* 2. TTY stdin: render the list + ask interactively (`[y/N]`).
|
|
20077
|
+
* 3. Non-TTY (CI / pipe / agent harness): refuse with a directed
|
|
20078
|
+
* message that names the extensions and points at `--yes`.
|
|
20079
|
+
*
|
|
20080
|
+
* Returns `true` when the verb should proceed, `false` when it
|
|
20081
|
+
* should abort. Single-extension targets (bare bundle id mapping to
|
|
20082
|
+
* one child, or qualified ids) skip the gate uniformly.
|
|
20083
|
+
*/
|
|
20084
|
+
// Cyclomatic count comes from the three-stage gate (--yes shortcut,
|
|
20085
|
+
// TTY interactive path, non-TTY rejection) folded over the targets
|
|
20086
|
+
// loop. Splitting them scatters the contract without making the
|
|
20087
|
+
// algorithm clearer.
|
|
20088
|
+
// eslint-disable-next-line complexity
|
|
20089
|
+
async #confirmMacroIfNeeded(targets, verb, ansi) {
|
|
20090
|
+
const macroTargets = targets.filter((t) => requiresMacroConfirm(t));
|
|
20091
|
+
if (macroTargets.length === 0) return true;
|
|
20092
|
+
if (this.yes) return true;
|
|
20093
|
+
const isTty = Boolean(this.context.stdin && "isTTY" in this.context.stdin && this.context.stdin.isTTY);
|
|
20094
|
+
for (const target of macroTargets) {
|
|
20095
|
+
const bundleLabel = target.origin === "bare" ? target.bundleId ?? "--all" : "--all";
|
|
20096
|
+
this.printer.info(
|
|
20097
|
+
tx(PLUGINS_TEXTS.bundleMacroHeader, {
|
|
20098
|
+
verb,
|
|
20099
|
+
bundleId: sanitizeForTerminal(bundleLabel),
|
|
20100
|
+
count: target.keys.length
|
|
20101
|
+
})
|
|
20102
|
+
);
|
|
20103
|
+
for (const key of target.keys) {
|
|
20104
|
+
this.printer.info(tx(PLUGINS_TEXTS.bundleMacroRow, { id: sanitizeForTerminal(key) }));
|
|
20105
|
+
}
|
|
20106
|
+
}
|
|
20107
|
+
if (!isTty) {
|
|
20108
|
+
this.printer.error(
|
|
20109
|
+
tx(PLUGINS_TEXTS.bundleMacroRequiresYes, {
|
|
20110
|
+
glyph: ansi.red("\u2715"),
|
|
20111
|
+
verb,
|
|
20112
|
+
hint: ansi.dim(PLUGINS_TEXTS.bundleMacroRequiresYesHint)
|
|
20113
|
+
})
|
|
20114
|
+
);
|
|
20115
|
+
return false;
|
|
20116
|
+
}
|
|
20117
|
+
const ok = await confirm(
|
|
20118
|
+
tx(PLUGINS_TEXTS.bundleMacroConfirmPrompt, { verb }),
|
|
20119
|
+
{ stdin: this.context.stdin, stderr: this.context.stderr }
|
|
20120
|
+
);
|
|
20121
|
+
if (!ok) {
|
|
20122
|
+
this.printer.info(PLUGINS_TEXTS.bundleMacroCancelled);
|
|
19937
20123
|
}
|
|
19938
|
-
return
|
|
20124
|
+
return ok;
|
|
19939
20125
|
}
|
|
19940
20126
|
/**
|
|
19941
20127
|
* Host lock, see `src/kernel/config/locked-plugins.ts`. Bulk modes
|
|
19942
|
-
* (`--all
|
|
19943
|
-
*
|
|
19944
|
-
*
|
|
19945
|
-
* intended target was
|
|
20128
|
+
* (`--all`, an explicit batch of >1 targets, or a macro expansion
|
|
20129
|
+
* with >1 keys) silently skip locked extensions so the user can
|
|
20130
|
+
* still toggle the rest. Single-extension mode surfaces a directed
|
|
20131
|
+
* exit-5 message so the user knows their one intended target was
|
|
20132
|
+
* refused.
|
|
19946
20133
|
*/
|
|
19947
20134
|
#applyLockGate(targets, ansi) {
|
|
19948
|
-
|
|
19949
|
-
const
|
|
19950
|
-
if (
|
|
20135
|
+
const totalKeys = targets.reduce((acc, t) => acc + t.keys.length, 0);
|
|
20136
|
+
const bulk = this.all || this.ids.length > 1 || totalKeys > 1;
|
|
20137
|
+
if (bulk) {
|
|
20138
|
+
return targets.map((t) => ({ ...t, keys: t.keys.filter((k) => !isPluginLocked(k)) }));
|
|
20139
|
+
}
|
|
20140
|
+
const onlyKey = targets[0]?.keys[0];
|
|
20141
|
+
if (!onlyKey || !isPluginLocked(onlyKey)) return targets;
|
|
19951
20142
|
this.printer.error(
|
|
19952
20143
|
tx(PLUGINS_TEXTS.pluginLocked, {
|
|
19953
20144
|
glyph: ansi.red("\u2715"),
|
|
19954
|
-
id: sanitizeForTerminal(
|
|
20145
|
+
id: sanitizeForTerminal(onlyKey),
|
|
19955
20146
|
hint: ansi.dim(PLUGINS_TEXTS.pluginLockedHint)
|
|
19956
20147
|
})
|
|
19957
20148
|
);
|
|
19958
20149
|
return ExitCode.NotFound;
|
|
19959
20150
|
}
|
|
19960
20151
|
/**
|
|
19961
|
-
* Persist
|
|
19962
|
-
* the plugin's `scan_contributions` rows immediately (matches
|
|
19963
|
-
* BFF route, see `server/routes/plugins.ts:applyChangeToAdapter`).
|
|
19964
|
-
*
|
|
19965
|
-
*
|
|
19966
|
-
* how the catalog sweep groups rows.
|
|
20152
|
+
* Persist every qualified id in `config_plugins`. On disable, also
|
|
20153
|
+
* purge the plugin's `scan_contributions` rows immediately (matches
|
|
20154
|
+
* the BFF route, see `server/routes/plugins.ts:applyChangeToAdapter`).
|
|
20155
|
+
* Every key is `<bundle>/<ext>` shape so the contribution purge can
|
|
20156
|
+
* split into `(pluginId, extensionId)` cleanly.
|
|
19967
20157
|
*/
|
|
19968
|
-
async #
|
|
20158
|
+
async #persistKeys(keys, enabled) {
|
|
19969
20159
|
const ctx = defaultRuntimeContext();
|
|
19970
20160
|
const dbPath = resolveDbPath({ db: void 0, cwd: ctx.cwd });
|
|
19971
20161
|
await withSqlite({ databasePath: dbPath, autoBackup: false }, async (adapter) => {
|
|
19972
|
-
for (const id of
|
|
20162
|
+
for (const id of keys) {
|
|
19973
20163
|
await adapter.pluginConfig.set(id, enabled);
|
|
19974
20164
|
if (!enabled) await purgeContributionsFor(adapter, id);
|
|
19975
20165
|
}
|
|
19976
20166
|
});
|
|
19977
20167
|
}
|
|
19978
|
-
#renderSuccess(
|
|
20168
|
+
#renderSuccess(keys, enabled) {
|
|
19979
20169
|
const verbPast = enabled ? "enabled" : "disabled";
|
|
19980
|
-
if (
|
|
19981
|
-
this.printer.data(tx(PLUGINS_TEXTS.toggleAppliedSingle, { verbPast, id:
|
|
20170
|
+
if (keys.length === 1) {
|
|
20171
|
+
this.printer.data(tx(PLUGINS_TEXTS.toggleAppliedSingle, { verbPast, id: keys[0] }));
|
|
19982
20172
|
} else {
|
|
19983
20173
|
this.printer.data(
|
|
19984
|
-
tx(PLUGINS_TEXTS.toggleAppliedManyHeader, { verbPast, count:
|
|
20174
|
+
tx(PLUGINS_TEXTS.toggleAppliedManyHeader, { verbPast, count: keys.length })
|
|
19985
20175
|
);
|
|
19986
|
-
for (const id of
|
|
20176
|
+
for (const id of keys) {
|
|
19987
20177
|
this.printer.data(tx(PLUGINS_TEXTS.toggleAppliedManyRow, { id }));
|
|
19988
20178
|
}
|
|
19989
20179
|
}
|
|
19990
20180
|
}
|
|
19991
20181
|
};
|
|
20182
|
+
function requiresMacroConfirm(target) {
|
|
20183
|
+
if (target.origin !== "bare") return false;
|
|
20184
|
+
return target.keys.length >= 2;
|
|
20185
|
+
}
|
|
20186
|
+
function expandToKeys(targets) {
|
|
20187
|
+
const out = [];
|
|
20188
|
+
const seen = /* @__PURE__ */ new Set();
|
|
20189
|
+
for (const t of targets) {
|
|
20190
|
+
for (const k of t.keys) {
|
|
20191
|
+
if (seen.has(k)) continue;
|
|
20192
|
+
seen.add(k);
|
|
20193
|
+
out.push(k);
|
|
20194
|
+
}
|
|
20195
|
+
}
|
|
20196
|
+
return out;
|
|
20197
|
+
}
|
|
19992
20198
|
async function purgeContributionsFor(adapter, id) {
|
|
19993
20199
|
const slash = id.indexOf("/");
|
|
19994
20200
|
if (slash < 0) {
|
|
@@ -20001,25 +20207,24 @@ var PluginsEnableCommand = class extends TogglePluginsBase {
|
|
|
20001
20207
|
static paths = [["plugins", "enable"]];
|
|
20002
20208
|
static usage = Command25.Usage({
|
|
20003
20209
|
category: "Plugins",
|
|
20004
|
-
description: "Enable one or more
|
|
20210
|
+
description: "Enable one or more extensions (or --all). Persists in config_plugins.",
|
|
20005
20211
|
details: `
|
|
20006
|
-
Writes a row to config_plugins with enabled=1 per
|
|
20007
|
-
precedence over the team-shared baseline at
|
|
20212
|
+
Writes a row to config_plugins with enabled=1 per qualified
|
|
20213
|
+
extension id. Takes precedence over the team-shared baseline at
|
|
20008
20214
|
settings.json#/plugins/<id>/enabled. Use sm plugins disable to
|
|
20009
20215
|
flip; sm config reset plugins.<id>.enabled drops the settings.json
|
|
20010
20216
|
baseline.
|
|
20011
20217
|
|
|
20012
|
-
Accepts
|
|
20013
|
-
|
|
20014
|
-
|
|
20015
|
-
|
|
20016
|
-
|
|
20218
|
+
Accepts qualified ids (\`claude/at-directive\`) and bare bundle
|
|
20219
|
+
ids (\`claude\`, which fans the toggle out across every extension
|
|
20220
|
+
inside the bundle). Multi-extension bundles need --yes (or an
|
|
20221
|
+
interactive TTY confirm) to avoid flipping 27 core extensions by
|
|
20222
|
+
accident. Single-extension bundles (openai, agent-skills,
|
|
20223
|
+
antigravity) apply without prompting.
|
|
20017
20224
|
|
|
20018
|
-
|
|
20019
|
-
|
|
20020
|
-
|
|
20021
|
-
only qualified ids '<bundle>/<ext-id>'. Mismatches are rejected
|
|
20022
|
-
with directed guidance.
|
|
20225
|
+
Batches are all-or-nothing: a single unknown id aborts before
|
|
20226
|
+
any write. Repeated ids are deduped. Locked extensions inside a
|
|
20227
|
+
batch are silently skipped.
|
|
20023
20228
|
`
|
|
20024
20229
|
});
|
|
20025
20230
|
async run() {
|
|
@@ -20030,24 +20235,23 @@ var PluginsDisableCommand = class extends TogglePluginsBase {
|
|
|
20030
20235
|
static paths = [["plugins", "disable"]];
|
|
20031
20236
|
static usage = Command25.Usage({
|
|
20032
20237
|
category: "Plugins",
|
|
20033
|
-
description: "Disable one or more
|
|
20238
|
+
description: "Disable one or more extensions (or --all). Persists in config_plugins; does not delete files.",
|
|
20034
20239
|
details: `
|
|
20035
|
-
Writes a row to config_plugins with enabled=0 per
|
|
20036
|
-
still surfaces the plugin in
|
|
20037
|
-
status=disabled;
|
|
20038
|
-
|
|
20039
|
-
|
|
20040
|
-
Accepts
|
|
20041
|
-
|
|
20042
|
-
|
|
20043
|
-
|
|
20240
|
+
Writes a row to config_plugins with enabled=0 per qualified
|
|
20241
|
+
extension id. Discovery still surfaces the plugin in
|
|
20242
|
+
sm plugins list, but with status=disabled; the kernel will not
|
|
20243
|
+
run any of its disabled extensions.
|
|
20244
|
+
|
|
20245
|
+
Accepts qualified ids (\`core/markdown-link\`) and bare bundle
|
|
20246
|
+
ids (\`core\`, which fans the toggle out across every extension
|
|
20247
|
+
inside the bundle). Multi-extension bundles need --yes (or an
|
|
20248
|
+
interactive TTY confirm) to avoid flipping 27 core extensions by
|
|
20249
|
+
accident. Single-extension bundles (openai, agent-skills,
|
|
20250
|
+
antigravity) apply without prompting.
|
|
20251
|
+
|
|
20252
|
+
Batches are all-or-nothing: a single unknown id aborts before
|
|
20253
|
+
any write. Repeated ids are deduped. Locked extensions inside a
|
|
20044
20254
|
batch are silently skipped.
|
|
20045
|
-
|
|
20046
|
-
Granularity: a bundle-granularity plugin (default for user plugins,
|
|
20047
|
-
and the built-in 'claude' bundle) accepts only the bundle id. An
|
|
20048
|
-
extension-granularity plugin (the built-in 'core' bundle) accepts
|
|
20049
|
-
only qualified ids '<bundle>/<ext-id>'. Mismatches are rejected
|
|
20050
|
-
with directed guidance.
|
|
20051
20255
|
`
|
|
20052
20256
|
});
|
|
20053
20257
|
async run() {
|
|
@@ -20059,23 +20263,21 @@ function bundleCatalogue(plugins) {
|
|
|
20059
20263
|
for (const bundle of builtInBundles) {
|
|
20060
20264
|
out.push({
|
|
20061
20265
|
id: bundle.id,
|
|
20062
|
-
granularity: bundle.granularity,
|
|
20063
20266
|
extensionIds: bundle.extensions.map((e) => e.id)
|
|
20064
20267
|
});
|
|
20065
20268
|
}
|
|
20066
20269
|
for (const p of plugins) {
|
|
20067
20270
|
out.push({
|
|
20068
20271
|
id: p.id,
|
|
20069
|
-
granularity: p.granularity ?? "bundle",
|
|
20070
20272
|
extensionIds: p.extensions?.map((e) => e.id) ?? []
|
|
20071
20273
|
});
|
|
20072
20274
|
}
|
|
20073
20275
|
return out;
|
|
20074
20276
|
}
|
|
20075
|
-
function resolveToggleTarget(id, catalogue,
|
|
20076
|
-
return id.includes("/") ? resolveQualifiedToggle(id, catalogue,
|
|
20277
|
+
function resolveToggleTarget(id, catalogue, ansi) {
|
|
20278
|
+
return id.includes("/") ? resolveQualifiedToggle(id, catalogue, ansi) : resolveBareToggle(id, catalogue);
|
|
20077
20279
|
}
|
|
20078
|
-
function resolveQualifiedToggle(id, catalogue,
|
|
20280
|
+
function resolveQualifiedToggle(id, catalogue, ansi) {
|
|
20079
20281
|
const errGlyph = ansi.red("\u2715");
|
|
20080
20282
|
const [bundleId, extId, ...rest] = id.split("/");
|
|
20081
20283
|
if (!bundleId || !extId || rest.length > 0) {
|
|
@@ -20097,17 +20299,6 @@ function resolveQualifiedToggle(id, catalogue, verb, ansi) {
|
|
|
20097
20299
|
})
|
|
20098
20300
|
};
|
|
20099
20301
|
}
|
|
20100
|
-
if (bundle.granularity === "bundle") {
|
|
20101
|
-
return {
|
|
20102
|
-
error: tx(PLUGINS_TEXTS.granularityBundleRejectsQualified, {
|
|
20103
|
-
glyph: errGlyph,
|
|
20104
|
-
bundleId: sanitizeForTerminal(bundleId),
|
|
20105
|
-
extId: sanitizeForTerminal(extId),
|
|
20106
|
-
verb,
|
|
20107
|
-
hint: ansi.dim(PLUGINS_TEXTS.granularityBundleRejectsQualifiedHint)
|
|
20108
|
-
})
|
|
20109
|
-
};
|
|
20110
|
-
}
|
|
20111
20302
|
if (!bundle.extensionIds.includes(extId)) {
|
|
20112
20303
|
return {
|
|
20113
20304
|
error: tx(PLUGINS_TEXTS.qualifiedIdNotFound, {
|
|
@@ -20119,31 +20310,27 @@ function resolveQualifiedToggle(id, catalogue, verb, ansi) {
|
|
|
20119
20310
|
})
|
|
20120
20311
|
};
|
|
20121
20312
|
}
|
|
20122
|
-
return {
|
|
20313
|
+
return {
|
|
20314
|
+
origin: "qualified",
|
|
20315
|
+
keys: [qualifiedExtensionId(bundleId, extId)]
|
|
20316
|
+
};
|
|
20123
20317
|
}
|
|
20124
|
-
function resolveBareToggle(id, catalogue
|
|
20125
|
-
const errGlyph = ansi.red("\u2715");
|
|
20318
|
+
function resolveBareToggle(id, catalogue) {
|
|
20126
20319
|
const bundle = catalogue.find((b) => b.id === id);
|
|
20127
20320
|
if (!bundle) {
|
|
20128
20321
|
return {
|
|
20129
20322
|
error: tx(PLUGINS_TEXTS.pluginNotFound, {
|
|
20130
|
-
glyph:
|
|
20323
|
+
glyph: "\u2715",
|
|
20131
20324
|
id: sanitizeForTerminal(id),
|
|
20132
|
-
hint:
|
|
20133
|
-
})
|
|
20134
|
-
};
|
|
20135
|
-
}
|
|
20136
|
-
if (bundle.granularity === "extension") {
|
|
20137
|
-
return {
|
|
20138
|
-
error: tx(PLUGINS_TEXTS.granularityExtensionRejectsBundleId, {
|
|
20139
|
-
glyph: errGlyph,
|
|
20140
|
-
bundleId: sanitizeForTerminal(id),
|
|
20141
|
-
verb,
|
|
20142
|
-
hint: ansi.dim(PLUGINS_TEXTS.granularityExtensionRejectsBundleIdHint)
|
|
20325
|
+
hint: PLUGINS_TEXTS.pluginNotFoundHint
|
|
20143
20326
|
})
|
|
20144
20327
|
};
|
|
20145
20328
|
}
|
|
20146
|
-
return {
|
|
20329
|
+
return {
|
|
20330
|
+
origin: "bare",
|
|
20331
|
+
bundleId: bundle.id,
|
|
20332
|
+
keys: bundle.extensionIds.map((extId) => qualifiedExtensionId(bundle.id, extId))
|
|
20333
|
+
};
|
|
20147
20334
|
}
|
|
20148
20335
|
|
|
20149
20336
|
// cli/commands/plugins/create.ts
|
|
@@ -20194,7 +20381,6 @@ var PluginsCreateCommand = class extends SmCommand {
|
|
|
20194
20381
|
version: "0.1.0",
|
|
20195
20382
|
specCompat: `^${specVersion}`,
|
|
20196
20383
|
catalogCompat: "^1.0.0",
|
|
20197
|
-
granularity: "bundle",
|
|
20198
20384
|
description: "Generated by `sm plugins create`. Edit to taste.",
|
|
20199
20385
|
settings: {
|
|
20200
20386
|
keywords: {
|
|
@@ -20310,7 +20496,7 @@ var VIEW_SLOTS_CATALOG = [
|
|
|
20310
20496
|
{ id: "card.subtitle.left", summary: "Single non-negative integer in the card subtitle row." },
|
|
20311
20497
|
{ id: "card.footer.left", summary: "Counter chip in the left footer of the card." },
|
|
20312
20498
|
{ id: "card.footer.right", summary: "Counter chip in the right footer of the card." },
|
|
20313
|
-
{ id: "graph.node.alert", summary:
|
|
20499
|
+
{ id: "graph.node.alert", summary: 'Reserved corner badge on the graph node, special-case signals only. No core analyzer emits here; routine "this node has a problem" findings belong in `card.footer.right`.' },
|
|
20314
20500
|
{ id: "inspector.header.badge.counter", summary: "Counter chip in the inspector header badge cluster." },
|
|
20315
20501
|
{ id: "inspector.header.badge.tag", summary: "Qualitative tag chip in the inspector header badge cluster." },
|
|
20316
20502
|
{ id: "inspector.body.panel.breakdown", summary: "Top-N labeled values rendered as a bar chart in the inspector body." },
|
|
@@ -21608,13 +21794,12 @@ var ScanCommand = class extends SmCommand {
|
|
|
21608
21794
|
const ansi = this.ansiFor("stdout");
|
|
21609
21795
|
const cwd = defaultRuntimeContext().cwd;
|
|
21610
21796
|
const hasErrors = exitCode2 === ExitCode.Issues;
|
|
21611
|
-
const
|
|
21612
|
-
const glyph = hasErrors ?
|
|
21797
|
+
const severityCounts = countBySeverity(result.issues);
|
|
21798
|
+
const glyph = hasErrors ? " " : ansi.green("\u2713");
|
|
21613
21799
|
const counts = formatScanCounts({
|
|
21614
21800
|
nodes: result.stats.nodesCount,
|
|
21615
21801
|
links: result.stats.linksCount,
|
|
21616
|
-
|
|
21617
|
-
hasErrors,
|
|
21802
|
+
severities: severityCounts,
|
|
21618
21803
|
ansi
|
|
21619
21804
|
});
|
|
21620
21805
|
const duration = ansi.dim(`in ${result.stats.durationMs}ms`);
|
|
@@ -21661,11 +21846,49 @@ var ScanCommand = class extends SmCommand {
|
|
|
21661
21846
|
return exitCode2;
|
|
21662
21847
|
}
|
|
21663
21848
|
};
|
|
21849
|
+
function countBySeverity(issues) {
|
|
21850
|
+
const buckets = {
|
|
21851
|
+
error: /* @__PURE__ */ new Set(),
|
|
21852
|
+
warn: /* @__PURE__ */ new Set(),
|
|
21853
|
+
info: /* @__PURE__ */ new Set()
|
|
21854
|
+
};
|
|
21855
|
+
for (const i of issues) {
|
|
21856
|
+
const tier = i.severity;
|
|
21857
|
+
const bucket = buckets[tier];
|
|
21858
|
+
if (!bucket) continue;
|
|
21859
|
+
fillSeverityBucket(bucket, i.nodeIds);
|
|
21860
|
+
}
|
|
21861
|
+
return { errors: buckets.error.size, warns: buckets.warn.size, info: buckets.info.size };
|
|
21862
|
+
}
|
|
21863
|
+
function fillSeverityBucket(bucket, nodeIds) {
|
|
21864
|
+
const ids = nodeIds ?? [];
|
|
21865
|
+
if (ids.length === 0) {
|
|
21866
|
+
bucket.add("");
|
|
21867
|
+
return;
|
|
21868
|
+
}
|
|
21869
|
+
for (const id of ids) bucket.add(id);
|
|
21870
|
+
}
|
|
21664
21871
|
function formatScanCounts(opts) {
|
|
21665
|
-
const { nodes, links,
|
|
21666
|
-
const
|
|
21667
|
-
|
|
21668
|
-
|
|
21872
|
+
const { nodes, links, severities, ansi } = opts;
|
|
21873
|
+
const parts = [
|
|
21874
|
+
`${nodes} ${plural(nodes, "node")}`,
|
|
21875
|
+
`${links} ${plural(links, "link")}`
|
|
21876
|
+
];
|
|
21877
|
+
const total = severities.errors + severities.warns + severities.info;
|
|
21878
|
+
if (total === 0) {
|
|
21879
|
+
parts.push(ansi.dim("0 issues"));
|
|
21880
|
+
} else {
|
|
21881
|
+
if (severities.errors > 0) {
|
|
21882
|
+
parts.push(ansi.red(`${severities.errors} ${plural(severities.errors, "error")}`));
|
|
21883
|
+
}
|
|
21884
|
+
if (severities.warns > 0) {
|
|
21885
|
+
parts.push(ansi.yellow(`${severities.warns} ${plural(severities.warns, "warning")}`));
|
|
21886
|
+
}
|
|
21887
|
+
if (severities.info > 0) {
|
|
21888
|
+
parts.push(ansi.dim(`${severities.info} info`));
|
|
21889
|
+
}
|
|
21890
|
+
}
|
|
21891
|
+
return parts.join(" \xB7 ");
|
|
21669
21892
|
}
|
|
21670
21893
|
function plural(count, word) {
|
|
21671
21894
|
return count === 1 ? word : `${word}s`;
|
|
@@ -21919,6 +22142,18 @@ import { spawn as spawn2 } from "child_process";
|
|
|
21919
22142
|
import { existsSync as existsSync28 } from "fs";
|
|
21920
22143
|
import { Command as Command33, Option as Option31 } from "clipanion";
|
|
21921
22144
|
|
|
22145
|
+
// kernel/util/dev-mode.ts
|
|
22146
|
+
import { sep as sep6 } from "path";
|
|
22147
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
22148
|
+
var SELF_PATH = fileURLToPath5(import.meta.url);
|
|
22149
|
+
var IS_DEV_BUILD = isDevBuildFromPath(SELF_PATH, sep6);
|
|
22150
|
+
function isDevBuildFromPath(filePath, separator = sep6) {
|
|
22151
|
+
return !filePath.includes(`${separator}node_modules${separator}`);
|
|
22152
|
+
}
|
|
22153
|
+
function isDevBuild() {
|
|
22154
|
+
return IS_DEV_BUILD;
|
|
22155
|
+
}
|
|
22156
|
+
|
|
21922
22157
|
// cli/util/browser-launch.ts
|
|
21923
22158
|
function validateBrowserUrl(url) {
|
|
21924
22159
|
if (typeof url !== "string" || url.length === 0) return false;
|
|
@@ -22136,10 +22371,10 @@ var SERVER_TEXTS = {
|
|
|
22136
22371
|
pluginsBodyNotJson: "Request body must be valid JSON.",
|
|
22137
22372
|
pluginsBodyNotObject: "Request body must be a JSON object.",
|
|
22138
22373
|
pluginsEnabledRequired: "`enabled` is required and must be a boolean.",
|
|
22139
|
-
// 400,
|
|
22140
|
-
//
|
|
22141
|
-
|
|
22142
|
-
|
|
22374
|
+
// 400, cascade route rejects qualified ids: the bare-id PATCH is the
|
|
22375
|
+
// bundle macro endpoint. Anything containing `/` needs the dedicated
|
|
22376
|
+
// per-extension route below.
|
|
22377
|
+
pluginsCascadeRouteQualifiedRejected: 'Plugin id "{{id}}" contains "/"; toggle individual extensions via PATCH /api/plugins/<bundle>/extensions/<extensionId>.',
|
|
22143
22378
|
// 404, unknown plugin / extension.
|
|
22144
22379
|
pluginsUnknown: 'No plugin with id "{{id}}".',
|
|
22145
22380
|
pluginsExtensionUnknown: 'Plugin "{{bundleId}}" has no extension named "{{extensionId}}".',
|
|
@@ -22632,6 +22867,7 @@ function contentTypeFor(format) {
|
|
|
22632
22867
|
import { existsSync as existsSync23 } from "fs";
|
|
22633
22868
|
var FALLBACK_SCHEMA_VERSION = "1";
|
|
22634
22869
|
function buildHealth(deps) {
|
|
22870
|
+
const dev = isDevBuild();
|
|
22635
22871
|
return {
|
|
22636
22872
|
ok: true,
|
|
22637
22873
|
schemaVersion: FALLBACK_SCHEMA_VERSION,
|
|
@@ -22639,7 +22875,10 @@ function buildHealth(deps) {
|
|
|
22639
22875
|
implVersion: VERSION,
|
|
22640
22876
|
db: existsSync23(deps.dbPath) ? "present" : "missing",
|
|
22641
22877
|
cwd: deps.cwd,
|
|
22642
|
-
dbPath: deps.dbPath
|
|
22878
|
+
dbPath: deps.dbPath,
|
|
22879
|
+
// Only emit when truthy so a published install keeps the wire
|
|
22880
|
+
// shape lean and consumers branch on presence alone.
|
|
22881
|
+
...dev ? { dev: true } : {}
|
|
22643
22882
|
};
|
|
22644
22883
|
}
|
|
22645
22884
|
var cachedSpecVersion = null;
|
|
@@ -22764,13 +23003,13 @@ import { HTTPException as HTTPException6 } from "hono/http-exception";
|
|
|
22764
23003
|
// server/node-body.ts
|
|
22765
23004
|
import { constants as fsConstants2 } from "fs";
|
|
22766
23005
|
import { open } from "fs/promises";
|
|
22767
|
-
import { isAbsolute as isAbsolute10, resolve as resolvePath2, relative as relativePath, sep as
|
|
23006
|
+
import { isAbsolute as isAbsolute10, resolve as resolvePath2, relative as relativePath, sep as sep7 } from "path";
|
|
22768
23007
|
async function readNodeBody(cwd, relPath) {
|
|
22769
23008
|
if (isAbsolute10(relPath)) return null;
|
|
22770
23009
|
const absRoot = resolvePath2(cwd);
|
|
22771
23010
|
const absFile = resolvePath2(absRoot, relPath);
|
|
22772
23011
|
const rel = relativePath(absRoot, absFile);
|
|
22773
|
-
if (rel.startsWith("..") || rel.startsWith(
|
|
23012
|
+
if (rel.startsWith("..") || rel.startsWith(sep7) || rel.length === 0) {
|
|
22774
23013
|
return null;
|
|
22775
23014
|
}
|
|
22776
23015
|
let raw;
|
|
@@ -23142,7 +23381,7 @@ function registerPluginsRoute(app, deps) {
|
|
|
23142
23381
|
const id = c.req.param("id");
|
|
23143
23382
|
if (id.includes("/")) {
|
|
23144
23383
|
throw new HTTPException8(400, {
|
|
23145
|
-
message: tx(SERVER_TEXTS.
|
|
23384
|
+
message: tx(SERVER_TEXTS.pluginsCascadeRouteQualifiedRejected, { id })
|
|
23146
23385
|
});
|
|
23147
23386
|
}
|
|
23148
23387
|
const handle = findHandle(id, deps);
|
|
@@ -23151,18 +23390,15 @@ function registerPluginsRoute(app, deps) {
|
|
|
23151
23390
|
message: tx(SERVER_TEXTS.pluginsUnknown, { id })
|
|
23152
23391
|
});
|
|
23153
23392
|
}
|
|
23154
|
-
if (granularityOf(handle) !== "bundle") {
|
|
23155
|
-
throw new HTTPException8(400, {
|
|
23156
|
-
message: tx(SERVER_TEXTS.pluginsGranularityExtensionExpected, { id })
|
|
23157
|
-
});
|
|
23158
|
-
}
|
|
23159
23393
|
if (isPluginLocked(id)) {
|
|
23160
23394
|
throw new HTTPException8(403, {
|
|
23161
23395
|
message: tx(SERVER_TEXTS.pluginsLocked, { id })
|
|
23162
23396
|
});
|
|
23163
23397
|
}
|
|
23164
23398
|
const body = await parsePatchBody(c.req.raw);
|
|
23165
|
-
|
|
23399
|
+
const childIds = bundleExtensionIds(handle).map((extId) => qualifiedExtensionId(id, extId));
|
|
23400
|
+
const writable = childIds.filter((q) => !isPluginLocked(q));
|
|
23401
|
+
return await persistManyAndProject(c, deps, writable, body.enabled);
|
|
23166
23402
|
});
|
|
23167
23403
|
app.patch("/api/plugins/:bundleId/extensions/:extensionId", async (c) => {
|
|
23168
23404
|
const bundleId = c.req.param("bundleId");
|
|
@@ -23210,8 +23446,7 @@ function listItems(deps, resolveEnabled) {
|
|
|
23210
23446
|
];
|
|
23211
23447
|
}
|
|
23212
23448
|
function buildBuiltInItems(resolveEnabled) {
|
|
23213
|
-
return builtInBundles.map((bundle) => {
|
|
23214
|
-
const bundleEnabled = resolveEnabled(bundle.id);
|
|
23449
|
+
return sortBundlesForPresentation(builtInBundles).map((bundle) => {
|
|
23215
23450
|
const bundleLocked = isPluginLocked(bundle.id);
|
|
23216
23451
|
const extensions = bundle.extensions.map((ext) => {
|
|
23217
23452
|
const qualified = qualifiedExtensionId(bundle.id, ext.id);
|
|
@@ -23225,6 +23460,7 @@ function buildBuiltInItems(resolveEnabled) {
|
|
|
23225
23460
|
...extLocked ? { locked: true } : {}
|
|
23226
23461
|
};
|
|
23227
23462
|
});
|
|
23463
|
+
const bundleEnabled = extensions.some((e) => e.enabled);
|
|
23228
23464
|
return {
|
|
23229
23465
|
id: bundle.id,
|
|
23230
23466
|
version: firstVersion(bundle.extensions),
|
|
@@ -23232,7 +23468,6 @@ function buildBuiltInItems(resolveEnabled) {
|
|
|
23232
23468
|
status: bundleEnabled ? "enabled" : "disabled",
|
|
23233
23469
|
reason: null,
|
|
23234
23470
|
source: "built-in",
|
|
23235
|
-
granularity: bundle.granularity,
|
|
23236
23471
|
description: bundle.description,
|
|
23237
23472
|
...extensions.length > 0 ? { extensions } : {},
|
|
23238
23473
|
...bundleLocked ? { locked: true } : {}
|
|
@@ -23243,9 +23478,8 @@ function buildDiscoveredItems(discovered, deps, resolveEnabled) {
|
|
|
23243
23478
|
return discovered.map((plugin) => buildDiscoveredItem(plugin, deps, resolveEnabled));
|
|
23244
23479
|
}
|
|
23245
23480
|
function buildDiscoveredItem(plugin, deps, resolveEnabled) {
|
|
23246
|
-
const granularity = plugin.granularity ?? "bundle";
|
|
23247
23481
|
const bundleLocked = isPluginLocked(plugin.id);
|
|
23248
|
-
const extensions = projectExtensionRows(plugin,
|
|
23482
|
+
const extensions = projectExtensionRows(plugin, resolveEnabled, bundleLocked);
|
|
23249
23483
|
const optional = optionalDiscoveredFields(plugin, extensions);
|
|
23250
23484
|
return {
|
|
23251
23485
|
id: plugin.id,
|
|
@@ -23254,7 +23488,6 @@ function buildDiscoveredItem(plugin, deps, resolveEnabled) {
|
|
|
23254
23488
|
status: projectStatus(plugin, resolveEnabled),
|
|
23255
23489
|
reason: plugin.reason ?? null,
|
|
23256
23490
|
source: classifyPluginSource(plugin.path, deps),
|
|
23257
|
-
granularity,
|
|
23258
23491
|
...optional,
|
|
23259
23492
|
...bundleLocked ? { locked: true } : {},
|
|
23260
23493
|
...plugin.status === "disabled" ? { startsAsDisabled: true } : {}
|
|
@@ -23267,7 +23500,7 @@ function optionalDiscoveredFields(plugin, extensions) {
|
|
|
23267
23500
|
if (extensions) out.extensions = extensions;
|
|
23268
23501
|
return out;
|
|
23269
23502
|
}
|
|
23270
|
-
function projectExtensionRows(plugin,
|
|
23503
|
+
function projectExtensionRows(plugin, resolveEnabled, bundleLocked) {
|
|
23271
23504
|
if (!plugin.extensions || plugin.extensions.length === 0) return void 0;
|
|
23272
23505
|
return plugin.extensions.map((ext) => {
|
|
23273
23506
|
const description = readInstanceDescription(ext.instance);
|
|
@@ -23316,6 +23549,18 @@ async function persistAndProject(c, deps, configKey, enabled) {
|
|
|
23316
23549
|
);
|
|
23317
23550
|
return projectListResponse(c, deps, overrides);
|
|
23318
23551
|
}
|
|
23552
|
+
async function persistManyAndProject(c, deps, keys, enabled) {
|
|
23553
|
+
const overrides = await tryWithSqlite(
|
|
23554
|
+
{ databasePath: deps.options.dbPath, autoBackup: false },
|
|
23555
|
+
async (adapter) => {
|
|
23556
|
+
for (const key of keys) {
|
|
23557
|
+
await applyChangeToAdapter(adapter, key, enabled);
|
|
23558
|
+
}
|
|
23559
|
+
return await adapter.pluginConfig.loadOverrideMap();
|
|
23560
|
+
}
|
|
23561
|
+
);
|
|
23562
|
+
return projectListResponse(c, deps, overrides);
|
|
23563
|
+
}
|
|
23319
23564
|
async function applyChangeToAdapter(adapter, configKey, enabled) {
|
|
23320
23565
|
await adapter.pluginConfig.set(configKey, enabled);
|
|
23321
23566
|
if (enabled) return;
|
|
@@ -23359,13 +23604,6 @@ function validateBulkChange(change, deps) {
|
|
|
23359
23604
|
message: tx(SERVER_TEXTS.pluginsUnknown, { id: change.id })
|
|
23360
23605
|
};
|
|
23361
23606
|
}
|
|
23362
|
-
if (granularityOf(handle2) !== "bundle") {
|
|
23363
|
-
return {
|
|
23364
|
-
status: 400,
|
|
23365
|
-
code: "bad-query",
|
|
23366
|
-
message: tx(SERVER_TEXTS.pluginsGranularityExtensionExpected, { id: change.id })
|
|
23367
|
-
};
|
|
23368
|
-
}
|
|
23369
23607
|
if (isPluginLocked(change.id)) {
|
|
23370
23608
|
return {
|
|
23371
23609
|
status: 403,
|
|
@@ -23406,13 +23644,22 @@ async function persistBulkAndProject(c, deps, changes) {
|
|
|
23406
23644
|
{ databasePath: deps.options.dbPath, autoBackup: false },
|
|
23407
23645
|
async (adapter) => {
|
|
23408
23646
|
for (const change of changes) {
|
|
23409
|
-
|
|
23647
|
+
const writeKeys = expandBulkChangeKeys(change, deps);
|
|
23648
|
+
for (const key of writeKeys) {
|
|
23649
|
+
await applyChangeToAdapter(adapter, key, change.enabled);
|
|
23650
|
+
}
|
|
23410
23651
|
}
|
|
23411
23652
|
return await adapter.pluginConfig.loadOverrideMap();
|
|
23412
23653
|
}
|
|
23413
23654
|
);
|
|
23414
23655
|
return projectListResponse(c, deps, overrides);
|
|
23415
23656
|
}
|
|
23657
|
+
function expandBulkChangeKeys(change, deps) {
|
|
23658
|
+
if (change.id.includes("/")) return [change.id];
|
|
23659
|
+
const handle = findHandle(change.id, deps);
|
|
23660
|
+
if (!handle) return [];
|
|
23661
|
+
return bundleExtensionIds(handle).map((extId) => qualifiedExtensionId(change.id, extId)).filter((q) => !isPluginLocked(q));
|
|
23662
|
+
}
|
|
23416
23663
|
async function buildFreshResolver2(deps) {
|
|
23417
23664
|
return buildFreshResolver({
|
|
23418
23665
|
databasePath: deps.options.dbPath,
|
|
@@ -23430,8 +23677,11 @@ function findHandle(id, deps) {
|
|
|
23430
23677
|
if (discovered) return { kind: "discovered", plugin: discovered };
|
|
23431
23678
|
return null;
|
|
23432
23679
|
}
|
|
23433
|
-
function
|
|
23434
|
-
|
|
23680
|
+
function bundleExtensionIds(handle) {
|
|
23681
|
+
if (handle.kind === "built-in") {
|
|
23682
|
+
return handle.bundle.extensions.map((e) => e.id);
|
|
23683
|
+
}
|
|
23684
|
+
return (handle.plugin.extensions ?? []).map((e) => e.id);
|
|
23435
23685
|
}
|
|
23436
23686
|
function hasExtension(handle, extensionId) {
|
|
23437
23687
|
if (handle.kind === "built-in") {
|
|
@@ -24365,12 +24615,12 @@ async function loadNode(deps, nodePath) {
|
|
|
24365
24615
|
return node;
|
|
24366
24616
|
}
|
|
24367
24617
|
function invokeBump2(node, absPath, body) {
|
|
24368
|
-
if (!
|
|
24618
|
+
if (!nodeBumpAction.invoke) {
|
|
24369
24619
|
throw new HTTPException14(500, { message: SERVER_TEXTS.sidecarBumpInvokeMissing });
|
|
24370
24620
|
}
|
|
24371
24621
|
const input = {};
|
|
24372
24622
|
if (body.force === true) input.force = true;
|
|
24373
|
-
return
|
|
24623
|
+
return nodeBumpAction.invoke(input, {
|
|
24374
24624
|
node,
|
|
24375
24625
|
nodeAbsolutePath: absPath,
|
|
24376
24626
|
invoker: "ui",
|
|
@@ -25041,7 +25291,7 @@ function validateNoUi(noUi, uiDist) {
|
|
|
25041
25291
|
// server/paths.ts
|
|
25042
25292
|
import { existsSync as existsSync27, statSync as statSync10 } from "fs";
|
|
25043
25293
|
import { dirname as dirname18, isAbsolute as isAbsolute11, join as join20, resolve as resolve37 } from "path";
|
|
25044
|
-
import { fileURLToPath as
|
|
25294
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
25045
25295
|
var DEFAULT_UI_REL = join20("ui", "dist", "ui", "browser");
|
|
25046
25296
|
var PACKAGE_UI_REL = "ui";
|
|
25047
25297
|
var INDEX_HTML2 = "index.html";
|
|
@@ -25065,7 +25315,7 @@ function isUiBundleDir(path) {
|
|
|
25065
25315
|
function resolvePackageBundledUi() {
|
|
25066
25316
|
let here;
|
|
25067
25317
|
try {
|
|
25068
|
-
here = dirname18(
|
|
25318
|
+
here = dirname18(fileURLToPath6(import.meta.url));
|
|
25069
25319
|
} catch {
|
|
25070
25320
|
return null;
|
|
25071
25321
|
}
|
|
@@ -25357,7 +25607,9 @@ var ESC2 = {
|
|
|
25357
25607
|
/** 256-color violet (xterm 141). */
|
|
25358
25608
|
violet: "\x1B[38;5;141m",
|
|
25359
25609
|
/** 256-color green (xterm 42). */
|
|
25360
|
-
green: "\x1B[38;5;42m"
|
|
25610
|
+
green: "\x1B[38;5;42m",
|
|
25611
|
+
/** 256-color yellow (xterm 214), matches `cli/util/ansi.ts:yellow`. */
|
|
25612
|
+
yellow: "\x1B[38;5;214m"
|
|
25361
25613
|
};
|
|
25362
25614
|
var LOGO_LINES = [
|
|
25363
25615
|
" ____ _ _ _ _ __ __ ",
|
|
@@ -25377,7 +25629,8 @@ function renderBanner(input) {
|
|
|
25377
25629
|
host: input.host,
|
|
25378
25630
|
port: input.port,
|
|
25379
25631
|
dbPath: input.dbPath,
|
|
25380
|
-
openBrowser: input.openBrowser
|
|
25632
|
+
openBrowser: input.openBrowser,
|
|
25633
|
+
dev: input.dev === true
|
|
25381
25634
|
});
|
|
25382
25635
|
}
|
|
25383
25636
|
return renderFiglet({
|
|
@@ -25387,7 +25640,8 @@ function renderBanner(input) {
|
|
|
25387
25640
|
pathDisplay: formatCwdPath(input.cwd),
|
|
25388
25641
|
browserLine,
|
|
25389
25642
|
colorEnabled: input.colorEnabled,
|
|
25390
|
-
referencePaths: input.referencePaths ?? []
|
|
25643
|
+
referencePaths: input.referencePaths ?? [],
|
|
25644
|
+
dev: input.dev === true
|
|
25391
25645
|
});
|
|
25392
25646
|
}
|
|
25393
25647
|
function resolveColorEnabled(opts) {
|
|
@@ -25402,8 +25656,9 @@ function renderFlat(input) {
|
|
|
25402
25656
|
const safeHost = sanitizeForTerminal(input.host);
|
|
25403
25657
|
const safeDb = sanitizeForTerminal(input.dbPath);
|
|
25404
25658
|
const url = `http://${safeHost}:${input.port}`;
|
|
25659
|
+
const devSuffix = input.dev ? " [dev]" : "";
|
|
25405
25660
|
const linesOut = [];
|
|
25406
|
-
linesOut.push(`sm serve: listening on ${url} (db=${safeDb})`);
|
|
25661
|
+
linesOut.push(`sm serve${devSuffix}: listening on ${url} (db=${safeDb})`);
|
|
25407
25662
|
if (input.openBrowser) {
|
|
25408
25663
|
linesOut.push(`sm serve: opening ${url}/ in your browser. Press Ctrl+C to stop.`);
|
|
25409
25664
|
} else {
|
|
@@ -25430,12 +25685,16 @@ function renderFiglet(input) {
|
|
|
25430
25685
|
greenUnderline,
|
|
25431
25686
|
greenUnderlineClose,
|
|
25432
25687
|
violetOpen,
|
|
25433
|
-
violetClose
|
|
25688
|
+
violetClose,
|
|
25689
|
+
yellowOpen,
|
|
25690
|
+
yellowClose
|
|
25434
25691
|
} = resolveAnsi(input.colorEnabled);
|
|
25435
25692
|
const logoLines = LOGO_LINES.map((line) => `${violetOpen}${line}${violetClose}`);
|
|
25436
25693
|
const versionText = `v${input.version}`;
|
|
25437
|
-
const
|
|
25438
|
-
const
|
|
25694
|
+
const devText = "[dev]";
|
|
25695
|
+
const versionWidth = input.dev ? devText.length : versionText.length;
|
|
25696
|
+
const versionPad = Math.max(0, LOGO_WIDTH - versionWidth);
|
|
25697
|
+
const versionLine = input.dev ? `${" ".repeat(versionPad)}${yellowOpen}${devText}${yellowClose}` : `${" ".repeat(versionPad)}${dimOpen}${versionText}${dimClose}`;
|
|
25439
25698
|
const lines = [];
|
|
25440
25699
|
lines.push(...logoLines);
|
|
25441
25700
|
lines.push("");
|
|
@@ -25467,7 +25726,9 @@ var EMPTY_ANSI = {
|
|
|
25467
25726
|
greenUnderline: "",
|
|
25468
25727
|
greenUnderlineClose: "",
|
|
25469
25728
|
violetOpen: "",
|
|
25470
|
-
violetClose: ""
|
|
25729
|
+
violetClose: "",
|
|
25730
|
+
yellowOpen: "",
|
|
25731
|
+
yellowClose: ""
|
|
25471
25732
|
};
|
|
25472
25733
|
var ENABLED_ANSI = {
|
|
25473
25734
|
dimOpen: ESC2.dim,
|
|
@@ -25475,7 +25736,9 @@ var ENABLED_ANSI = {
|
|
|
25475
25736
|
greenUnderline: `${ESC2.green}${ESC2.underline}`,
|
|
25476
25737
|
greenUnderlineClose: ESC2.reset,
|
|
25477
25738
|
violetOpen: ESC2.violet,
|
|
25478
|
-
violetClose: ESC2.reset
|
|
25739
|
+
violetClose: ESC2.reset,
|
|
25740
|
+
yellowOpen: ESC2.yellow,
|
|
25741
|
+
yellowClose: ESC2.reset
|
|
25479
25742
|
};
|
|
25480
25743
|
function resolveAnsi(colorEnabled) {
|
|
25481
25744
|
return colorEnabled ? ENABLED_ANSI : EMPTY_ANSI;
|
|
@@ -25696,7 +25959,8 @@ var ServeCommand = class extends SmCommand {
|
|
|
25696
25959
|
openBrowser: validation.options.open,
|
|
25697
25960
|
isTTY,
|
|
25698
25961
|
colorEnabled,
|
|
25699
|
-
referencePaths
|
|
25962
|
+
referencePaths,
|
|
25963
|
+
dev: isDevBuild()
|
|
25700
25964
|
})
|
|
25701
25965
|
);
|
|
25702
25966
|
if (validation.options.open) {
|
|
@@ -26830,7 +27094,7 @@ var STUB_COMMANDS = [
|
|
|
26830
27094
|
// cli/commands/tutorial.ts
|
|
26831
27095
|
import { cpSync as cpSync2, existsSync as existsSync29, mkdirSync as mkdirSync6, rmSync as rmSync2, statSync as statSync11 } from "fs";
|
|
26832
27096
|
import { dirname as dirname19, join as join21, resolve as resolve39 } from "path";
|
|
26833
|
-
import { fileURLToPath as
|
|
27097
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
26834
27098
|
import { Command as Command37, Option as Option35 } from "clipanion";
|
|
26835
27099
|
|
|
26836
27100
|
// cli/i18n/tutorial.texts.ts
|
|
@@ -27000,7 +27264,7 @@ function resolveSkillSourceDir(variant) {
|
|
|
27000
27264
|
const cached = cachedSourceDirs.get(variant);
|
|
27001
27265
|
if (cached !== void 0) return cached;
|
|
27002
27266
|
const spec = VARIANT_SPECS[variant];
|
|
27003
|
-
const here = dirname19(
|
|
27267
|
+
const here = dirname19(fileURLToPath7(import.meta.url));
|
|
27004
27268
|
const candidates = [
|
|
27005
27269
|
// dev: src/cli/commands/ → repo-root .claude/skills/<slug>/
|
|
27006
27270
|
resolve39(here, "../../..", spec.sourceDir),
|
|
@@ -27048,6 +27312,7 @@ var VersionCommand = class extends SmCommand {
|
|
|
27048
27312
|
const kernelVersion = VERSION;
|
|
27049
27313
|
const specVersion = await resolveSpecVersion3();
|
|
27050
27314
|
const dbSchema = await resolveDbSchemaVersion();
|
|
27315
|
+
const dev = isDevBuild();
|
|
27051
27316
|
if (this.json) {
|
|
27052
27317
|
const payload = {
|
|
27053
27318
|
sm: VERSION,
|
|
@@ -27055,17 +27320,19 @@ var VersionCommand = class extends SmCommand {
|
|
|
27055
27320
|
spec: specVersion,
|
|
27056
27321
|
dbSchema
|
|
27057
27322
|
};
|
|
27323
|
+
if (dev) payload["dev"] = true;
|
|
27058
27324
|
this.printer.data(JSON.stringify(payload) + "\n");
|
|
27059
27325
|
return ExitCode.Ok;
|
|
27060
27326
|
}
|
|
27327
|
+
const ansi = this.ansiFor("stdout");
|
|
27328
|
+
const smValue = dev ? `${VERSION} ${ansi.yellow("[dev]")}` : VERSION;
|
|
27061
27329
|
const lines = [
|
|
27062
|
-
["sm",
|
|
27330
|
+
["sm", smValue],
|
|
27063
27331
|
["kernel", kernelVersion],
|
|
27064
27332
|
["spec", specVersion],
|
|
27065
27333
|
["runtime", runtime],
|
|
27066
27334
|
["db-schema", dbSchema]
|
|
27067
27335
|
];
|
|
27068
|
-
const ansi = this.ansiFor("stdout");
|
|
27069
27336
|
const pad = Math.max(...lines.map(([k]) => k.length));
|
|
27070
27337
|
for (const [k, v] of lines) {
|
|
27071
27338
|
this.printer.data(
|