@skill-map/cli 0.25.0 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -748,19 +748,19 @@ var annotationsExtractor = {
748
748
  pluginId: "core",
749
749
  kind: "extractor",
750
750
  version: "1.0.0",
751
- description: "Turns the `supersedes`, `requires`, `related`, `conflictsWith`, and `supersededBy` entries you write in a node's `.sm` sidecar into the arrows (edges) shown between nodes in the graph.",
751
+ 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.",
752
752
  stability: "stable",
753
- emitsLinkKinds: ["supersedes", "references"],
753
+ emitsLinkKinds: ["supersedes"],
754
754
  defaultConfidence: "high",
755
755
  scope: "frontmatter",
756
756
  extract(ctx) {
757
757
  const sourcePath = ctx.node.path;
758
758
  const seen = /* @__PURE__ */ new Set();
759
- function emit(source, target, kind) {
760
- const key = `${source} ${target} ${kind}`;
759
+ function emit(source, target) {
760
+ const key = `${source} ${target}`;
761
761
  if (seen.has(key)) return;
762
762
  seen.add(key);
763
- ctx.emitLink(link(source, target, kind));
763
+ ctx.emitLink(link(source, target));
764
764
  }
765
765
  const ann = pickAnnotations(ctx.node);
766
766
  if (ann) processBlock(ann, sourcePath, emit);
@@ -768,20 +768,11 @@ var annotationsExtractor = {
768
768
  };
769
769
  function processBlock(block, sourcePath, emit) {
770
770
  for (const target of stringArray(block["supersedes"])) {
771
- emit(sourcePath, target, "supersedes");
771
+ emit(sourcePath, target);
772
772
  }
773
773
  const supersededBy = block["supersededBy"];
774
774
  if (typeof supersededBy === "string" && supersededBy.length > 0) {
775
- emit(supersededBy, sourcePath, "supersedes");
776
- }
777
- for (const target of stringArray(block["requires"])) {
778
- emit(sourcePath, target, "references");
779
- }
780
- for (const target of stringArray(block["related"])) {
781
- emit(sourcePath, target, "references");
782
- }
783
- for (const target of stringArray(block["conflictsWith"])) {
784
- emit(sourcePath, target, "references");
775
+ emit(supersededBy, sourcePath);
785
776
  }
786
777
  }
787
778
  function pickAnnotations(node) {
@@ -797,11 +788,11 @@ function stringArray(value) {
797
788
  if (!Array.isArray(value)) return [];
798
789
  return value.filter((v) => typeof v === "string" && v.length > 0);
799
790
  }
800
- function link(source, target, kind) {
791
+ function link(source, target) {
801
792
  return {
802
793
  source,
803
794
  target,
804
- kind,
795
+ kind: "supersedes",
805
796
  confidence: "high",
806
797
  sources: [ID]
807
798
  };
@@ -2318,7 +2309,13 @@ var VALIDATE_ALL_TEXTS = {
2318
2309
  /** `Node <path> failed schema validation: <errors>` */
2319
2310
  nodeFailure: "Node {{path}} failed schema validation: {{errors}}",
2320
2311
  /** `Link <source> → <target> failed schema validation: <errors>` */
2321
- linkFailure: "Link {{source}} \u2192 {{target}} failed schema validation: {{errors}}"
2312
+ linkFailure: "Link {{source}} \u2192 {{target}} failed schema validation: {{errors}}",
2313
+ /** `Node <path> is missing required frontmatter fields: <missing>` */
2314
+ frontmatterBaseFailure: "Node {{path}} is missing required frontmatter fields: {{missing}}.",
2315
+ /** Singular tooltip on the alert / chip when a node has exactly one validation failure. */
2316
+ alertTooltipSingle: "Frontmatter or schema validation failed.",
2317
+ /** Plural tooltip; `{{count}}` capped at 99 in the chip badge but the tooltip text shows the raw count. */
2318
+ alertTooltipMany: "{{count}} schema validation issues on this node."
2322
2319
  };
2323
2320
 
2324
2321
  // built-in-plugins/analyzers/validate-all/index.ts
@@ -2331,15 +2328,56 @@ var validateAllAnalyzer = {
2331
2328
  description: "Detects and flags nodes or links violating the project schemas.",
2332
2329
  stability: "stable",
2333
2330
  mode: "deterministic",
2331
+ viewContributions: {
2332
+ // Corner badge on the graph card; surfaces when the node body /
2333
+ // frontmatter fails schema validation (parse error, missing
2334
+ // `name`/`description`, malformed YAML, etc.). Same visual
2335
+ // chassis as `core/broken-ref`, danger severity.
2336
+ alert: {
2337
+ slot: "graph.node.alert",
2338
+ icon: "fa-solid fa-triangle-exclamation",
2339
+ emitWhenEmpty: false
2340
+ },
2341
+ // Footer chip that mirrors the corner alert with the actual
2342
+ // count so the operator can scan the cards and prioritise.
2343
+ // Outlined (vs the filled corner alert) per the broken-ref
2344
+ // pattern: two beats of the same signal.
2345
+ chip: {
2346
+ slot: "card.footer.right",
2347
+ icon: "fa-regular fa-triangle-exclamation",
2348
+ emitWhenEmpty: false,
2349
+ priority: 35
2350
+ }
2351
+ },
2334
2352
  evaluate(ctx) {
2335
2353
  const validators = loadSchemaValidators();
2336
2354
  const findings = [];
2355
+ const perNode = /* @__PURE__ */ new Map();
2337
2356
  for (const node of ctx.nodes) {
2357
+ const before = findings.length;
2338
2358
  collectNodeFindings(validators, node, findings);
2359
+ collectFrontmatterBaseFindings(node, findings);
2360
+ if (findings.length > before) {
2361
+ perNode.set(node.path, (perNode.get(node.path) ?? 0) + (findings.length - before));
2362
+ }
2339
2363
  }
2340
2364
  for (const link2 of ctx.links) {
2341
2365
  collectLinkFindings(validators, link2, findings);
2342
2366
  }
2367
+ for (const [nodePath, count] of perNode) {
2368
+ const tooltip = count === 1 ? VALIDATE_ALL_TEXTS.alertTooltipSingle : tx(VALIDATE_ALL_TEXTS.alertTooltipMany, { count });
2369
+ const capped = Math.min(count, 99);
2370
+ ctx.emitContribution(nodePath, "alert", {
2371
+ icon: "fa-solid fa-triangle-exclamation",
2372
+ severity: "danger",
2373
+ tooltip
2374
+ });
2375
+ ctx.emitContribution(nodePath, "chip", {
2376
+ value: capped,
2377
+ severity: "danger",
2378
+ tooltip
2379
+ });
2380
+ }
2343
2381
  return findings;
2344
2382
  }
2345
2383
  };
@@ -2357,6 +2395,33 @@ function collectNodeFindings(v, node, out) {
2357
2395
  data: { target: "node", path: node.path }
2358
2396
  });
2359
2397
  }
2398
+ function collectFrontmatterBaseFindings(node, out) {
2399
+ if (node.provider === "markdown") return;
2400
+ if (node.bytes.frontmatter === 0) return;
2401
+ const fm = node.frontmatter ?? {};
2402
+ const missing = [];
2403
+ if (isMissingStringField(fm, "name")) missing.push("name");
2404
+ if (isMissingStringField(fm, "description")) missing.push("description");
2405
+ if (missing.length === 0) return;
2406
+ out.push({
2407
+ analyzerId: ID19,
2408
+ // `warn` (not `error`) so the default `sm scan` exit code stays
2409
+ // 0 even when nodes are missing frontmatter base fields. Strict
2410
+ // mode (`sm scan --strict`) still escalates to exit 1. Matches
2411
+ // the `frontmatter-invalid` severity policy of the orchestrator.
2412
+ severity: "warn",
2413
+ nodeIds: [node.path],
2414
+ message: tx(VALIDATE_ALL_TEXTS.frontmatterBaseFailure, {
2415
+ path: node.path,
2416
+ missing: missing.join(", ")
2417
+ }),
2418
+ data: { target: "frontmatter", path: node.path, missing }
2419
+ });
2420
+ }
2421
+ function isMissingStringField(fm, field) {
2422
+ const v = fm[field];
2423
+ return typeof v !== "string" || v.length === 0;
2424
+ }
2360
2425
  function collectLinkFindings(v, link2, out) {
2361
2426
  const result = v.validate("link", toLinkForSchema(link2));
2362
2427
  if (result.ok) return;
@@ -2401,6 +2466,54 @@ function toLinkForSchema(link2) {
2401
2466
  };
2402
2467
  }
2403
2468
 
2469
+ // kernel/util/trigger-resolve.ts
2470
+ function buildNameIndex(nodes) {
2471
+ const out = /* @__PURE__ */ new Map();
2472
+ indexByCanonicalName(nodes, out);
2473
+ fillIndexWithPathBasename(nodes, out);
2474
+ return out;
2475
+ }
2476
+ function indexByCanonicalName(nodes, out) {
2477
+ for (const node of nodes) {
2478
+ const raw = canonicalName(node);
2479
+ if (raw === null) continue;
2480
+ const key = normalizeTrigger(raw);
2481
+ if (!out.has(key)) out.set(key, node.path);
2482
+ }
2483
+ }
2484
+ function fillIndexWithPathBasename(nodes, out) {
2485
+ for (const node of nodes) {
2486
+ if (canonicalName(node) !== null) continue;
2487
+ const derived = pathBasenameForLink(node.path);
2488
+ if (derived.length === 0) continue;
2489
+ const key = normalizeTrigger(derived);
2490
+ if (!out.has(key)) out.set(key, node.path);
2491
+ }
2492
+ }
2493
+ function canonicalName(node) {
2494
+ const raw = node.frontmatter?.["name"];
2495
+ if (typeof raw !== "string" || raw.length === 0) return null;
2496
+ return raw;
2497
+ }
2498
+ function pathBasenameForLink(path) {
2499
+ const segments = path.split("/").filter((s) => s.length > 0);
2500
+ if (segments.length === 0) return path;
2501
+ const last = segments[segments.length - 1];
2502
+ if (last === "SKILL.md" && segments.length >= 2) {
2503
+ return segments[segments.length - 2];
2504
+ }
2505
+ return last.replace(/\.md$/, "");
2506
+ }
2507
+ function resolveLinkTargetToPath(link2, nameIndex) {
2508
+ const raw = link2.target;
2509
+ const sigil = raw.charAt(0);
2510
+ if (sigil !== "/" && sigil !== "@") return raw;
2511
+ const normalizedTrigger = link2.trigger?.normalizedTrigger;
2512
+ const normalized = typeof normalizedTrigger === "string" ? normalizedTrigger.replace(/^[/@]/, "").trim() : normalizeTrigger(raw.slice(1));
2513
+ const resolved = nameIndex.get(normalized);
2514
+ return resolved ?? raw;
2515
+ }
2516
+
2404
2517
  // built-in-plugins/analyzers/link-counts/index.ts
2405
2518
  var ID20 = "link-counts";
2406
2519
  var linkCountsAnalyzer = {
@@ -2428,10 +2541,12 @@ var linkCountsAnalyzer = {
2428
2541
  }
2429
2542
  },
2430
2543
  evaluate(ctx) {
2544
+ const nameIndex = buildNameIndex(ctx.nodes);
2431
2545
  const perTarget = /* @__PURE__ */ new Map();
2432
2546
  const perSource = /* @__PURE__ */ new Map();
2433
2547
  for (const link2 of ctx.links) {
2434
- bump(perTarget, link2.target, link2.kind);
2548
+ const resolvedTarget = resolveLinkTargetToPath(link2, nameIndex);
2549
+ bump(perTarget, resolvedTarget, link2.kind);
2435
2550
  bump(perSource, link2.source, link2.kind);
2436
2551
  }
2437
2552
  for (const node of ctx.nodes) {
@@ -5747,7 +5862,7 @@ var UPDATE_CHECK_TEXTS = {
5747
5862
  // package.json
5748
5863
  var package_default = {
5749
5864
  name: "@skill-map/cli",
5750
- version: "0.25.0",
5865
+ version: "0.26.1",
5751
5866
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
5752
5867
  license: "MIT",
5753
5868
  type: "module",
@@ -6705,16 +6820,25 @@ var BUMP_TEXTS = {
6705
6820
  // --- failures -------------------------------------------------------------
6706
6821
  bumpFailed: "{{glyph}} sm bump: {{message}}\n",
6707
6822
  storeFailedDetail: "sidecar write failed for {{path}}: {{message}}",
6708
- resolveAbsPathFailed: "cannot resolve absolute path for {{nodePath}}: {{message}}",
6823
+ resolveAbsPathFailed: "cannot resolve absolute path for {{nodePath}}: {{message}}"
6709
6824
  // --- .sm consent gate ---------------------------------------------------
6825
+ // The shared strings live in `consent.texts.ts` (CONSENT_TEXTS); they
6826
+ // are used by every verb that writes a sidecar (`sm bump`,
6827
+ // `sm sidecar refresh`, `sm sidecar annotate`) with a `{{verb}}`
6828
+ // placeholder for the directed prefix.
6829
+ };
6830
+
6831
+ // cli/i18n/consent.texts.ts
6832
+ var CONSENT_TEXTS = {
6710
6833
  /**
6711
6834
  * Pre-prompt context shown before the interactive `confirm()` so the
6712
- * operator sees what they are about to opt into. `.skill-map/settings.local.json`
6713
- * is gitignored, the choice is saved per-checkout, never travels via the repo.
6835
+ * operator sees what they are about to opt into.
6836
+ * `.skill-map/settings.local.json` is gitignored, the choice is saved
6837
+ * per-checkout, never travels via the repo.
6714
6838
  */
6715
- consentPrompt: "skill-map needs your consent to create .sm sidecar files next to your\nsource files in this project. The choice is saved to\n.skill-map/settings.local.json (gitignored, per-checkout) so this prompt\nnever appears again. Decline to abort without persisting the rejection.\n\nAllow .sm sidecar writes in this project?",
6716
- consentAborted: "{{glyph}} sm bump: aborted by user. No .sm sidecar files were written.\n",
6717
- consentRequiredNonTty: "{{glyph}} sm bump: consent required to write .sm sidecar files in this project.\n {{hint}}\n",
6839
+ consentPrompt: "{{glyph}} skill-map needs consent to create .sm sidecar files next to your\n .md sources. Your choice is saved to .skill-map/settings.local.json\n (gitignored) and this prompt will not appear again.\n\nAllow .sm sidecar writes in this project?",
6840
+ consentAborted: "{{glyph}} {{verb}}: aborted by user. No .sm sidecar files were written.\n",
6841
+ consentRequiredNonTty: "{{glyph}} {{verb}}: consent required to write .sm sidecar files in this project.\n {{hint}}\n",
6718
6842
  consentRequiredNonTtyHint: "Pass --yes to grant (writes to .skill-map/settings.local.json, gitignored)."
6719
6843
  };
6720
6844
 
@@ -7112,21 +7236,25 @@ var BumpCommand = class extends SmCommand {
7112
7236
  const isTTY = stdin.isTTY === true;
7113
7237
  if (!isTTY || this.yes) {
7114
7238
  this.printer.error(
7115
- tx(BUMP_TEXTS.consentRequiredNonTty, {
7239
+ tx(CONSENT_TEXTS.consentRequiredNonTty, {
7116
7240
  glyph: ansi.red("\u2715"),
7117
- hint: ansi.dim(BUMP_TEXTS.consentRequiredNonTtyHint)
7241
+ verb: "sm bump",
7242
+ hint: ansi.dim(CONSENT_TEXTS.consentRequiredNonTtyHint)
7118
7243
  })
7119
7244
  );
7120
7245
  return ExitCode.Error;
7121
7246
  }
7122
7247
  const ok = await confirm(
7123
- BUMP_TEXTS.consentPrompt,
7248
+ tx(CONSENT_TEXTS.consentPrompt, { glyph: ansi.cyan("\u2139") }),
7124
7249
  { stdin, stderr },
7125
7250
  { defaultAnswer: "yes" }
7126
7251
  );
7127
7252
  if (!ok) {
7128
7253
  this.printer.info(
7129
- tx(BUMP_TEXTS.consentAborted, { glyph: ansi.cyan("\u2139") })
7254
+ tx(CONSENT_TEXTS.consentAborted, {
7255
+ glyph: ansi.cyan("\u2139"),
7256
+ verb: "sm bump"
7257
+ })
7130
7258
  );
7131
7259
  return ExitCode.Error;
7132
7260
  }
@@ -12502,6 +12630,26 @@ function validateLink(extractor, link2, emitter) {
12502
12630
  const confidence = link2.confidence ?? extractor.defaultConfidence;
12503
12631
  return { ...link2, confidence };
12504
12632
  }
12633
+ function dedupeLinks(links) {
12634
+ const out = /* @__PURE__ */ new Map();
12635
+ for (const link2 of links) {
12636
+ const trigger = link2.trigger?.normalizedTrigger ?? "";
12637
+ const key = `${link2.source}\0${link2.target}\0${link2.kind}\0${trigger}`;
12638
+ const existing = out.get(key);
12639
+ if (existing) {
12640
+ const seen = new Set(existing.sources);
12641
+ for (const src of link2.sources) {
12642
+ if (!seen.has(src)) {
12643
+ seen.add(src);
12644
+ existing.sources = [...existing.sources, src];
12645
+ }
12646
+ }
12647
+ continue;
12648
+ }
12649
+ out.set(key, link2);
12650
+ }
12651
+ return [...out.values()];
12652
+ }
12505
12653
  function recomputeLinkCounts(nodes, links) {
12506
12654
  const byPath3 = /* @__PURE__ */ new Map();
12507
12655
  for (const node of nodes) {
@@ -13492,6 +13640,7 @@ async function runScanInternal(_kernel, options) {
13492
13640
  providerFrontmatter: setup.providerFrontmatter,
13493
13641
  pluginStores: options.pluginStores
13494
13642
  });
13643
+ walked.internalLinks = dedupeLinks(walked.internalLinks);
13495
13644
  recomputeLinkCounts(walked.nodes, walked.internalLinks);
13496
13645
  recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);
13497
13646
  await dispatchExtractorCompleted(exts.extractors, emitter, hookDispatcher);
@@ -22623,17 +22772,12 @@ var SIDECAR_TEXTS = {
22623
22772
  annotateCreated: "{{glyph}} Created {{sidecarPath}}. Edit it, then run `sm bump {{nodePath}}` to commit the version.\n",
22624
22773
  /** Trailing dim tag for sidecar prune dry-run (matches the orphans pattern). */
22625
22774
  sidecarDryRunTag: " (no changes made)",
22626
- annotateFailed: "{{glyph}} sm sidecar annotate: {{message}}\n",
22627
- // --- .sm consent gate (shared across refresh + annotate) -----------------
22628
- /**
22629
- * Pre-prompt context shown before the interactive `confirm()` so the
22630
- * operator sees what they are about to opt into. `.skill-map/settings.local.json`
22631
- * is gitignored, the choice is saved per-checkout, never travels via the repo.
22632
- */
22633
- consentPrompt: "skill-map needs your consent to create .sm sidecar files next to your\nsource files in this project. The choice is saved to\n.skill-map/settings.local.json (gitignored, per-checkout) so this prompt\nnever appears again. Decline to abort without persisting the rejection.\n\nAllow .sm sidecar writes in this project?",
22634
- consentAborted: "{{glyph}} sm sidecar: aborted by user. No .sm sidecar files were written.\n",
22635
- consentRequiredNonTty: "{{glyph}} sm sidecar: consent required to write .sm sidecar files in this project.\n {{hint}}\n",
22636
- consentRequiredNonTtyHint: "Pass --yes to grant (writes to .skill-map/settings.local.json, gitignored)."
22775
+ annotateFailed: "{{glyph}} sm sidecar annotate: {{message}}\n"
22776
+ // --- .sm consent gate ---------------------------------------------------
22777
+ // The shared strings live in `consent.texts.ts` (CONSENT_TEXTS); they
22778
+ // are used by every verb that writes a sidecar (`sm bump`,
22779
+ // `sm sidecar refresh`, `sm sidecar annotate`) with a `{{verb}}`
22780
+ // placeholder for the directed prefix.
22637
22781
  };
22638
22782
 
22639
22783
  // cli/commands/sidecar.ts
@@ -22646,21 +22790,25 @@ async function runWithSidecarConsent(bag, ansi, dispatch) {
22646
22790
  if (!isTTY || bag.yes) {
22647
22791
  const errGlyph = ansi.red("\u2715");
22648
22792
  bag.printError(
22649
- tx(SIDECAR_TEXTS.consentRequiredNonTty, {
22793
+ tx(CONSENT_TEXTS.consentRequiredNonTty, {
22650
22794
  glyph: errGlyph,
22651
- hint: ansi.dim(SIDECAR_TEXTS.consentRequiredNonTtyHint)
22795
+ verb: "sm sidecar",
22796
+ hint: ansi.dim(CONSENT_TEXTS.consentRequiredNonTtyHint)
22652
22797
  })
22653
22798
  );
22654
22799
  return ExitCode.Error;
22655
22800
  }
22656
22801
  const ok = await confirm(
22657
- SIDECAR_TEXTS.consentPrompt,
22802
+ tx(CONSENT_TEXTS.consentPrompt, { glyph: ansi.cyan("\u2139") }),
22658
22803
  { stdin: bag.stdin, stderr: bag.stderr },
22659
22804
  { defaultAnswer: "yes" }
22660
22805
  );
22661
22806
  if (!ok) {
22662
22807
  bag.printInfo(
22663
- tx(SIDECAR_TEXTS.consentAborted, { glyph: ansi.cyan("\u2139") })
22808
+ tx(CONSENT_TEXTS.consentAborted, {
22809
+ glyph: ansi.cyan("\u2139"),
22810
+ verb: "sm sidecar"
22811
+ })
22664
22812
  );
22665
22813
  return ExitCode.Error;
22666
22814
  }