@skill-map/cli 0.28.0 → 0.30.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.
Files changed (36) hide show
  1. package/dist/cli/tutorial/sm-tutorial.md +94 -20
  2. package/dist/cli.js +1561 -1214
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +116 -99
  5. package/dist/index.js.map +1 -1
  6. package/dist/kernel/index.d.ts +903 -1004
  7. package/dist/kernel/index.js +116 -99
  8. package/dist/kernel/index.js.map +1 -1
  9. package/dist/ui/chunk-3SI3TVER.js +7 -0
  10. package/dist/ui/{chunk-4GTCV7V4.js → chunk-47OZB7LR.js} +1 -1
  11. package/dist/ui/{chunk-JMP2LDMI.js → chunk-5JBW2LUN.js} +1 -1
  12. package/dist/ui/{chunk-Y7MXGXU3.js → chunk-DZBSELHN.js} +1 -1
  13. package/dist/ui/{chunk-Z2667C3S.js → chunk-FEPH4VNB.js} +1 -1
  14. package/dist/ui/{chunk-PY2R7LHN.js → chunk-FQOZBFJ5.js} +1 -1
  15. package/dist/ui/chunk-HOJFYUH4.js +123 -0
  16. package/dist/ui/{chunk-WOLLYGGL.js → chunk-KJQEO6P3.js} +1 -1
  17. package/dist/ui/chunk-LNRQ7VKE.js +1 -0
  18. package/dist/ui/{chunk-VO6NF24F.js → chunk-LS2NXZQZ.js} +1 -1
  19. package/dist/ui/{chunk-J3YWUNFO.js → chunk-LTQTJU54.js} +1 -1
  20. package/dist/ui/{chunk-6BG7PBUN.js → chunk-NGIFGXW7.js} +1 -1
  21. package/dist/ui/{chunk-5W6J6H76.js → chunk-SBCO7ZSP.js} +1 -1
  22. package/dist/ui/chunk-VB56BUGO.js +1 -0
  23. package/dist/ui/{chunk-UXCAEDR6.js → chunk-VDQLDTTR.js} +1 -1
  24. package/dist/ui/{chunk-AD7RBRD3.js → chunk-WJLIYGWJ.js} +5 -5
  25. package/dist/ui/chunk-YQIWQVJ6.js +317 -0
  26. package/dist/ui/favicon-matrix.svg +15 -0
  27. package/dist/ui/index.html +12 -2
  28. package/dist/ui/main-X5YGJFU6.js +2 -0
  29. package/dist/ui/{styles-IKG3B6AM.css → styles-2WO3KNOY.css} +1 -1
  30. package/package.json +10 -7
  31. package/dist/ui/chunk-BGDH7CDV.js +0 -1
  32. package/dist/ui/chunk-H2J55DNK.js +0 -7
  33. package/dist/ui/chunk-Q7L6LLAK.js +0 -1
  34. package/dist/ui/chunk-VH5GRUT7.js +0 -255
  35. package/dist/ui/chunk-XCUOAV77.js +0 -123
  36. package/dist/ui/main-TZL26MZU.js +0 -2
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // cli/entry.ts
2
- import { existsSync as existsSync29 } from "fs";
2
+ import { existsSync as existsSync30 } from "fs";
3
3
  import { Builtins, Cli as Cli2 } from "clipanion";
4
4
 
5
5
  // kernel/adapters/in-memory-progress.ts
@@ -150,14 +150,6 @@ function makeHookDispatcher(hooks, emitter) {
150
150
  }
151
151
  const byTrigger = /* @__PURE__ */ new Map();
152
152
  for (const hook of hooks) {
153
- if (hook.mode === "probabilistic") {
154
- const qualifiedId2 = qualifiedExtensionId(hook.pluginId, hook.id);
155
- log.warn(
156
- `Probabilistic hook ${qualifiedId2} deferred to job subsystem (future job subsystem). The hook is registered but will not dispatch in-scan.`,
157
- { hookId: qualifiedId2, mode: "probabilistic" }
158
- );
159
- continue;
160
- }
161
153
  for (const trig of hook.triggers) {
162
154
  const bucket = byTrigger.get(trig);
163
155
  if (bucket) bucket.push(hook);
@@ -249,7 +241,7 @@ function bucketByKind(kind, instance, bag) {
249
241
  }
250
242
  }
251
243
 
252
- // built-in-plugins/providers/claude/schemas/skill.schema.json
244
+ // plugins/claude/providers/claude/schemas/skill.schema.json
253
245
  var skill_schema_default = {
254
246
  $schema: "https://json-schema.org/draft/2020-12/schema",
255
247
  $id: "https://skill-map.dev/providers/claude/v1/frontmatter/skill.schema.json",
@@ -263,7 +255,7 @@ var skill_schema_default = {
263
255
  properties: {}
264
256
  };
265
257
 
266
- // built-in-plugins/providers/claude/schemas/skill-base.schema.json
258
+ // plugins/claude/providers/claude/schemas/skill-base.schema.json
267
259
  var skill_base_schema_default = {
268
260
  $schema: "https://json-schema.org/draft/2020-12/schema",
269
261
  $id: "https://skill-map.dev/providers/claude/v1/frontmatter/skill-base.schema.json",
@@ -342,7 +334,7 @@ var skill_base_schema_default = {
342
334
  }
343
335
  };
344
336
 
345
- // built-in-plugins/providers/claude/schemas/agent.schema.json
337
+ // plugins/claude/providers/claude/schemas/agent.schema.json
346
338
  var agent_schema_default = {
347
339
  $schema: "https://json-schema.org/draft/2020-12/schema",
348
340
  $id: "https://skill-map.dev/providers/claude/v1/frontmatter/agent.schema.json",
@@ -423,7 +415,7 @@ var agent_schema_default = {
423
415
  }
424
416
  };
425
417
 
426
- // built-in-plugins/providers/claude/schemas/command.schema.json
418
+ // plugins/claude/providers/claude/schemas/command.schema.json
427
419
  var command_schema_default = {
428
420
  $schema: "https://json-schema.org/draft/2020-12/schema",
429
421
  $id: "https://skill-map.dev/providers/claude/v1/frontmatter/command.schema.json",
@@ -437,14 +429,13 @@ var command_schema_default = {
437
429
  properties: {}
438
430
  };
439
431
 
440
- // built-in-plugins/providers/claude/index.ts
432
+ // plugins/claude/providers/claude/index.ts
441
433
  var claudeProvider = {
442
434
  id: "claude",
443
435
  pluginId: "claude",
444
436
  kind: "provider",
445
437
  version: "1.0.0",
446
438
  description: "Walks Claude Code scope conventions (.claude/{agents,commands,skills}).",
447
- stability: "stable",
448
439
  // Declarative discovery: `.md` files parsed via the kernel's
449
440
  // `frontmatter-yaml` built-in. Equals the kernel's default but stated
450
441
  // explicitly so the Provider doubles as a copy-paste template for
@@ -474,7 +465,6 @@ var claudeProvider = {
474
465
  agent: {
475
466
  schema: "./schemas/agent.schema.json",
476
467
  schemaJson: agent_schema_default,
477
- defaultRefreshAction: "claude/summarize-agent",
478
468
  ui: {
479
469
  label: "Agents",
480
470
  color: "#3b82f6",
@@ -485,7 +475,6 @@ var claudeProvider = {
485
475
  command: {
486
476
  schema: "./schemas/command.schema.json",
487
477
  schemaJson: command_schema_default,
488
- defaultRefreshAction: "claude/summarize-command",
489
478
  ui: {
490
479
  label: "Commands",
491
480
  color: "#f59e0b",
@@ -499,7 +488,6 @@ var claudeProvider = {
499
488
  skill: {
500
489
  schema: "./schemas/skill.schema.json",
501
490
  schemaJson: skill_schema_default,
502
- defaultRefreshAction: "claude/summarize-skill",
503
491
  ui: {
504
492
  label: "Skills",
505
493
  color: "#10b981",
@@ -523,7 +511,7 @@ var claudeProvider = {
523
511
  }
524
512
  };
525
513
 
526
- // built-in-plugins/providers/gemini/schemas/agent.schema.json
514
+ // plugins/gemini/providers/gemini/schemas/agent.schema.json
527
515
  var agent_schema_default2 = {
528
516
  $schema: "https://json-schema.org/draft/2020-12/schema",
529
517
  $id: "https://skill-map.dev/providers/gemini/v1/frontmatter/agent.schema.json",
@@ -577,7 +565,7 @@ var agent_schema_default2 = {
577
565
  }
578
566
  };
579
567
 
580
- // built-in-plugins/providers/gemini/schemas/skill.schema.json
568
+ // plugins/gemini/providers/gemini/schemas/skill.schema.json
581
569
  var skill_schema_default2 = {
582
570
  $schema: "https://json-schema.org/draft/2020-12/schema",
583
571
  $id: "https://skill-map.dev/providers/gemini/v1/frontmatter/skill.schema.json",
@@ -591,14 +579,13 @@ var skill_schema_default2 = {
591
579
  properties: {}
592
580
  };
593
581
 
594
- // built-in-plugins/providers/gemini/index.ts
582
+ // plugins/gemini/providers/gemini/index.ts
595
583
  var geminiProvider = {
596
584
  id: "gemini",
597
585
  pluginId: "gemini",
598
586
  kind: "provider",
599
587
  version: "1.0.0",
600
588
  description: "Walks Gemini CLI scope conventions (.gemini/{agents,skills}).",
601
- stability: "stable",
602
589
  read: { extensions: [".md"], parser: "frontmatter-yaml" },
603
590
  // Per spec § A.6, defaultRefreshAction values MUST be qualified
604
591
  // action ids. The summarize-* actions are not yet implemented as
@@ -616,7 +603,6 @@ var geminiProvider = {
616
603
  agent: {
617
604
  schema: "./schemas/agent.schema.json",
618
605
  schemaJson: agent_schema_default2,
619
- defaultRefreshAction: "gemini/summarize-agent",
620
606
  ui: {
621
607
  label: "Agents",
622
608
  color: "#3b82f6",
@@ -627,7 +613,6 @@ var geminiProvider = {
627
613
  skill: {
628
614
  schema: "./schemas/skill.schema.json",
629
615
  schemaJson: skill_schema_default2,
630
- defaultRefreshAction: "gemini/summarize-skill",
631
616
  ui: {
632
617
  label: "Skills",
633
618
  color: "#10b981",
@@ -644,7 +629,7 @@ var geminiProvider = {
644
629
  }
645
630
  };
646
631
 
647
- // built-in-plugins/providers/agent-skills/schemas/skill.schema.json
632
+ // plugins/agent-skills/providers/agent-skills/schemas/skill.schema.json
648
633
  var skill_schema_default3 = {
649
634
  $schema: "https://json-schema.org/draft/2020-12/schema",
650
635
  $id: "https://skill-map.dev/providers/agent-skills/v1/frontmatter/skill.schema.json",
@@ -658,20 +643,18 @@ var skill_schema_default3 = {
658
643
  properties: {}
659
644
  };
660
645
 
661
- // built-in-plugins/providers/agent-skills/index.ts
646
+ // plugins/agent-skills/providers/agent-skills/index.ts
662
647
  var agentSkillsProvider = {
663
648
  id: "agent-skills",
664
649
  pluginId: "agent-skills",
665
650
  kind: "provider",
666
651
  version: "1.0.0",
667
652
  description: "Agent Skills open standard. Vendor-neutral path `.agents/skills/<name>/SKILL.md` (Anthropic, OpenAI, Google). See agentskills.io.",
668
- stability: "stable",
669
653
  read: { extensions: [".md"], parser: "frontmatter-yaml" },
670
654
  kinds: {
671
655
  skill: {
672
656
  schema: "./schemas/skill.schema.json",
673
657
  schemaJson: skill_schema_default3,
674
- defaultRefreshAction: "agent-skills/summarize-skill",
675
658
  ui: {
676
659
  label: "Skills",
677
660
  color: "#10b981",
@@ -686,7 +669,7 @@ var agentSkillsProvider = {
686
669
  }
687
670
  };
688
671
 
689
- // built-in-plugins/providers/core-markdown/schemas/markdown.schema.json
672
+ // plugins/core/providers/core-markdown/schemas/markdown.schema.json
690
673
  var markdown_schema_default = {
691
674
  $schema: "https://json-schema.org/draft/2020-12/schema",
692
675
  $id: "https://skill-map.dev/providers/core/v1/frontmatter/markdown.schema.json",
@@ -699,14 +682,13 @@ var markdown_schema_default = {
699
682
  additionalProperties: true
700
683
  };
701
684
 
702
- // built-in-plugins/providers/core-markdown/index.ts
685
+ // plugins/core/providers/core-markdown/index.ts
703
686
  var coreMarkdownProvider = {
704
687
  id: "markdown",
705
688
  pluginId: "core",
706
689
  kind: "provider",
707
690
  version: "1.0.0",
708
691
  description: "Universal `.md` fallback. Claims any markdown file no vendor-specific Provider classifies.",
709
- stability: "stable",
710
692
  read: { extensions: [".md"], parser: "frontmatter-yaml" },
711
693
  // Per spec § A.6, defaultRefreshAction values MUST be qualified
712
694
  // action ids. The summarize-markdown action is not yet implemented
@@ -723,7 +705,6 @@ var coreMarkdownProvider = {
723
705
  markdown: {
724
706
  schema: "./schemas/markdown.schema.json",
725
707
  schemaJson: markdown_schema_default,
726
- defaultRefreshAction: "core/summarize-markdown",
727
708
  ui: {
728
709
  label: "Markdown",
729
710
  color: "#5b908c",
@@ -740,7 +721,7 @@ var coreMarkdownProvider = {
740
721
  }
741
722
  };
742
723
 
743
- // built-in-plugins/extractors/annotations/index.ts
724
+ // plugins/core/extractors/annotations/index.ts
744
725
  var ID = "annotations";
745
726
  var annotationsExtractor = {
746
727
  id: ID,
@@ -748,9 +729,6 @@ var annotationsExtractor = {
748
729
  kind: "extractor",
749
730
  version: "1.0.0",
750
731
  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.",
751
- stability: "stable",
752
- emitsLinkKinds: ["supersedes"],
753
- defaultConfidence: "high",
754
732
  scope: "frontmatter",
755
733
  extract(ctx) {
756
734
  const sourcePath = ctx.node.path;
@@ -797,6 +775,53 @@ function link(source, target) {
797
775
  };
798
776
  }
799
777
 
778
+ // kernel/util/strip-code-blocks.ts
779
+ var FENCE_RE = /^(?<indent> {0,3})(?<fence>`{3,}|~{3,})/;
780
+ function stripCodeBlocks(input) {
781
+ if (!input) return input;
782
+ const fenceless = stripFences(input);
783
+ return stripInline(fenceless);
784
+ }
785
+ function stripFences(input) {
786
+ const out = [];
787
+ const lines = input.split("\n");
788
+ let openFence = null;
789
+ for (const line of lines) {
790
+ if (openFence) {
791
+ const closer = matchClosingFence(line, openFence);
792
+ if (closer) {
793
+ out.push(blank(line));
794
+ openFence = null;
795
+ } else {
796
+ out.push(blank(line));
797
+ }
798
+ continue;
799
+ }
800
+ const open = FENCE_RE.exec(line);
801
+ if (open?.groups) {
802
+ openFence = open.groups["fence"];
803
+ out.push(blank(line));
804
+ continue;
805
+ }
806
+ out.push(line);
807
+ }
808
+ return out.join("\n");
809
+ }
810
+ function matchClosingFence(line, openFence) {
811
+ const m = FENCE_RE.exec(line);
812
+ if (!m?.groups) return false;
813
+ const fence = m.groups["fence"];
814
+ return fence[0] === openFence[0] && fence.length >= openFence.length;
815
+ }
816
+ function stripInline(input) {
817
+ return input.replace(/(`+)([\s\S]*?)\1/g, (_full, ticks, body) => {
818
+ return ticks.replace(/`/g, " ") + blank(body) + ticks.replace(/`/g, " ");
819
+ });
820
+ }
821
+ function blank(s) {
822
+ return s.replace(/[^\s]/g, " ");
823
+ }
824
+
800
825
  // kernel/trigger-normalize.ts
801
826
  function normalizeTrigger(source) {
802
827
  let out = source.normalize("NFD");
@@ -807,67 +832,51 @@ function normalizeTrigger(source) {
807
832
  return out.trim();
808
833
  }
809
834
 
810
- // built-in-plugins/extractors/slash/index.ts
811
- var ID2 = "slash";
812
- var SLASH_RE = /(?<![A-Za-z0-9_/.:?#])(\/[a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)/gi;
813
- var slashExtractor = {
814
- id: ID2,
815
- pluginId: "core",
816
- kind: "extractor",
817
- version: "1.0.0",
818
- description: "Detects `/command` invocations in a node's body and turns each one into an arrow between nodes in the graph.",
819
- stability: "stable",
820
- emitsLinkKinds: ["invokes"],
821
- defaultConfidence: "medium",
822
- scope: "body",
823
- extract(ctx) {
824
- const seen = /* @__PURE__ */ new Set();
825
- for (const match of ctx.body.matchAll(SLASH_RE)) {
826
- const original = match[1];
827
- const normalized = normalizeTrigger(original);
828
- if (seen.has(normalized)) continue;
829
- seen.add(normalized);
830
- ctx.emitLink({
831
- source: ctx.node.path,
832
- target: original,
833
- kind: "invokes",
834
- confidence: "medium",
835
- sources: [ID2],
836
- trigger: {
837
- originalTrigger: original,
838
- normalizedTrigger: normalized
839
- }
840
- });
841
- }
842
- }
843
- };
844
-
845
- // built-in-plugins/extractors/at-directive/index.ts
846
- var ID3 = "at-directive";
847
- var AT_RE = /(?:^|[^A-Za-z0-9_@])(@[a-z0-9][a-z0-9_-]*(?:[/:][a-z0-9][a-z0-9_-]*)?)/gi;
835
+ // plugins/core/extractors/at-directive/index.ts
836
+ var ID2 = "at-directive";
837
+ var AT_RE = /(?:^|[^A-Za-z0-9_@])(@(?:\.{1,2}\/|\/)?[a-z0-9](?:[a-z0-9_\-./]*[a-z0-9_])?(?::[a-z0-9][a-z0-9_-]*)?)/gi;
838
+ var FILE_EXT_RE = /\.(md|mdx|js|jsx|ts|tsx|json|yml|yaml|toml|txt|html|css|scss|less|py|rb|go|rs|java|c|cpp|h|hpp|sh|sql|svg|png|jpg|jpeg|gif|webp|pdf)$/i;
848
839
  var atDirectiveExtractor = {
849
- id: ID3,
840
+ id: ID2,
850
841
  pluginId: "core",
851
842
  kind: "extractor",
852
843
  version: "1.0.0",
853
- description: "Detects `@agent-name` mentions in a node's body and turns each one into an arrow between nodes in the graph.",
854
- stability: "stable",
855
- emitsLinkKinds: ["mentions"],
856
- defaultConfidence: "medium",
844
+ description: "Detects `@<token>` directives in a node's body. 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, matching how Claude Code / Gemini CLI / Cursor read the same syntax.",
857
845
  scope: "body",
858
846
  extract(ctx) {
859
- const seen = /* @__PURE__ */ new Set();
860
- for (const match of ctx.body.matchAll(AT_RE)) {
847
+ const seenMentions = /* @__PURE__ */ new Set();
848
+ const seenReferences = /* @__PURE__ */ new Set();
849
+ const body = stripCodeBlocks(ctx.body);
850
+ for (const match of body.matchAll(AT_RE)) {
861
851
  const original = match[1];
852
+ const bare = original.slice(1);
853
+ const isReference = bare.startsWith("./") || bare.startsWith("../") || bare.startsWith("/") || FILE_EXT_RE.test(bare);
854
+ if (isReference) {
855
+ const target = bare.replace(/^\.\//, "");
856
+ if (seenReferences.has(target)) continue;
857
+ seenReferences.add(target);
858
+ ctx.emitLink({
859
+ source: ctx.node.path,
860
+ target,
861
+ kind: "references",
862
+ confidence: "medium",
863
+ sources: [ID2],
864
+ trigger: {
865
+ originalTrigger: original,
866
+ normalizedTrigger: target.toLowerCase()
867
+ }
868
+ });
869
+ continue;
870
+ }
862
871
  const normalized = normalizeTrigger(original);
863
- if (seen.has(normalized)) continue;
864
- seen.add(normalized);
872
+ if (seenMentions.has(normalized)) continue;
873
+ seenMentions.add(normalized);
865
874
  ctx.emitLink({
866
875
  source: ctx.node.path,
867
876
  target: original,
868
877
  kind: "mentions",
869
878
  confidence: "medium",
870
- sources: [ID3],
879
+ sources: [ID2],
871
880
  trigger: {
872
881
  originalTrigger: original,
873
882
  normalizedTrigger: normalized
@@ -877,19 +886,16 @@ var atDirectiveExtractor = {
877
886
  }
878
887
  };
879
888
 
880
- // built-in-plugins/extractors/external-url-counter/index.ts
881
- var ID4 = "external-url-counter";
889
+ // plugins/core/extractors/external-url-counter/index.ts
890
+ var ID3 = "external-url-counter";
882
891
  var URL_RE = /https?:\/\/[^\s<>"'`)\]]+/g;
883
892
  var TRAILING_PUNCT = /[.,;:!?]+$/;
884
893
  var externalUrlCounterExtractor = {
885
- id: ID4,
894
+ id: ID3,
886
895
  pluginId: "core",
887
896
  kind: "extractor",
888
897
  version: "1.0.0",
889
898
  description: "Counts the distinct external URLs in a node's body and shows the total on the card.",
890
- stability: "stable",
891
- emitsLinkKinds: ["references"],
892
- defaultConfidence: "low",
893
899
  scope: "body",
894
900
  /**
895
901
  * Phase 6 / View contribution system, surface the distinct-URL
@@ -907,7 +913,7 @@ var externalUrlCounterExtractor = {
907
913
  * inherited from the footer `.sm-gnode__stat` styles cloned by
908
914
  * the `NodeCounter` renderer.
909
915
  */
910
- viewContributions: {
916
+ ui: {
911
917
  count: {
912
918
  slot: "card.footer.left",
913
919
  icon: "pi-link",
@@ -932,7 +938,7 @@ var externalUrlCounterExtractor = {
932
938
  target: normalized,
933
939
  kind: "references",
934
940
  confidence: "low",
935
- sources: [ID4],
941
+ sources: [ID3],
936
942
  trigger: {
937
943
  originalTrigger: original,
938
944
  normalizedTrigger: normalized
@@ -977,20 +983,17 @@ function lineFor(lineStarts, offset) {
977
983
  return lo + 1;
978
984
  }
979
985
 
980
- // built-in-plugins/extractors/markdown-link/index.ts
986
+ // plugins/core/extractors/markdown-link/index.ts
981
987
  import { posix as pathPosix } from "path";
982
- var ID5 = "markdown-link";
988
+ var ID4 = "markdown-link";
983
989
  var LINK_RE = /(?<!!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
984
990
  var URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
985
991
  var markdownLinkExtractor = {
986
- id: ID5,
992
+ id: ID4,
987
993
  pluginId: "core",
988
994
  kind: "extractor",
989
995
  version: "1.0.0",
990
996
  description: "Detects markdown links (`[text](path)`) in a node's body and turns each one into an arrow between nodes in the graph.",
991
- stability: "stable",
992
- emitsLinkKinds: ["references"],
993
- defaultConfidence: "high",
994
997
  scope: "body",
995
998
  extract(ctx) {
996
999
  const seen = /* @__PURE__ */ new Set();
@@ -1008,7 +1011,7 @@ var markdownLinkExtractor = {
1008
1011
  target: resolved,
1009
1012
  kind: "references",
1010
1013
  confidence: "high",
1011
- sources: [ID5],
1014
+ sources: [ID4],
1012
1015
  trigger: {
1013
1016
  originalTrigger: original,
1014
1017
  normalizedTrigger: resolved
@@ -1047,99 +1050,54 @@ function lineFor2(lineStarts, offset) {
1047
1050
  return lo + 1;
1048
1051
  }
1049
1052
 
1050
- // built-in-plugins/analyzers/stability/index.ts
1051
- var ID6 = "stability";
1052
- var EXPERIMENTAL_TOOLTIP = "Experimental: API may change";
1053
- var DEPRECATED_TOOLTIP = "Deprecated: avoid in new code";
1054
- var stabilityAnalyzer = {
1055
- id: ID6,
1053
+ // plugins/core/extractors/slash/index.ts
1054
+ var ID5 = "slash";
1055
+ var SLASH_RE = /(?<![A-Za-z0-9_/.:?#])(\/[a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)/gi;
1056
+ var slashExtractor = {
1057
+ id: ID5,
1056
1058
  pluginId: "core",
1057
- kind: "analyzer",
1059
+ kind: "extractor",
1058
1060
  version: "1.0.0",
1059
- description: "Reports node lifecycle stage (`experimental`, `deprecated`) on the card.",
1060
- stability: "stable",
1061
- mode: "deterministic",
1062
- viewContributions: {
1063
- experimental: {
1064
- slot: "card.footer.right",
1065
- icon: "fa-solid fa-flask",
1066
- label: "experimental",
1067
- emitWhenEmpty: false,
1068
- priority: 10
1069
- },
1070
- deprecated: {
1071
- slot: "card.footer.right",
1072
- icon: "pi-ban",
1073
- label: "deprecated",
1074
- emitWhenEmpty: false,
1075
- priority: 10
1076
- }
1077
- },
1078
- evaluate(ctx) {
1079
- const issues = [];
1080
- for (const node of ctx.nodes) {
1081
- const stability = readStability(node);
1082
- if (stability === "experimental") {
1083
- ctx.emitContribution(node.path, "experimental", {
1084
- value: 0,
1085
- tooltip: EXPERIMENTAL_TOOLTIP
1086
- });
1087
- issues.push({
1088
- analyzerId: ID6,
1089
- severity: "info",
1090
- nodeIds: [node.path],
1091
- message: `Node '${node.path}' is marked experimental: API may change.`,
1092
- data: { stability }
1093
- });
1094
- } else if (stability === "deprecated") {
1095
- ctx.emitContribution(node.path, "deprecated", {
1096
- value: 0,
1097
- tooltip: DEPRECATED_TOOLTIP,
1098
- severity: "warn"
1099
- });
1100
- issues.push({
1101
- analyzerId: ID6,
1102
- severity: "warn",
1103
- nodeIds: [node.path],
1104
- message: `Node '${node.path}' is marked deprecated: avoid in new code.`,
1105
- data: { stability }
1106
- });
1107
- }
1061
+ description: "Detects `/command` invocations in a node's body and turns each one into an arrow between nodes in the graph.",
1062
+ scope: "body",
1063
+ extract(ctx) {
1064
+ const seen = /* @__PURE__ */ new Set();
1065
+ const body = stripCodeBlocks(ctx.body);
1066
+ for (const match of body.matchAll(SLASH_RE)) {
1067
+ const original = match[1];
1068
+ const endIdx = (match.index ?? 0) + match[0].length;
1069
+ const nextChar = body[endIdx];
1070
+ if (nextChar && /[A-Za-z0-9_/-]/.test(nextChar)) continue;
1071
+ const normalized = normalizeTrigger(original);
1072
+ if (seen.has(normalized)) continue;
1073
+ seen.add(normalized);
1074
+ ctx.emitLink({
1075
+ source: ctx.node.path,
1076
+ target: original,
1077
+ kind: "invokes",
1078
+ confidence: "medium",
1079
+ sources: [ID5],
1080
+ trigger: {
1081
+ originalTrigger: original,
1082
+ normalizedTrigger: normalized
1083
+ }
1084
+ });
1108
1085
  }
1109
- return issues;
1110
1086
  }
1111
1087
  };
1112
- function readStability(node) {
1113
- const fromAnn = node.sidecar?.annotations?.["stability"];
1114
- if (isStability(fromAnn)) return fromAnn;
1115
- const legacy = readLegacyMetadataStability(node.frontmatter);
1116
- return isStability(legacy) ? legacy : null;
1117
- }
1118
- function readLegacyMetadataStability(fm) {
1119
- if (!fm) return void 0;
1120
- const meta = fm["metadata"];
1121
- if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0;
1122
- return meta["stability"];
1123
- }
1124
- function isStability(value) {
1125
- return value === "experimental" || value === "deprecated" || value === "stable";
1126
- }
1127
1088
 
1128
- // built-in-plugins/extractors/tools-count/index.ts
1129
- var ID7 = "tools-count";
1089
+ // plugins/core/extractors/tools-count/index.ts
1090
+ var ID6 = "tools-count";
1130
1091
  var TOOLTIP_MAX = 255;
1131
1092
  var toolsCountExtractor = {
1132
- id: ID7,
1093
+ id: ID6,
1133
1094
  pluginId: "core",
1134
1095
  kind: "extractor",
1135
1096
  version: "1.0.0",
1136
1097
  description: "Counts the tools an agent declares in its frontmatter and shows the total on the agent card.",
1137
- stability: "stable",
1138
- emitsLinkKinds: [],
1139
- defaultConfidence: "high",
1140
1098
  scope: "frontmatter",
1141
- applicableKinds: ["agent"],
1142
- viewContributions: {
1099
+ precondition: { kind: ["claude/agent"] },
1100
+ ui: {
1143
1101
  count: {
1144
1102
  slot: "card.footer.left",
1145
1103
  icon: "pi-wrench",
@@ -1168,165 +1126,148 @@ function buildTooltip(names) {
1168
1126
  return `${joined.slice(0, TOOLTIP_MAX - 1)}\u2026`;
1169
1127
  }
1170
1128
 
1171
- // built-in-plugins/i18n/trigger-collision.texts.ts
1172
- var TRIGGER_COLLISION_TEXTS = {
1173
- /**
1174
- * Top-level message when `analyzeTriggerBucket` accumulated exactly one
1175
- * cause part. Used for the advertiser-ambiguous-only, invocation-
1176
- * ambiguous-only, and cross-kind-only branches.
1177
- */
1178
- messageOnePart: 'Trigger "{{normalized}}" has {{part}}.',
1179
- /**
1180
- * Top-level message when `analyzeTriggerBucket` accumulated two cause
1181
- * parts (advertiser-ambiguous AND invocation-ambiguous fire together).
1182
- * The joiner lives inside the template so future locales can adapt it
1183
- * (e.g. `'; y '` in Spanish) without touching the rule code.
1184
- */
1185
- messageTwoParts: 'Trigger "{{normalized}}" has {{first}}; and {{second}}.',
1186
- /** `<n> nodes advertise it: <list>` part, fires on the advertiser-ambiguous branch. */
1187
- partAdvertisers: "{{count}} nodes advertise it: {{paths}}",
1188
- /** `<n> distinct invocation forms: <list>` part, fires on the invocation-ambiguous branch. */
1189
- partInvocations: "{{count}} distinct invocation forms: {{forms}}",
1190
- /** Singular cross-kind cause: `non-canonical invocation <form> against advertiser <path>`. */
1191
- partNonCanonicalSingular: "non-canonical invocation {{forms}} against advertiser {{advertiser}}",
1192
- /** Plural cross-kind cause: `non-canonical invocations <forms> against advertiser <path>`. */
1193
- partNonCanonicalPlural: "non-canonical invocations {{forms}} against advertiser {{advertiser}}"
1129
+ // plugins/core/analyzers/annotation-orphan/text.ts
1130
+ var ANNOTATION_ORPHAN_TEXTS = {
1131
+ /** Sidecar `<path>.sm` has no matching `<path>.md`. */
1132
+ message: "Orphan sidecar: {{sidecarPath}} has no matching markdown node at {{expectedMdPath}}."
1194
1133
  };
1195
1134
 
1196
- // built-in-plugins/analyzers/trigger-collision/index.ts
1197
- var ID8 = "trigger-collision";
1198
- var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
1199
- "command",
1200
- "skill",
1201
- "agent"
1202
- ]);
1203
- var triggerCollisionAnalyzer = {
1204
- id: ID8,
1135
+ // plugins/core/analyzers/annotation-orphan/index.ts
1136
+ var ID7 = "annotation-orphan";
1137
+ var annotationOrphanAnalyzer = {
1138
+ id: ID7,
1205
1139
  pluginId: "core",
1206
1140
  kind: "analyzer",
1207
- mode: "deterministic",
1208
1141
  version: "1.0.0",
1209
- description: "Detects and flags two or more nodes claiming the same `/command` or `@agent` name.",
1210
- stability: "stable",
1211
- // Two claim-collection passes (advertisement + invocation) feeding
1212
- // the bucket map. Per-bucket analysis lives in `analyzeTriggerBucket`.
1213
- // eslint-disable-next-line complexity
1142
+ description: "Detects and flags sidecars (`.sm`) whose `.md` no longer exists.",
1143
+ mode: "deterministic",
1214
1144
  evaluate(ctx) {
1215
- const buckets = /* @__PURE__ */ new Map();
1216
- const push = (key, claim) => {
1217
- const bucket = buckets.get(key) ?? [];
1218
- bucket.push(claim);
1219
- buckets.set(key, bucket);
1220
- };
1221
- for (const node of ctx.nodes) {
1222
- if (!ADVERTISING_KINDS.has(node.kind)) continue;
1223
- const raw = node.frontmatter?.["name"];
1224
- if (typeof raw !== "string" || raw.length === 0) continue;
1225
- const normalized = `/${normalizeTrigger(raw)}`;
1226
- if (normalized === "/") continue;
1227
- push(normalized, {
1228
- kind: "advertiser",
1229
- token: node.path,
1230
- nodeId: node.path,
1231
- canonicalForm: `/${raw}`
1232
- });
1233
- }
1234
- for (const link2 of ctx.links) {
1235
- const normalized = link2.trigger?.normalizedTrigger;
1236
- if (!normalized) continue;
1237
- push(normalized, {
1238
- kind: "invocation",
1239
- token: link2.target,
1240
- nodeId: link2.source
1241
- });
1242
- }
1145
+ const orphans = ctx.orphanSidecars;
1146
+ if (!orphans || orphans.length === 0) return [];
1243
1147
  const issues = [];
1244
- for (const [normalized, claims] of buckets) {
1245
- const issue = analyzeTriggerBucket(normalized, claims);
1246
- if (issue) issues.push(issue);
1148
+ for (const orphan of orphans) {
1149
+ const expectedMdRelative = orphan.relativePath.endsWith(".sm") ? `${orphan.relativePath.slice(0, -".sm".length)}.md` : `${orphan.relativePath}.md`;
1150
+ issues.push({
1151
+ analyzerId: ID7,
1152
+ severity: "warn",
1153
+ nodeIds: [expectedMdRelative],
1154
+ message: tx(ANNOTATION_ORPHAN_TEXTS.message, {
1155
+ sidecarPath: orphan.relativePath,
1156
+ expectedMdPath: orphan.expectedMdPath
1157
+ }),
1158
+ data: {
1159
+ sidecarPath: orphan.relativePath,
1160
+ expectedMdPath: orphan.expectedMdPath
1161
+ }
1162
+ });
1247
1163
  }
1248
1164
  return issues;
1249
1165
  }
1250
1166
  };
1251
- function analyzeTriggerBucket(normalized, claims) {
1252
- const advertiserPaths = [
1253
- ...new Set(claims.filter((c) => c.kind === "advertiser").map((c) => c.token))
1254
- ].sort();
1255
- const invocationTargets = [
1256
- ...new Set(claims.filter((c) => c.kind === "invocation").map((c) => c.token))
1257
- ].sort();
1258
- const advertisers = claims.filter(
1259
- (c) => c.kind === "advertiser"
1260
- );
1261
- const advertiserAmbiguous = advertiserPaths.length >= 2;
1262
- const invocationAmbiguous = invocationTargets.length >= 2;
1263
- const canonicalForms = new Set(advertisers.map((a) => a.canonicalForm));
1264
- const nonCanonicalInvocations = invocationTargets.filter((t) => !canonicalForms.has(t));
1265
- const crossKindAmbiguous = advertiserPaths.length === 1 && nonCanonicalInvocations.length >= 1;
1266
- if (!advertiserAmbiguous && !invocationAmbiguous && !crossKindAmbiguous) {
1267
- return null;
1268
- }
1269
- const nodeIds = [...new Set(claims.map((c) => c.nodeId))].sort();
1270
- const parts = [];
1271
- if (advertiserAmbiguous) {
1272
- parts.push(
1273
- tx(TRIGGER_COLLISION_TEXTS.partAdvertisers, {
1274
- count: advertiserPaths.length,
1275
- paths: advertiserPaths.join(", ")
1276
- })
1277
- );
1167
+
1168
+ // plugins/core/analyzers/annotation-stale/text.ts
1169
+ var ANNOTATION_STALE_TEXTS = {
1170
+ /** body changed since last bump */
1171
+ bodyDrift: "{{path}}: sidecar `.sm` is stale (body changed since last bump).",
1172
+ /** frontmatter changed since last bump */
1173
+ frontmatterDrift: "{{path}}: sidecar `.sm` is stale (frontmatter changed since last bump).",
1174
+ /** both body and frontmatter changed */
1175
+ bothDrift: "{{path}}: sidecar `.sm` is stale (body and frontmatter changed since last bump).",
1176
+ // Tooltips for the `card.footer.right` clock chip emitted alongside
1177
+ // the issue. Lists only the drifted face(s), in-sync faces are
1178
+ // omitted so the operator immediately sees what's modified without
1179
+ // scanning prose. No `{{path}}` placeholder, the chip already sits
1180
+ // on the affected node. The hint `sm bump <path>` keeps `<path>` as
1181
+ // a literal placeholder the operator substitutes.
1182
+ bodyTooltip: "Sidecar drift since last bump:\n \u2022 body\nRun `sm bump <path>` to refresh.",
1183
+ frontmatterTooltip: "Sidecar drift since last bump:\n \u2022 frontmatter\nRun `sm bump <path>` to refresh.",
1184
+ bothTooltip: "Sidecar drift since last bump:\n \u2022 body\n \u2022 frontmatter\nRun `sm bump <path>` to refresh."
1185
+ };
1186
+
1187
+ // plugins/core/analyzers/annotation-stale/index.ts
1188
+ var ID8 = "annotation-stale";
1189
+ var annotationStaleAnalyzer = {
1190
+ id: ID8,
1191
+ pluginId: "core",
1192
+ kind: "analyzer",
1193
+ version: "1.0.0",
1194
+ description: "Detects and marks sidecars (`.sm`) out of date of their `.md`.",
1195
+ mode: "deterministic",
1196
+ // The natural fix is to bump the node: refreshes `for` hashes,
1197
+ // increments `annotations.version`, and stamps the audit block. The
1198
+ // UI surfaces `core/bump` in the node inspector under "Recommended
1199
+ // for issues" whenever this analyzer fires.
1200
+ ui: {
1201
+ // A `pi-clock` chip in the footer-right cluster so the operator
1202
+ // spots drift in the list / inspector view (and on the graph card
1203
+ // body). Emitted with `value: 0` and `emitWhenEmpty: true` so the
1204
+ // renderer treats it as icon-only, drift severity is binary at
1205
+ // this surface (the tooltip carries the per-face detail body /
1206
+ // frontmatter / both). The corner badge on `graph.node.alert` was
1207
+ // dropped on purpose: a tooltip on the footer chip is enough, and
1208
+ // the corner badge stacked on top of broken-ref / unknown-field
1209
+ // alerts produced visual noise.
1210
+ staleIcon: {
1211
+ slot: "card.footer.right",
1212
+ icon: "pi-clock",
1213
+ emitWhenEmpty: true,
1214
+ priority: 20
1215
+ }
1216
+ },
1217
+ evaluate(ctx) {
1218
+ const issues = [];
1219
+ for (const node of ctx.nodes) {
1220
+ const status = node.sidecar?.status;
1221
+ if (status === void 0 || status === null) continue;
1222
+ if (status === "fresh") continue;
1223
+ 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 });
1224
+ issues.push({
1225
+ analyzerId: ID8,
1226
+ severity: "warn",
1227
+ nodeIds: [node.path],
1228
+ message,
1229
+ data: { status }
1230
+ });
1231
+ ctx.emitContribution(node.path, "staleIcon", {
1232
+ value: 0,
1233
+ severity: "warn",
1234
+ tooltip: tooltipFor(status)
1235
+ });
1236
+ }
1237
+ return issues;
1278
1238
  }
1279
- if (invocationAmbiguous) {
1280
- parts.push(
1281
- tx(TRIGGER_COLLISION_TEXTS.partInvocations, {
1282
- count: invocationTargets.length,
1283
- forms: invocationTargets.join(", ")
1284
- })
1285
- );
1286
- } else if (crossKindAmbiguous) {
1287
- const template = nonCanonicalInvocations.length > 1 ? TRIGGER_COLLISION_TEXTS.partNonCanonicalPlural : TRIGGER_COLLISION_TEXTS.partNonCanonicalSingular;
1288
- parts.push(
1289
- tx(template, {
1290
- forms: nonCanonicalInvocations.join(", "),
1291
- advertiser: advertiserPaths[0]
1292
- })
1293
- );
1239
+ };
1240
+ function tooltipFor(status) {
1241
+ switch (status) {
1242
+ case "stale-body":
1243
+ return ANNOTATION_STALE_TEXTS.bodyTooltip;
1244
+ case "stale-frontmatter":
1245
+ return ANNOTATION_STALE_TEXTS.frontmatterTooltip;
1246
+ case "stale-both":
1247
+ return ANNOTATION_STALE_TEXTS.bothTooltip;
1294
1248
  }
1295
- const message = parts.length === 2 ? tx(TRIGGER_COLLISION_TEXTS.messageTwoParts, {
1296
- normalized,
1297
- first: parts[0],
1298
- second: parts[1]
1299
- }) : tx(TRIGGER_COLLISION_TEXTS.messageOnePart, {
1300
- normalized,
1301
- part: parts[0]
1302
- });
1303
- return {
1304
- analyzerId: ID8,
1305
- severity: "error",
1306
- nodeIds,
1307
- message,
1308
- data: {
1309
- normalizedTrigger: normalized,
1310
- invocationTargets,
1311
- advertiserPaths
1312
- }
1313
- };
1314
1249
  }
1315
1250
 
1316
- // built-in-plugins/analyzers/broken-ref/index.ts
1317
- import { resolve } from "path";
1251
+ // plugins/core/analyzers/broken-ref/index.ts
1252
+ import { posix as pathPosix2, resolve } from "path";
1318
1253
 
1319
- // built-in-plugins/i18n/broken-ref.texts.ts
1254
+ // plugins/core/analyzers/broken-ref/text.ts
1320
1255
  var BROKEN_REF_TEXTS = {
1321
1256
  /** `Broken <kind> reference from <source> → <target>` */
1322
1257
  message: "Broken {{kind}} reference from {{source}} \u2192 {{target}}",
1323
1258
  // Tooltips for the per-node view-contribution badges. Singular vs
1324
1259
  // plural keeps the count grammar correct without a sub-template.
1325
1260
  alertTooltipSingle: "This node has a broken reference. Open the inspector for details.",
1326
- alertTooltipMany: "This node has {{count}} broken references. Open the inspector for details."
1261
+ alertTooltipMany: "This node has {{count}} broken references. Open the inspector for details.",
1262
+ // Fix-summary copy when the broken trigger has a same-named file on
1263
+ // disk that does not advertise `name:` in its frontmatter. Two
1264
+ // variants for single vs multiple candidates; same template family
1265
+ // as the alert tooltips above.
1266
+ hintSummarySingle: "Add `name: {{name}}` to the frontmatter of {{candidate}} so this reference resolves.",
1267
+ hintSummaryMany: "Add `name: {{name}}` to the frontmatter of one of these files so this reference resolves: {{candidates}}."
1327
1268
  };
1328
1269
 
1329
- // built-in-plugins/analyzers/broken-ref/index.ts
1270
+ // plugins/core/analyzers/broken-ref/index.ts
1330
1271
  var ID9 = "broken-ref";
1331
1272
  var brokenRefAnalyzer = {
1332
1273
  id: ID9,
@@ -1334,9 +1275,8 @@ var brokenRefAnalyzer = {
1334
1275
  kind: "analyzer",
1335
1276
  version: "1.0.0",
1336
1277
  description: "Detects and flags arrows pointing at a node not part of the current scan.",
1337
- stability: "stable",
1338
1278
  mode: "deterministic",
1339
- viewContributions: {
1279
+ ui: {
1340
1280
  // Corner badge on the graph card; count omitted when there is a
1341
1281
  // single broken ref (avoids a noisy "icon + 1" chip).
1342
1282
  alert: {
@@ -1364,13 +1304,15 @@ var brokenRefAnalyzer = {
1364
1304
  evaluate(ctx) {
1365
1305
  const byPath3 = new Set(ctx.nodes.map((n) => n.path));
1366
1306
  const byNormalizedName = indexByNormalizedName(ctx.nodes);
1307
+ const byBasenameWithoutName = indexByBasenameWithoutName(ctx.nodes);
1367
1308
  const refIndex = ctx.referenceablePaths && ctx.referenceablePaths.size > 0 && ctx.cwd ? { paths: ctx.referenceablePaths, cwd: ctx.cwd } : null;
1368
1309
  const issues = [];
1369
1310
  const perNode = /* @__PURE__ */ new Map();
1370
1311
  for (const link2 of ctx.links) {
1371
1312
  if (isResolved(link2, byPath3, byNormalizedName)) continue;
1372
1313
  if (refIndex && resolvesViaReferencePaths(link2, refIndex)) continue;
1373
- issues.push(buildIssue(link2));
1314
+ const candidates = findHintCandidates(link2, byBasenameWithoutName);
1315
+ issues.push(buildIssue(link2, candidates));
1374
1316
  perNode.set(link2.source, (perNode.get(link2.source) ?? 0) + 1);
1375
1317
  }
1376
1318
  for (const [nodePath, count] of perNode) {
@@ -1390,8 +1332,13 @@ var brokenRefAnalyzer = {
1390
1332
  return issues;
1391
1333
  }
1392
1334
  };
1393
- function buildIssue(link2) {
1394
- return {
1335
+ function buildIssue(link2, hintCandidates = []) {
1336
+ const data = {
1337
+ target: link2.target,
1338
+ kind: link2.kind,
1339
+ trigger: link2.trigger?.normalizedTrigger ?? null
1340
+ };
1341
+ const issue = {
1395
1342
  analyzerId: ID9,
1396
1343
  severity: "warn",
1397
1344
  nodeIds: [link2.source],
@@ -1400,12 +1347,28 @@ function buildIssue(link2) {
1400
1347
  source: link2.source,
1401
1348
  target: link2.target
1402
1349
  }),
1403
- data: {
1404
- target: link2.target,
1405
- kind: link2.kind,
1406
- trigger: link2.trigger?.normalizedTrigger ?? null
1407
- }
1350
+ data
1408
1351
  };
1352
+ if (hintCandidates.length > 0) {
1353
+ const suggestedName = (link2.trigger?.normalizedTrigger ?? "").replace(/^[/@]/, "").trim();
1354
+ const candidatePaths = hintCandidates.map((n) => n.path);
1355
+ data["hint"] = {
1356
+ kind: "missing-frontmatter-name",
1357
+ suggestedName,
1358
+ candidates: candidatePaths
1359
+ };
1360
+ issue.fix = {
1361
+ summary: candidatePaths.length === 1 ? tx(BROKEN_REF_TEXTS.hintSummarySingle, {
1362
+ name: suggestedName,
1363
+ candidate: candidatePaths[0]
1364
+ }) : tx(BROKEN_REF_TEXTS.hintSummaryMany, {
1365
+ name: suggestedName,
1366
+ candidates: candidatePaths.join(", ")
1367
+ }),
1368
+ autofixable: false
1369
+ };
1370
+ }
1371
+ return issue;
1409
1372
  }
1410
1373
  function resolvesViaReferencePaths(link2, refIndex) {
1411
1374
  if (!isPathStyleLink(link2)) return false;
@@ -1424,6 +1387,36 @@ function indexByNormalizedName(nodes) {
1424
1387
  }
1425
1388
  return out;
1426
1389
  }
1390
+ function basenameWithoutExt(path) {
1391
+ const base = pathPosix2.basename(path);
1392
+ const ext = pathPosix2.extname(base);
1393
+ return ext ? base.slice(0, -ext.length) : base;
1394
+ }
1395
+ function indexByBasenameWithoutName(nodes) {
1396
+ const out = /* @__PURE__ */ new Map();
1397
+ for (const node of nodes) {
1398
+ const raw = node.frontmatter?.["name"];
1399
+ const name = typeof raw === "string" ? raw : "";
1400
+ if (name) continue;
1401
+ const bare = basenameWithoutExt(node.path);
1402
+ if (!bare) continue;
1403
+ const key = normalizeTrigger(bare);
1404
+ if (!key) continue;
1405
+ const bucket = out.get(key) ?? [];
1406
+ bucket.push(node);
1407
+ out.set(key, bucket);
1408
+ }
1409
+ return out;
1410
+ }
1411
+ function findHintCandidates(link2, idx) {
1412
+ const normalized = link2.trigger?.normalizedTrigger;
1413
+ if (!normalized) return [];
1414
+ const sigil = normalized.charAt(0);
1415
+ if (sigil !== "/" && sigil !== "@") return [];
1416
+ const withoutSigil = normalized.slice(1).trim();
1417
+ if (!withoutSigil) return [];
1418
+ return idx.get(withoutSigil) ?? [];
1419
+ }
1427
1420
  function isResolved(link2, byPath3, byNormalizedName) {
1428
1421
  const normalized = link2.trigger?.normalizedTrigger;
1429
1422
  if (normalized) {
@@ -1439,66 +1432,69 @@ function isPathStyleLink(link2) {
1439
1432
  return true;
1440
1433
  }
1441
1434
 
1442
- // built-in-plugins/i18n/superseded.texts.ts
1443
- var SUPERSEDED_TEXTS = {
1444
- /** `<path> is superseded by <supersededBy>` */
1445
- message: "{{path}} is superseded by {{supersededBy}}"
1435
+ // plugins/core/analyzers/contribution-orphan/index.ts
1436
+ var ID10 = "contribution-orphan";
1437
+ var contributionOrphanAnalyzer = {
1438
+ id: ID10,
1439
+ pluginId: "core",
1440
+ kind: "analyzer",
1441
+ version: "0.0.0",
1442
+ description: "Detects and warns about plugin data referencing nodes renamed or deleted in the latest scan.",
1443
+ mode: "deterministic",
1444
+ evaluate(_ctx) {
1445
+ return [];
1446
+ }
1446
1447
  };
1447
1448
 
1448
- // built-in-plugins/analyzers/superseded/index.ts
1449
- var ID10 = "superseded";
1450
- var supersededAnalyzer = {
1451
- id: ID10,
1449
+ // plugins/core/analyzers/job-orphan-file/text.ts
1450
+ var JOB_ORPHAN_FILE_TEXTS = {
1451
+ /**
1452
+ * `<path>.md` lives under `.skill-map/jobs/` but no `state_jobs.filePath`
1453
+ * row references it. Run `sm job prune --orphan-files` to remove.
1454
+ */
1455
+ message: "Orphan job file: {{filePath}} is not referenced by any state_jobs row. Run `sm job prune --orphan-files` to remove it."
1456
+ };
1457
+
1458
+ // plugins/core/analyzers/job-orphan-file/index.ts
1459
+ var ID11 = "job-orphan-file";
1460
+ var jobOrphanFileAnalyzer = {
1461
+ id: ID11,
1452
1462
  pluginId: "core",
1453
1463
  kind: "analyzer",
1454
1464
  version: "1.0.0",
1455
- description: "Detects and marks nodes replaced by a newer one via `supersededBy`.",
1456
- stability: "stable",
1465
+ description: "Detects and flags leftover job result files (no live job references them). Cleanup via `sm job prune --orphan-files`.",
1457
1466
  mode: "deterministic",
1458
1467
  evaluate(ctx) {
1468
+ const orphans = ctx.orphanJobFiles;
1469
+ if (!orphans || orphans.length === 0) return [];
1459
1470
  const issues = [];
1460
- for (const node of ctx.nodes) {
1461
- const supersededBy = pickSupersededBy(node);
1462
- if (supersededBy === null) continue;
1471
+ for (const filePath of orphans) {
1463
1472
  issues.push({
1464
- analyzerId: ID10,
1465
- severity: "info",
1466
- nodeIds: [node.path],
1467
- message: tx(SUPERSEDED_TEXTS.message, {
1468
- path: node.path,
1469
- supersededBy
1470
- }),
1471
- data: { supersededBy }
1473
+ analyzerId: ID11,
1474
+ severity: "warn",
1475
+ nodeIds: [filePath],
1476
+ message: tx(JOB_ORPHAN_FILE_TEXTS.message, { filePath }),
1477
+ data: { filePath }
1472
1478
  });
1473
1479
  }
1474
1480
  return issues;
1475
1481
  }
1476
1482
  };
1477
- function pickSupersededBy(node) {
1478
- const sidecar = node.sidecar;
1479
- if (!sidecar || sidecar.present !== true) return null;
1480
- const ann = sidecar.annotations;
1481
- if (!ann || typeof ann !== "object" || Array.isArray(ann)) return null;
1482
- const value = ann["supersededBy"];
1483
- if (typeof value !== "string" || value.length === 0) return null;
1484
- return value;
1485
- }
1486
1483
 
1487
- // built-in-plugins/i18n/link-conflict.texts.ts
1484
+ // plugins/core/analyzers/link-conflict/text.ts
1488
1485
  var LINK_CONFLICT_TEXTS = {
1489
1486
  /** `Detectors disagree on link kind for <source> → <target> (<kindList>)` */
1490
1487
  message: "Detectors disagree on link kind for {{source}} \u2192 {{target}} ({{kindList}})"
1491
1488
  };
1492
1489
 
1493
- // built-in-plugins/analyzers/link-conflict/index.ts
1494
- var ID11 = "link-conflict";
1490
+ // plugins/core/analyzers/link-conflict/index.ts
1491
+ var ID12 = "link-conflict";
1495
1492
  var linkConflictAnalyzer = {
1496
- id: ID11,
1493
+ id: ID12,
1497
1494
  pluginId: "core",
1498
1495
  kind: "analyzer",
1499
1496
  version: "1.0.0",
1500
1497
  description: 'Detects and flags conflicting arrow meanings between extractors (e.g. "references" vs "invokes").',
1501
- stability: "stable",
1502
1498
  mode: "deterministic",
1503
1499
  // Bucket links by (source, target), then per-bucket detect distinct
1504
1500
  // kinds. The branching is intrinsic to the per-bucket conflict
@@ -1542,7 +1538,7 @@ var linkConflictAnalyzer = {
1542
1538
  const [source, target] = key.split("\0");
1543
1539
  const kindList = variants.map((v) => v.kind).join(" / ");
1544
1540
  issues.push({
1545
- analyzerId: ID11,
1541
+ analyzerId: ID12,
1546
1542
  severity: "warn",
1547
1543
  nodeIds: [source, target],
1548
1544
  message: tx(LINK_CONFLICT_TEXTS.message, {
@@ -1567,168 +1563,386 @@ function rankConfidence(c) {
1567
1563
  }
1568
1564
  }
1569
1565
 
1570
- // built-in-plugins/i18n/annotation-stale.texts.ts
1571
- var ANNOTATION_STALE_TEXTS = {
1572
- /** body changed since last bump */
1573
- bodyDrift: "{{path}}: sidecar `.sm` is stale (body changed since last bump).",
1574
- /** frontmatter changed since last bump */
1575
- frontmatterDrift: "{{path}}: sidecar `.sm` is stale (frontmatter changed since last bump).",
1576
- /** both body and frontmatter changed */
1577
- bothDrift: "{{path}}: sidecar `.sm` is stale (body and frontmatter changed since last bump).",
1578
- // Tooltips for the `card.footer.right` clock chip emitted alongside
1579
- // the issue. Lists only the drifted face(s), in-sync faces are
1580
- // omitted so the operator immediately sees what's modified without
1581
- // scanning prose. No `{{path}}` placeholder, the chip already sits
1582
- // on the affected node. The hint `sm bump <path>` keeps `<path>` as
1583
- // a literal placeholder the operator substitutes.
1584
- bodyTooltip: "Sidecar drift since last bump:\n \u2022 body\nRun `sm bump <path>` to refresh.",
1585
- frontmatterTooltip: "Sidecar drift since last bump:\n \u2022 frontmatter\nRun `sm bump <path>` to refresh.",
1586
- bothTooltip: "Sidecar drift since last bump:\n \u2022 body\n \u2022 frontmatter\nRun `sm bump <path>` to refresh."
1587
- };
1588
-
1589
- // built-in-plugins/analyzers/annotation-stale/index.ts
1590
- var ID12 = "annotation-stale";
1591
- var annotationStaleAnalyzer = {
1592
- id: ID12,
1593
- pluginId: "core",
1594
- kind: "analyzer",
1595
- version: "1.0.0",
1596
- description: "Detects and marks sidecars (`.sm`) out of date of their `.md`.",
1597
- stability: "stable",
1598
- mode: "deterministic",
1599
- // The natural fix is to bump the node: refreshes `for` hashes,
1600
- // increments `annotations.version`, and stamps the audit block. The
1601
- // UI surfaces `core/bump` in the node inspector under "Recommended
1602
- // for issues" whenever this analyzer fires.
1603
- recommendedActions: ["core/bump"],
1604
- viewContributions: {
1605
- // A `pi-clock` chip in the footer-right cluster so the operator
1606
- // spots drift in the list / inspector view (and on the graph card
1607
- // body). Emitted with `value: 0` and `emitWhenEmpty: true` so the
1608
- // renderer treats it as icon-only, drift severity is binary at
1609
- // this surface (the tooltip carries the per-face detail body /
1610
- // frontmatter / both). The corner badge on `graph.node.alert` was
1611
- // dropped on purpose: a tooltip on the footer chip is enough, and
1612
- // the corner badge stacked on top of broken-ref / unknown-field
1613
- // alerts produced visual noise.
1614
- staleIcon: {
1615
- slot: "card.footer.right",
1616
- icon: "pi-clock",
1617
- emitWhenEmpty: true,
1566
+ // kernel/util/trigger-resolve.ts
1567
+ function buildNameIndex(nodes) {
1568
+ const out = /* @__PURE__ */ new Map();
1569
+ indexByCanonicalName(nodes, out);
1570
+ fillIndexWithPathBasename(nodes, out);
1571
+ return out;
1572
+ }
1573
+ function indexByCanonicalName(nodes, out) {
1574
+ for (const node of nodes) {
1575
+ const raw = canonicalName(node);
1576
+ if (raw === null) continue;
1577
+ const key = normalizeTrigger(raw);
1578
+ if (!out.has(key)) out.set(key, node.path);
1579
+ }
1580
+ }
1581
+ function fillIndexWithPathBasename(nodes, out) {
1582
+ for (const node of nodes) {
1583
+ if (canonicalName(node) !== null) continue;
1584
+ const derived = pathBasenameForLink(node.path);
1585
+ if (derived.length === 0) continue;
1586
+ const key = normalizeTrigger(derived);
1587
+ if (!out.has(key)) out.set(key, node.path);
1588
+ }
1589
+ }
1590
+ function canonicalName(node) {
1591
+ const raw = node.frontmatter?.["name"];
1592
+ if (typeof raw !== "string" || raw.length === 0) return null;
1593
+ return raw;
1594
+ }
1595
+ function pathBasenameForLink(path) {
1596
+ const segments = path.split("/").filter((s) => s.length > 0);
1597
+ if (segments.length === 0) return path;
1598
+ const last = segments[segments.length - 1];
1599
+ if (last === "SKILL.md" && segments.length >= 2) {
1600
+ return segments[segments.length - 2];
1601
+ }
1602
+ return last.replace(/\.md$/, "");
1603
+ }
1604
+ function resolveLinkTargetToPath(link2, nameIndex) {
1605
+ const raw = link2.target;
1606
+ const sigil = raw.charAt(0);
1607
+ if (sigil !== "/" && sigil !== "@") return raw;
1608
+ const normalizedTrigger = link2.trigger?.normalizedTrigger;
1609
+ const normalized = typeof normalizedTrigger === "string" ? normalizedTrigger.replace(/^[/@]/, "").trim() : normalizeTrigger(raw.slice(1));
1610
+ const resolved = nameIndex.get(normalized);
1611
+ return resolved ?? raw;
1612
+ }
1613
+
1614
+ // plugins/core/analyzers/link-counts/index.ts
1615
+ var ID13 = "link-counts";
1616
+ var linkCountsAnalyzer = {
1617
+ id: ID13,
1618
+ pluginId: "core",
1619
+ kind: "analyzer",
1620
+ version: "1.0.0",
1621
+ description: "Counts incoming and outgoing links per node.",
1622
+ mode: "deterministic",
1623
+ ui: {
1624
+ linksIn: {
1625
+ slot: "card.footer.left",
1626
+ icon: "pi-download",
1627
+ label: "incoming links",
1628
+ emitWhenEmpty: false,
1629
+ priority: 10
1630
+ },
1631
+ linksOut: {
1632
+ slot: "card.footer.left",
1633
+ icon: "pi-upload",
1634
+ label: "outgoing links",
1635
+ emitWhenEmpty: false,
1618
1636
  priority: 20
1619
1637
  }
1620
1638
  },
1639
+ evaluate(ctx) {
1640
+ const nameIndex = buildNameIndex(ctx.nodes);
1641
+ const perTarget = /* @__PURE__ */ new Map();
1642
+ const perSource = /* @__PURE__ */ new Map();
1643
+ for (const link2 of ctx.links) {
1644
+ const resolvedTarget = resolveLinkTargetToPath(link2, nameIndex);
1645
+ bump(perTarget, resolvedTarget, link2.kind);
1646
+ bump(perSource, link2.source, link2.kind);
1647
+ }
1648
+ for (const node of ctx.nodes) {
1649
+ emitChip(ctx, node.path, "linksIn", perTarget.get(node.path));
1650
+ emitChip(ctx, node.path, "linksOut", perSource.get(node.path));
1651
+ }
1652
+ return [];
1653
+ }
1654
+ };
1655
+ function bump(map, key, kind) {
1656
+ let byKind = map.get(key);
1657
+ if (!byKind) {
1658
+ byKind = /* @__PURE__ */ new Map();
1659
+ map.set(key, byKind);
1660
+ }
1661
+ byKind.set(kind, (byKind.get(kind) ?? 0) + 1);
1662
+ }
1663
+ function emitChip(ctx, nodePath, contributionId, byKind) {
1664
+ if (!byKind) return;
1665
+ let total = 0;
1666
+ for (const n of byKind.values()) total += n;
1667
+ if (total === 0) return;
1668
+ const capped = Math.min(total, 99);
1669
+ const direction = contributionId === "linksIn" ? "in" : "out";
1670
+ ctx.emitContribution(nodePath, contributionId, {
1671
+ value: capped,
1672
+ tooltip: formatBreakdown(byKind, direction)
1673
+ });
1674
+ }
1675
+ function formatBreakdown(byKind, direction) {
1676
+ const lines = [...byKind.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([kind, n]) => `${kind}: ${n}`);
1677
+ return [direction, ...lines].join("\n");
1678
+ }
1679
+
1680
+ // plugins/core/analyzers/stability/index.ts
1681
+ var ID14 = "stability";
1682
+ var EXPERIMENTAL_TOOLTIP = "Experimental: API may change";
1683
+ var DEPRECATED_TOOLTIP = "Deprecated: avoid in new code";
1684
+ var stabilityAnalyzer = {
1685
+ id: ID14,
1686
+ pluginId: "core",
1687
+ kind: "analyzer",
1688
+ version: "1.0.0",
1689
+ description: "Reports node lifecycle stage (`experimental`, `deprecated`) on the card.",
1690
+ mode: "deterministic",
1691
+ ui: {
1692
+ experimental: {
1693
+ slot: "card.footer.right",
1694
+ icon: "fa-solid fa-flask",
1695
+ label: "experimental",
1696
+ emitWhenEmpty: false,
1697
+ priority: 10
1698
+ },
1699
+ deprecated: {
1700
+ slot: "card.footer.right",
1701
+ icon: "pi-ban",
1702
+ label: "deprecated",
1703
+ emitWhenEmpty: false,
1704
+ priority: 10
1705
+ }
1706
+ },
1621
1707
  evaluate(ctx) {
1622
1708
  const issues = [];
1623
1709
  for (const node of ctx.nodes) {
1624
- const status = node.sidecar?.status;
1625
- if (status === void 0 || status === null) continue;
1626
- if (status === "fresh") continue;
1627
- 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 });
1628
- issues.push({
1629
- analyzerId: ID12,
1630
- severity: "warn",
1631
- nodeIds: [node.path],
1632
- message,
1633
- data: { status }
1634
- });
1635
- ctx.emitContribution(node.path, "staleIcon", {
1636
- value: 0,
1637
- severity: "warn",
1638
- tooltip: tooltipFor(status)
1639
- });
1710
+ const stability = readStability(node);
1711
+ if (stability === "experimental") {
1712
+ ctx.emitContribution(node.path, "experimental", {
1713
+ value: 0,
1714
+ tooltip: EXPERIMENTAL_TOOLTIP
1715
+ });
1716
+ issues.push({
1717
+ analyzerId: ID14,
1718
+ severity: "info",
1719
+ nodeIds: [node.path],
1720
+ message: `Node '${node.path}' is marked experimental: API may change.`,
1721
+ data: { stability }
1722
+ });
1723
+ } else if (stability === "deprecated") {
1724
+ ctx.emitContribution(node.path, "deprecated", {
1725
+ value: 0,
1726
+ tooltip: DEPRECATED_TOOLTIP,
1727
+ severity: "warn"
1728
+ });
1729
+ issues.push({
1730
+ analyzerId: ID14,
1731
+ severity: "warn",
1732
+ nodeIds: [node.path],
1733
+ message: `Node '${node.path}' is marked deprecated: avoid in new code.`,
1734
+ data: { stability }
1735
+ });
1736
+ }
1640
1737
  }
1641
1738
  return issues;
1642
1739
  }
1643
1740
  };
1644
- function tooltipFor(status) {
1645
- switch (status) {
1646
- case "stale-body":
1647
- return ANNOTATION_STALE_TEXTS.bodyTooltip;
1648
- case "stale-frontmatter":
1649
- return ANNOTATION_STALE_TEXTS.frontmatterTooltip;
1650
- case "stale-both":
1651
- return ANNOTATION_STALE_TEXTS.bothTooltip;
1652
- }
1741
+ function readStability(node) {
1742
+ const fromAnn = node.sidecar?.annotations?.["stability"];
1743
+ if (isStability(fromAnn)) return fromAnn;
1744
+ const legacy = readLegacyMetadataStability(node.frontmatter);
1745
+ return isStability(legacy) ? legacy : null;
1746
+ }
1747
+ function readLegacyMetadataStability(fm) {
1748
+ if (!fm) return void 0;
1749
+ const meta = fm["metadata"];
1750
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0;
1751
+ return meta["stability"];
1752
+ }
1753
+ function isStability(value) {
1754
+ return value === "experimental" || value === "deprecated" || value === "stable";
1653
1755
  }
1654
1756
 
1655
- // built-in-plugins/i18n/annotation-orphan.texts.ts
1656
- var ANNOTATION_ORPHAN_TEXTS = {
1657
- /** Sidecar `<path>.sm` has no matching `<path>.md`. */
1658
- message: "Orphan sidecar: {{sidecarPath}} has no matching markdown node at {{expectedMdPath}}."
1757
+ // plugins/core/analyzers/superseded/text.ts
1758
+ var SUPERSEDED_TEXTS = {
1759
+ /** `<path> is superseded by <supersededBy>` */
1760
+ message: "{{path}} is superseded by {{supersededBy}}"
1659
1761
  };
1660
1762
 
1661
- // built-in-plugins/analyzers/annotation-orphan/index.ts
1662
- var ID13 = "annotation-orphan";
1663
- var annotationOrphanAnalyzer = {
1664
- id: ID13,
1763
+ // plugins/core/analyzers/superseded/index.ts
1764
+ var ID15 = "superseded";
1765
+ var supersededAnalyzer = {
1766
+ id: ID15,
1665
1767
  pluginId: "core",
1666
1768
  kind: "analyzer",
1667
1769
  version: "1.0.0",
1668
- description: "Detects and flags sidecars (`.sm`) whose `.md` no longer exists.",
1669
- stability: "stable",
1770
+ description: "Detects and marks nodes replaced by a newer one via `supersededBy`.",
1670
1771
  mode: "deterministic",
1671
1772
  evaluate(ctx) {
1672
- const orphans = ctx.orphanSidecars;
1673
- if (!orphans || orphans.length === 0) return [];
1674
1773
  const issues = [];
1675
- for (const orphan of orphans) {
1676
- const expectedMdRelative = orphan.relativePath.endsWith(".sm") ? `${orphan.relativePath.slice(0, -".sm".length)}.md` : `${orphan.relativePath}.md`;
1774
+ for (const node of ctx.nodes) {
1775
+ const supersededBy = pickSupersededBy(node);
1776
+ if (supersededBy === null) continue;
1677
1777
  issues.push({
1678
- analyzerId: ID13,
1679
- severity: "warn",
1680
- nodeIds: [expectedMdRelative],
1681
- message: tx(ANNOTATION_ORPHAN_TEXTS.message, {
1682
- sidecarPath: orphan.relativePath,
1683
- expectedMdPath: orphan.expectedMdPath
1778
+ analyzerId: ID15,
1779
+ severity: "info",
1780
+ nodeIds: [node.path],
1781
+ message: tx(SUPERSEDED_TEXTS.message, {
1782
+ path: node.path,
1783
+ supersededBy
1684
1784
  }),
1685
- data: {
1686
- sidecarPath: orphan.relativePath,
1687
- expectedMdPath: orphan.expectedMdPath
1688
- }
1785
+ data: { supersededBy }
1689
1786
  });
1690
1787
  }
1691
1788
  return issues;
1692
1789
  }
1693
1790
  };
1791
+ function pickSupersededBy(node) {
1792
+ const sidecar = node.sidecar;
1793
+ if (!sidecar || sidecar.present !== true) return null;
1794
+ const ann = sidecar.annotations;
1795
+ if (!ann || typeof ann !== "object" || Array.isArray(ann)) return null;
1796
+ const value = ann["supersededBy"];
1797
+ if (typeof value !== "string" || value.length === 0) return null;
1798
+ return value;
1799
+ }
1694
1800
 
1695
- // built-in-plugins/i18n/job-orphan-file.texts.ts
1696
- var JOB_ORPHAN_FILE_TEXTS = {
1801
+ // plugins/core/analyzers/trigger-collision/text.ts
1802
+ var TRIGGER_COLLISION_TEXTS = {
1697
1803
  /**
1698
- * `<path>.md` lives under `.skill-map/jobs/` but no `state_jobs.filePath`
1699
- * row references it. Run `sm job prune --orphan-files` to remove.
1804
+ * Top-level message when `analyzeTriggerBucket` accumulated exactly one
1805
+ * cause part. Used for the advertiser-ambiguous-only, invocation-
1806
+ * ambiguous-only, and cross-kind-only branches.
1700
1807
  */
1701
- message: "Orphan job file: {{filePath}} is not referenced by any state_jobs row. Run `sm job prune --orphan-files` to remove it."
1808
+ messageOnePart: 'Trigger "{{normalized}}" has {{part}}.',
1809
+ /**
1810
+ * Top-level message when `analyzeTriggerBucket` accumulated two cause
1811
+ * parts (advertiser-ambiguous AND invocation-ambiguous fire together).
1812
+ * The joiner lives inside the template so future locales can adapt it
1813
+ * (e.g. `'; y '` in Spanish) without touching the rule code.
1814
+ */
1815
+ messageTwoParts: 'Trigger "{{normalized}}" has {{first}}; and {{second}}.',
1816
+ /** `<n> nodes advertise it: <list>` part, fires on the advertiser-ambiguous branch. */
1817
+ partAdvertisers: "{{count}} nodes advertise it: {{paths}}",
1818
+ /** `<n> distinct invocation forms: <list>` part, fires on the invocation-ambiguous branch. */
1819
+ partInvocations: "{{count}} distinct invocation forms: {{forms}}",
1820
+ /** Singular cross-kind cause: `non-canonical invocation <form> against advertiser <path>`. */
1821
+ partNonCanonicalSingular: "non-canonical invocation {{forms}} against advertiser {{advertiser}}",
1822
+ /** Plural cross-kind cause: `non-canonical invocations <forms> against advertiser <path>`. */
1823
+ partNonCanonicalPlural: "non-canonical invocations {{forms}} against advertiser {{advertiser}}"
1702
1824
  };
1703
1825
 
1704
- // built-in-plugins/analyzers/job-orphan-file/index.ts
1705
- var ID14 = "job-orphan-file";
1706
- var jobOrphanFileAnalyzer = {
1707
- id: ID14,
1826
+ // plugins/core/analyzers/trigger-collision/index.ts
1827
+ var ID16 = "trigger-collision";
1828
+ var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
1829
+ "command",
1830
+ "skill",
1831
+ "agent"
1832
+ ]);
1833
+ var triggerCollisionAnalyzer = {
1834
+ id: ID16,
1708
1835
  pluginId: "core",
1709
1836
  kind: "analyzer",
1710
- version: "1.0.0",
1711
- description: "Detects and flags leftover job result files (no live job references them). Cleanup via `sm job prune --orphan-files`.",
1712
- stability: "stable",
1713
1837
  mode: "deterministic",
1838
+ version: "1.0.0",
1839
+ description: "Detects and flags two or more nodes claiming the same `/command` or `@agent` name.",
1840
+ // Two claim-collection passes (advertisement + invocation) feeding
1841
+ // the bucket map. Per-bucket analysis lives in `analyzeTriggerBucket`.
1842
+ // eslint-disable-next-line complexity
1714
1843
  evaluate(ctx) {
1715
- const orphans = ctx.orphanJobFiles;
1716
- if (!orphans || orphans.length === 0) return [];
1717
- const issues = [];
1718
- for (const filePath of orphans) {
1719
- issues.push({
1720
- analyzerId: ID14,
1721
- severity: "warn",
1722
- nodeIds: [filePath],
1723
- message: tx(JOB_ORPHAN_FILE_TEXTS.message, { filePath }),
1724
- data: { filePath }
1844
+ const buckets = /* @__PURE__ */ new Map();
1845
+ const push = (key, claim) => {
1846
+ const bucket = buckets.get(key) ?? [];
1847
+ bucket.push(claim);
1848
+ buckets.set(key, bucket);
1849
+ };
1850
+ for (const node of ctx.nodes) {
1851
+ if (!ADVERTISING_KINDS.has(node.kind)) continue;
1852
+ const raw = node.frontmatter?.["name"];
1853
+ if (typeof raw !== "string" || raw.length === 0) continue;
1854
+ const normalized = `/${normalizeTrigger(raw)}`;
1855
+ if (normalized === "/") continue;
1856
+ push(normalized, {
1857
+ kind: "advertiser",
1858
+ token: node.path,
1859
+ nodeId: node.path,
1860
+ canonicalForm: `/${raw}`
1861
+ });
1862
+ }
1863
+ for (const link2 of ctx.links) {
1864
+ const normalized = link2.trigger?.normalizedTrigger;
1865
+ if (!normalized) continue;
1866
+ push(normalized, {
1867
+ kind: "invocation",
1868
+ token: link2.target,
1869
+ nodeId: link2.source
1725
1870
  });
1726
1871
  }
1872
+ const issues = [];
1873
+ for (const [normalized, claims] of buckets) {
1874
+ const issue = analyzeTriggerBucket(normalized, claims);
1875
+ if (issue) issues.push(issue);
1876
+ }
1727
1877
  return issues;
1728
1878
  }
1729
1879
  };
1880
+ function analyzeTriggerBucket(normalized, claims) {
1881
+ const advertiserPaths = [
1882
+ ...new Set(claims.filter((c) => c.kind === "advertiser").map((c) => c.token))
1883
+ ].sort();
1884
+ const invocationTargets = [
1885
+ ...new Set(claims.filter((c) => c.kind === "invocation").map((c) => c.token))
1886
+ ].sort();
1887
+ const advertisers = claims.filter(
1888
+ (c) => c.kind === "advertiser"
1889
+ );
1890
+ const advertiserAmbiguous = advertiserPaths.length >= 2;
1891
+ const invocationAmbiguous = invocationTargets.length >= 2;
1892
+ const canonicalForms = new Set(advertisers.map((a) => a.canonicalForm));
1893
+ const nonCanonicalInvocations = invocationTargets.filter((t) => !canonicalForms.has(t));
1894
+ const crossKindAmbiguous = advertiserPaths.length === 1 && nonCanonicalInvocations.length >= 1;
1895
+ if (!advertiserAmbiguous && !invocationAmbiguous && !crossKindAmbiguous) {
1896
+ return null;
1897
+ }
1898
+ const nodeIds = [...new Set(claims.map((c) => c.nodeId))].sort();
1899
+ const parts = [];
1900
+ if (advertiserAmbiguous) {
1901
+ parts.push(
1902
+ tx(TRIGGER_COLLISION_TEXTS.partAdvertisers, {
1903
+ count: advertiserPaths.length,
1904
+ paths: advertiserPaths.join(", ")
1905
+ })
1906
+ );
1907
+ }
1908
+ if (invocationAmbiguous) {
1909
+ parts.push(
1910
+ tx(TRIGGER_COLLISION_TEXTS.partInvocations, {
1911
+ count: invocationTargets.length,
1912
+ forms: invocationTargets.join(", ")
1913
+ })
1914
+ );
1915
+ } else if (crossKindAmbiguous) {
1916
+ const template = nonCanonicalInvocations.length > 1 ? TRIGGER_COLLISION_TEXTS.partNonCanonicalPlural : TRIGGER_COLLISION_TEXTS.partNonCanonicalSingular;
1917
+ parts.push(
1918
+ tx(template, {
1919
+ forms: nonCanonicalInvocations.join(", "),
1920
+ advertiser: advertiserPaths[0]
1921
+ })
1922
+ );
1923
+ }
1924
+ const message = parts.length === 2 ? tx(TRIGGER_COLLISION_TEXTS.messageTwoParts, {
1925
+ normalized,
1926
+ first: parts[0],
1927
+ second: parts[1]
1928
+ }) : tx(TRIGGER_COLLISION_TEXTS.messageOnePart, {
1929
+ normalized,
1930
+ part: parts[0]
1931
+ });
1932
+ return {
1933
+ analyzerId: ID16,
1934
+ severity: "error",
1935
+ nodeIds,
1936
+ message,
1937
+ data: {
1938
+ normalizedTrigger: normalized,
1939
+ invocationTargets,
1940
+ advertiserPaths
1941
+ }
1942
+ };
1943
+ }
1730
1944
 
1731
- // built-in-plugins/analyzers/unknown-field/index.ts
1945
+ // plugins/core/analyzers/unknown-field/index.ts
1732
1946
  import { readFileSync } from "fs";
1733
1947
  import { dirname, resolve as resolve2 } from "path";
1734
1948
  import { createRequire } from "module";
@@ -1741,7 +1955,7 @@ function applyAjvFormats(ajv) {
1741
1955
  addFormats(ajv);
1742
1956
  }
1743
1957
 
1744
- // built-in-plugins/i18n/unknown-field.texts.ts
1958
+ // plugins/core/analyzers/unknown-field/text.ts
1745
1959
  var UNKNOWN_FIELD_TEXTS = {
1746
1960
  /** Key inside `annotations:` is not in the curated catalog. */
1747
1961
  unknownAnnotationKey: "{{path}}: sidecar annotations contain unknown key '{{key}}' (not in annotations.schema.json catalog).",
@@ -1755,18 +1969,17 @@ var UNKNOWN_FIELD_TEXTS = {
1755
1969
  alertTooltipMany: "This node has {{count}} unknown fields in its sidecar. Open the inspector for details."
1756
1970
  };
1757
1971
 
1758
- // built-in-plugins/analyzers/unknown-field/index.ts
1759
- var ID15 = "unknown-field";
1972
+ // plugins/core/analyzers/unknown-field/index.ts
1973
+ var ID17 = "unknown-field";
1760
1974
  var RESERVED_ROOT_BLOCKS = /* @__PURE__ */ new Set(["identity", "annotations", "settings", "audit"]);
1761
1975
  var unknownFieldAnalyzer = {
1762
- id: ID15,
1976
+ id: ID17,
1763
1977
  pluginId: "core",
1764
1978
  kind: "analyzer",
1765
1979
  version: "1.0.0",
1766
1980
  description: "Detects and flags typos or unrecognized keys in sidecars (`.sm`).",
1767
- stability: "stable",
1768
1981
  mode: "deterministic",
1769
- viewContributions: {
1982
+ ui: {
1770
1983
  // Corner badge on the graph card; count omitted when there is a
1771
1984
  // single unknown field (avoids a noisy "icon + 1" chip).
1772
1985
  alert: {
@@ -1818,7 +2031,7 @@ var unknownFieldAnalyzer = {
1818
2031
  for (const key of Object.keys(annotations)) {
1819
2032
  if (!knownAnnotationKeys.has(key)) {
1820
2033
  issues.push({
1821
- analyzerId: ID15,
2034
+ analyzerId: ID17,
1822
2035
  severity: "warn",
1823
2036
  nodeIds: [node.path],
1824
2037
  message: tx(UNKNOWN_FIELD_TEXTS.unknownAnnotationKey, {
@@ -1845,7 +2058,7 @@ var unknownFieldAnalyzer = {
1845
2058
  if (validator(value)) continue;
1846
2059
  const errors = (validator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
1847
2060
  issues.push({
1848
- analyzerId: ID15,
2061
+ analyzerId: ID17,
1849
2062
  severity: "warn",
1850
2063
  nodeIds: [node.path],
1851
2064
  message: tx(UNKNOWN_FIELD_TEXTS.pluginNamespaceInvalid, {
@@ -1861,7 +2074,7 @@ var unknownFieldAnalyzer = {
1861
2074
  continue;
1862
2075
  }
1863
2076
  issues.push({
1864
- analyzerId: ID15,
2077
+ analyzerId: ID17,
1865
2078
  severity: "warn",
1866
2079
  nodeIds: [node.path],
1867
2080
  message: tx(UNKNOWN_FIELD_TEXTS.unknownRootKey, {
@@ -1922,181 +2135,15 @@ function indexNamespacedContributions(contributions) {
1922
2135
  function indexRootContributions(contributions) {
1923
2136
  const out = /* @__PURE__ */ new Set();
1924
2137
  for (const entry of contributions) {
1925
- if (entry.location === "root") out.add(entry.key);
1926
- }
1927
- return out;
1928
- }
1929
- function collectPluginIds(contributions) {
1930
- const out = /* @__PURE__ */ new Set();
1931
- for (const entry of contributions) out.add(entry.pluginId);
1932
- return out;
1933
- }
1934
-
1935
- // built-in-plugins/analyzers/contribution-orphan/index.ts
1936
- var ID16 = "contribution-orphan";
1937
- var contributionOrphanAnalyzer = {
1938
- id: ID16,
1939
- pluginId: "core",
1940
- kind: "analyzer",
1941
- version: "0.0.0",
1942
- description: "Detects and warns about plugin data referencing nodes renamed or deleted in the latest scan.",
1943
- stability: "experimental",
1944
- mode: "deterministic",
1945
- evaluate(_ctx) {
1946
- return [];
1947
- }
1948
- };
1949
-
1950
- // kernel/util/safe-text.ts
1951
- var ANSI_ESCAPE_RE = /[›][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
1952
- var C0_CONTROL_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
1953
- function sanitizeForTerminal(text) {
1954
- return text.replace(ANSI_ESCAPE_RE, "").replace(C0_CONTROL_RE, "");
1955
- }
1956
-
1957
- // built-in-plugins/i18n/ascii.texts.ts
1958
- var ASCII_FORMATTER_TEXTS = {
1959
- /** Header line: `skill-map graph: N nodes, M links, K issues`. */
1960
- header: "skill-map graph: {{nodes}} nodes, {{links}} links, {{issues}} issues",
1961
- /** Per-node-kind section header: `## <kind> (<count>)`. */
1962
- kindSectionHeader: "## {{kind}} ({{count}})",
1963
- /** Plain node bullet: `- <path>`. */
1964
- nodeBullet: "- {{path}}",
1965
- /** Node bullet with title suffix: `- <path>: "<title>"`. */
1966
- nodeBulletWithTitle: '- {{path}}: "{{title}}"',
1967
- /** `## links (<count>)` section header. */
1968
- linksSectionHeader: "## links ({{count}})",
1969
- /** Link bullet: `- <source> --<kind>--> <target> [<confidence>]`. */
1970
- linkBullet: "- {{source}} --{{kind}}--> {{target}} [{{confidence}}]",
1971
- /** `## issues (<count>)` section header. */
1972
- issuesSectionHeader: "## issues ({{count}})",
1973
- /** Issue bullet: `- [<severity>] <analyzerId>: <message>`. */
1974
- issueBullet: "- [{{severity}}] {{analyzerId}}: {{message}}"
1975
- };
1976
-
1977
- // built-in-plugins/formatters/ascii/index.ts
1978
- var ID17 = "ascii";
1979
- var KIND_ORDER = ["agent", "command", "skill", "markdown"];
1980
- var asciiFormatter = {
1981
- id: ID17,
1982
- pluginId: "core",
1983
- kind: "formatter",
1984
- version: "1.0.0",
1985
- description: "Renders the scan as plain text, grouped by kind, arrows, and issues. Used by `sm scan --format=ascii`.",
1986
- stability: "stable",
1987
- formatId: "ascii",
1988
- // ASCII tree formatter, header + per-kind sections + per-issue
1989
- // section. Each section iterates and renders; splitting per section
1990
- // would multiply the for-loop boilerplate.
1991
- // eslint-disable-next-line complexity
1992
- format(ctx) {
1993
- const out = [];
1994
- out.push(
1995
- tx(ASCII_FORMATTER_TEXTS.header, {
1996
- nodes: ctx.nodes.length,
1997
- links: ctx.links.length,
1998
- issues: ctx.issues.length
1999
- }),
2000
- ""
2001
- );
2002
- const byKind = /* @__PURE__ */ new Map();
2003
- for (const node of ctx.nodes) {
2004
- if (!byKind.has(node.kind)) byKind.set(node.kind, []);
2005
- byKind.get(node.kind).push(node);
2006
- }
2007
- const renderedKinds = /* @__PURE__ */ new Set();
2008
- for (const kind of KIND_ORDER) {
2009
- const group = byKind.get(kind);
2010
- if (!group || group.length === 0) continue;
2011
- renderSection(out, kind, group);
2012
- renderedKinds.add(kind);
2013
- }
2014
- const extraKinds = [...byKind.keys()].filter((k) => !renderedKinds.has(k)).sort();
2015
- for (const kind of extraKinds) {
2016
- const group = byKind.get(kind);
2017
- if (!group || group.length === 0) continue;
2018
- renderSection(out, kind, group);
2019
- }
2020
- if (ctx.links.length > 0) {
2021
- out.push(tx(ASCII_FORMATTER_TEXTS.linksSectionHeader, { count: ctx.links.length }));
2022
- const sorted = [...ctx.links].sort((a, b) => {
2023
- const aKey = `${a.source}\0${a.kind}\0${a.target}`;
2024
- const bKey = `${b.source}\0${b.kind}\0${b.target}`;
2025
- return aKey.localeCompare(bKey);
2026
- });
2027
- for (const link2 of sorted) {
2028
- out.push(
2029
- tx(ASCII_FORMATTER_TEXTS.linkBullet, {
2030
- source: sanitizeForTerminal(link2.source),
2031
- kind: sanitizeForTerminal(link2.kind),
2032
- target: sanitizeForTerminal(link2.target),
2033
- confidence: link2.confidence
2034
- })
2035
- );
2036
- }
2037
- out.push("");
2038
- }
2039
- if (ctx.issues.length > 0) {
2040
- out.push(tx(ASCII_FORMATTER_TEXTS.issuesSectionHeader, { count: ctx.issues.length }));
2041
- for (const issue of ctx.issues) {
2042
- out.push(
2043
- tx(ASCII_FORMATTER_TEXTS.issueBullet, {
2044
- severity: issue.severity,
2045
- analyzerId: sanitizeForTerminal(issue.analyzerId),
2046
- message: sanitizeForTerminal(issue.message)
2047
- })
2048
- );
2049
- }
2050
- out.push("");
2051
- }
2052
- return out.join("\n");
2053
- }
2054
- };
2055
- function pickTitle(node) {
2056
- const name = node.frontmatter?.["name"];
2057
- return typeof name === "string" && name.length > 0 ? name : null;
2058
- }
2059
- function renderSection(out, kind, group) {
2060
- const sorted = [...group].sort((a, b) => a.path.localeCompare(b.path));
2061
- out.push(
2062
- tx(ASCII_FORMATTER_TEXTS.kindSectionHeader, {
2063
- kind: sanitizeForTerminal(kind),
2064
- count: sorted.length
2065
- })
2066
- );
2067
- for (const node of sorted) {
2068
- const title = pickTitle(node);
2069
- out.push(
2070
- title ? tx(ASCII_FORMATTER_TEXTS.nodeBulletWithTitle, {
2071
- path: sanitizeForTerminal(node.path),
2072
- title: sanitizeForTerminal(title)
2073
- }) : tx(ASCII_FORMATTER_TEXTS.nodeBullet, { path: sanitizeForTerminal(node.path) })
2074
- );
2075
- }
2076
- out.push("");
2077
- }
2078
-
2079
- // built-in-plugins/formatters/json/index.ts
2080
- var ID18 = "json";
2081
- var jsonFormatter = {
2082
- id: ID18,
2083
- pluginId: "core",
2084
- kind: "formatter",
2085
- version: "1.0.0",
2086
- description: "Renders the persisted scan as JSON (conforms to `scan-result.schema.json` when the full ScanResult is available). Used by `sm graph --format json` and `GET /api/graph?format=json`.",
2087
- stability: "stable",
2088
- formatId: ID18,
2089
- format(ctx) {
2090
- if (ctx.scanResult !== void 0) {
2091
- return JSON.stringify(ctx.scanResult);
2092
- }
2093
- return JSON.stringify({
2094
- nodes: ctx.nodes,
2095
- links: ctx.links,
2096
- issues: ctx.issues
2097
- });
2138
+ if (entry.location === "root") out.add(entry.key);
2098
2139
  }
2099
- };
2140
+ return out;
2141
+ }
2142
+ function collectPluginIds(contributions) {
2143
+ const out = /* @__PURE__ */ new Set();
2144
+ for (const entry of contributions) out.add(entry.pluginId);
2145
+ return out;
2146
+ }
2100
2147
 
2101
2148
  // kernel/adapters/schema-validators.ts
2102
2149
  import { readFileSync as readFileSync2 } from "fs";
@@ -2138,6 +2185,7 @@ var SCHEMA_FILES = {
2138
2185
  "conformance-case": "schemas/conformance-case.schema.json",
2139
2186
  "history-stats": "schemas/history-stats.schema.json",
2140
2187
  "extension-provider": "schemas/extensions/provider.schema.json",
2188
+ "extension-provider-kind": "schemas/extensions/provider-kind.schema.json",
2141
2189
  "extension-extractor": "schemas/extensions/extractor.schema.json",
2142
2190
  "extension-analyzer": "schemas/extensions/analyzer.schema.json",
2143
2191
  "extension-action": "schemas/extensions/action.schema.json",
@@ -2304,7 +2352,7 @@ function existsSyncSafe(path) {
2304
2352
  }
2305
2353
  }
2306
2354
 
2307
- // built-in-plugins/i18n/validate-all.texts.ts
2355
+ // plugins/core/analyzers/validate-all/text.ts
2308
2356
  var VALIDATE_ALL_TEXTS = {
2309
2357
  /** `Node <path> failed schema validation: <errors>` */
2310
2358
  nodeFailure: "Node {{path}} failed schema validation: {{errors}}",
@@ -2318,17 +2366,16 @@ var VALIDATE_ALL_TEXTS = {
2318
2366
  alertTooltipMany: "{{count}} schema validation issues on this node."
2319
2367
  };
2320
2368
 
2321
- // built-in-plugins/analyzers/validate-all/index.ts
2322
- var ID19 = "validate-all";
2369
+ // plugins/core/analyzers/validate-all/index.ts
2370
+ var ID18 = "validate-all";
2323
2371
  var validateAllAnalyzer = {
2324
- id: ID19,
2372
+ id: ID18,
2325
2373
  pluginId: "core",
2326
2374
  kind: "analyzer",
2327
2375
  version: "1.0.0",
2328
2376
  description: "Detects and flags nodes or links violating the project schemas.",
2329
- stability: "stable",
2330
2377
  mode: "deterministic",
2331
- viewContributions: {
2378
+ ui: {
2332
2379
  // Corner badge on the graph card; surfaces when the node body /
2333
2380
  // frontmatter fails schema validation (parse error, missing
2334
2381
  // `name`/`description`, malformed YAML, etc.). Same visual
@@ -2385,7 +2432,7 @@ function collectNodeFindings(v, node, out) {
2385
2432
  const result = v.validate("node", toNodeForSchema(node));
2386
2433
  if (result.ok) return;
2387
2434
  out.push({
2388
- analyzerId: ID19,
2435
+ analyzerId: ID18,
2389
2436
  severity: "error",
2390
2437
  nodeIds: [node.path],
2391
2438
  message: tx(VALIDATE_ALL_TEXTS.nodeFailure, {
@@ -2404,7 +2451,7 @@ function collectFrontmatterBaseFindings(node, out) {
2404
2451
  if (isMissingStringField(fm, "description")) missing.push("description");
2405
2452
  if (missing.length === 0) return;
2406
2453
  out.push({
2407
- analyzerId: ID19,
2454
+ analyzerId: ID18,
2408
2455
  // `warn` (not `error`) so the default `sm scan` exit code stays
2409
2456
  // 0 even when nodes are missing frontmatter base fields. Strict
2410
2457
  // mode (`sm scan --strict`) still escalates to exit 1. Matches
@@ -2426,7 +2473,7 @@ function collectLinkFindings(v, link2, out) {
2426
2473
  const result = v.validate("link", toLinkForSchema(link2));
2427
2474
  if (result.ok) return;
2428
2475
  out.push({
2429
- analyzerId: ID19,
2476
+ analyzerId: ID18,
2430
2477
  severity: "error",
2431
2478
  nodeIds: [link2.source],
2432
2479
  message: tx(VALIDATE_ALL_TEXTS.linkFailure, {
@@ -2466,120 +2513,154 @@ function toLinkForSchema(link2) {
2466
2513
  };
2467
2514
  }
2468
2515
 
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
- }
2516
+ // kernel/util/safe-text.ts
2517
+ var ANSI_ESCAPE_RE = /[›][[\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
2518
+ var C0_CONTROL_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
2519
+ function sanitizeForTerminal(text) {
2520
+ return text.replace(ANSI_ESCAPE_RE, "").replace(C0_CONTROL_RE, "");
2483
2521
  }
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);
2522
+
2523
+ // plugins/core/formatters/ascii/text.ts
2524
+ var ASCII_FORMATTER_TEXTS = {
2525
+ /** Header line: `skill-map graph: N nodes, M links, K issues`. */
2526
+ header: "skill-map graph: {{nodes}} nodes, {{links}} links, {{issues}} issues",
2527
+ /** Per-node-kind section header: `## <kind> (<count>)`. */
2528
+ kindSectionHeader: "## {{kind}} ({{count}})",
2529
+ /** Plain node bullet: `- <path>`. */
2530
+ nodeBullet: "- {{path}}",
2531
+ /** Node bullet with title suffix: `- <path>: "<title>"`. */
2532
+ nodeBulletWithTitle: '- {{path}}: "{{title}}"',
2533
+ /** `## links (<count>)` section header. */
2534
+ linksSectionHeader: "## links ({{count}})",
2535
+ /** Link bullet: `- <source> --<kind>--> <target> [<confidence>]`. */
2536
+ linkBullet: "- {{source}} --{{kind}}--> {{target}} [{{confidence}}]",
2537
+ /** `## issues (<count>)` section header. */
2538
+ issuesSectionHeader: "## issues ({{count}})",
2539
+ /** Issue bullet: `- [<severity>] <analyzerId>: <message>`. */
2540
+ issueBullet: "- [{{severity}}] {{analyzerId}}: {{message}}"
2541
+ };
2542
+
2543
+ // plugins/core/formatters/ascii/index.ts
2544
+ var ID19 = "ascii";
2545
+ var KIND_ORDER = ["agent", "command", "skill", "markdown"];
2546
+ var asciiFormatter = {
2547
+ id: ID19,
2548
+ pluginId: "core",
2549
+ kind: "formatter",
2550
+ formatId: ID19,
2551
+ version: "1.0.0",
2552
+ description: "Renders the scan as plain text, grouped by kind, arrows, and issues. Used by `sm scan --format=ascii`.",
2553
+ // ASCII tree formatter, header + per-kind sections + per-issue
2554
+ // section. Each section iterates and renders; splitting per section
2555
+ // would multiply the for-loop boilerplate.
2556
+ // eslint-disable-next-line complexity
2557
+ format(ctx) {
2558
+ const out = [];
2559
+ out.push(
2560
+ tx(ASCII_FORMATTER_TEXTS.header, {
2561
+ nodes: ctx.nodes.length,
2562
+ links: ctx.links.length,
2563
+ issues: ctx.issues.length
2564
+ }),
2565
+ ""
2566
+ );
2567
+ const byKind = /* @__PURE__ */ new Map();
2568
+ for (const node of ctx.nodes) {
2569
+ if (!byKind.has(node.kind)) byKind.set(node.kind, []);
2570
+ byKind.get(node.kind).push(node);
2571
+ }
2572
+ const renderedKinds = /* @__PURE__ */ new Set();
2573
+ for (const kind of KIND_ORDER) {
2574
+ const group = byKind.get(kind);
2575
+ if (!group || group.length === 0) continue;
2576
+ renderSection(out, kind, group);
2577
+ renderedKinds.add(kind);
2578
+ }
2579
+ const extraKinds = [...byKind.keys()].filter((k) => !renderedKinds.has(k)).sort();
2580
+ for (const kind of extraKinds) {
2581
+ const group = byKind.get(kind);
2582
+ if (!group || group.length === 0) continue;
2583
+ renderSection(out, kind, group);
2584
+ }
2585
+ if (ctx.links.length > 0) {
2586
+ out.push(tx(ASCII_FORMATTER_TEXTS.linksSectionHeader, { count: ctx.links.length }));
2587
+ const sorted = [...ctx.links].sort((a, b) => {
2588
+ const aKey = `${a.source}\0${a.kind}\0${a.target}`;
2589
+ const bKey = `${b.source}\0${b.kind}\0${b.target}`;
2590
+ return aKey.localeCompare(bKey);
2591
+ });
2592
+ for (const link2 of sorted) {
2593
+ out.push(
2594
+ tx(ASCII_FORMATTER_TEXTS.linkBullet, {
2595
+ source: sanitizeForTerminal(link2.source),
2596
+ kind: sanitizeForTerminal(link2.kind),
2597
+ target: sanitizeForTerminal(link2.target),
2598
+ confidence: link2.confidence
2599
+ })
2600
+ );
2601
+ }
2602
+ out.push("");
2603
+ }
2604
+ if (ctx.issues.length > 0) {
2605
+ out.push(tx(ASCII_FORMATTER_TEXTS.issuesSectionHeader, { count: ctx.issues.length }));
2606
+ for (const issue of ctx.issues) {
2607
+ out.push(
2608
+ tx(ASCII_FORMATTER_TEXTS.issueBullet, {
2609
+ severity: issue.severity,
2610
+ analyzerId: sanitizeForTerminal(issue.analyzerId),
2611
+ message: sanitizeForTerminal(issue.message)
2612
+ })
2613
+ );
2614
+ }
2615
+ out.push("");
2616
+ }
2617
+ return out.join("\n");
2491
2618
  }
2619
+ };
2620
+ function pickTitle(node) {
2621
+ const name = node.frontmatter?.["name"];
2622
+ return typeof name === "string" && name.length > 0 ? name : null;
2492
2623
  }
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];
2624
+ function renderSection(out, kind, group) {
2625
+ const sorted = [...group].sort((a, b) => a.path.localeCompare(b.path));
2626
+ out.push(
2627
+ tx(ASCII_FORMATTER_TEXTS.kindSectionHeader, {
2628
+ kind: sanitizeForTerminal(kind),
2629
+ count: sorted.length
2630
+ })
2631
+ );
2632
+ for (const node of sorted) {
2633
+ const title = pickTitle(node);
2634
+ out.push(
2635
+ title ? tx(ASCII_FORMATTER_TEXTS.nodeBulletWithTitle, {
2636
+ path: sanitizeForTerminal(node.path),
2637
+ title: sanitizeForTerminal(title)
2638
+ }) : tx(ASCII_FORMATTER_TEXTS.nodeBullet, { path: sanitizeForTerminal(node.path) })
2639
+ );
2504
2640
  }
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;
2641
+ out.push("");
2515
2642
  }
2516
2643
 
2517
- // built-in-plugins/analyzers/link-counts/index.ts
2518
- var ID20 = "link-counts";
2519
- var linkCountsAnalyzer = {
2644
+ // plugins/core/formatters/json/index.ts
2645
+ var ID20 = "json";
2646
+ var jsonFormatter = {
2520
2647
  id: ID20,
2521
2648
  pluginId: "core",
2522
- kind: "analyzer",
2649
+ kind: "formatter",
2523
2650
  version: "1.0.0",
2524
- description: "Counts incoming and outgoing links per node.",
2525
- stability: "stable",
2526
- mode: "deterministic",
2527
- viewContributions: {
2528
- linksIn: {
2529
- slot: "card.footer.left",
2530
- icon: "pi-download",
2531
- label: "incoming links",
2532
- emitWhenEmpty: false,
2533
- priority: 10
2534
- },
2535
- linksOut: {
2536
- slot: "card.footer.left",
2537
- icon: "pi-upload",
2538
- label: "outgoing links",
2539
- emitWhenEmpty: false,
2540
- priority: 20
2541
- }
2542
- },
2543
- evaluate(ctx) {
2544
- const nameIndex = buildNameIndex(ctx.nodes);
2545
- const perTarget = /* @__PURE__ */ new Map();
2546
- const perSource = /* @__PURE__ */ new Map();
2547
- for (const link2 of ctx.links) {
2548
- const resolvedTarget = resolveLinkTargetToPath(link2, nameIndex);
2549
- bump(perTarget, resolvedTarget, link2.kind);
2550
- bump(perSource, link2.source, link2.kind);
2551
- }
2552
- for (const node of ctx.nodes) {
2553
- emitChip(ctx, node.path, "linksIn", perTarget.get(node.path));
2554
- emitChip(ctx, node.path, "linksOut", perSource.get(node.path));
2651
+ description: "Renders the persisted scan as JSON (conforms to `scan-result.schema.json` when the full ScanResult is available). Used by `sm graph --format json` and `GET /api/graph?format=json`.",
2652
+ formatId: ID20,
2653
+ format(ctx) {
2654
+ if (ctx.scanResult !== void 0) {
2655
+ return JSON.stringify(ctx.scanResult);
2555
2656
  }
2556
- return [];
2657
+ return JSON.stringify({
2658
+ nodes: ctx.nodes,
2659
+ links: ctx.links,
2660
+ issues: ctx.issues
2661
+ });
2557
2662
  }
2558
2663
  };
2559
- function bump(map, key, kind) {
2560
- let byKind = map.get(key);
2561
- if (!byKind) {
2562
- byKind = /* @__PURE__ */ new Map();
2563
- map.set(key, byKind);
2564
- }
2565
- byKind.set(kind, (byKind.get(kind) ?? 0) + 1);
2566
- }
2567
- function emitChip(ctx, nodePath, contributionId, byKind) {
2568
- if (!byKind) return;
2569
- let total = 0;
2570
- for (const n of byKind.values()) total += n;
2571
- if (total === 0) return;
2572
- const capped = Math.min(total, 99);
2573
- const direction = contributionId === "linksIn" ? "in" : "out";
2574
- ctx.emitContribution(nodePath, contributionId, {
2575
- value: capped,
2576
- tooltip: formatBreakdown(byKind, direction)
2577
- });
2578
- }
2579
- function formatBreakdown(byKind, direction) {
2580
- const lines = [...byKind.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([kind, n]) => `${kind}: ${n}`);
2581
- return [direction, ...lines].join("\n");
2582
- }
2583
2664
 
2584
2665
  // kernel/sidecar/parse.ts
2585
2666
  import { existsSync, readFileSync as readFileSync3 } from "fs";
@@ -2706,7 +2787,7 @@ function resolveSpecRoot2() {
2706
2787
  }
2707
2788
  }
2708
2789
 
2709
- // built-in-plugins/actions/bump/index.ts
2790
+ // plugins/core/actions/bump/index.ts
2710
2791
  var ID21 = "bump";
2711
2792
  var PLUGIN_ID = "core";
2712
2793
  var bumpAction = {
@@ -2715,9 +2796,7 @@ var bumpAction = {
2715
2796
  kind: "action",
2716
2797
  version: "1.0.0",
2717
2798
  description: "Marks a node as updated: bumps version, refreshes sidecar hashes, records the timestamp.",
2718
- stability: "stable",
2719
2799
  mode: "deterministic",
2720
- reportSchemaRef: "https://skill-map.dev/spec/v0/bump-report.schema.json",
2721
2800
  // The runtime contract uses generic <TInput, TReport>; bump narrows
2722
2801
  // both. The cast is the standard pattern for built-ins that want
2723
2802
  // typed local I/O while staying compatible with the open generic.
@@ -2770,7 +2849,7 @@ function pickCurrentVersion(overlay) {
2770
2849
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
2771
2850
  }
2772
2851
 
2773
- // built-in-plugins/actions/mark-superseded/index.ts
2852
+ // plugins/core/actions/mark-superseded/index.ts
2774
2853
  var ID22 = "mark-superseded";
2775
2854
  var PLUGIN_ID2 = "core";
2776
2855
  var markSupersededAction = {
@@ -2779,9 +2858,7 @@ var markSupersededAction = {
2779
2858
  kind: "action",
2780
2859
  version: "0.0.0",
2781
2860
  description: "Declares the current node as superseded by another (writes `supersededBy` to the sidecar). Paired with the `core/superseded` analyzer.",
2782
- stability: "experimental",
2783
2861
  mode: "deterministic",
2784
- reportSchemaRef: "https://skill-map.dev/spec/v0/report-base-deterministic.schema.json",
2785
2862
  invoke(_input, _ctx) {
2786
2863
  const report = { ok: true, noop: true };
2787
2864
  return { report };
@@ -2886,7 +2963,7 @@ var UPDATE_CHECK_TEXTS = {
2886
2963
  // package.json
2887
2964
  var package_default = {
2888
2965
  name: "@skill-map/cli",
2889
- version: "0.28.0",
2966
+ version: "0.30.0",
2890
2967
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
2891
2968
  license: "MIT",
2892
2969
  type: "module",
@@ -2939,16 +3016,19 @@ var package_default = {
2939
3016
  "lint:fix": "eslint . --fix",
2940
3017
  reference: "node scripts/build-reference.js",
2941
3018
  "reference:check": "node scripts/build-reference.js --check",
3019
+ "build-built-ins": "node ../scripts/generate-built-ins.js",
3020
+ "built-ins:check": "node ../scripts/generate-built-ins.js --check",
3021
+ prebuild: "pnpm build-built-ins",
2942
3022
  validate: "pnpm validate:compile && pnpm validate:test",
2943
- "validate:compile": "pnpm typecheck && pnpm lint && pnpm build && pnpm reference:check",
3023
+ "validate:compile": "pnpm typecheck && pnpm lint && pnpm build && pnpm reference:check && pnpm built-ins:check",
2944
3024
  "validate:test": "pnpm test:ci",
2945
3025
  pretest: "tsup",
2946
3026
  "pretest:coverage": "tsup",
2947
3027
  "pretest:coverage:html": "tsup",
2948
- test: "tsc --noEmit && node --import tsx --test --test-reporter=spec 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts' 'server/**/*.test.ts'",
2949
- "test:ci": "node --import tsx --test 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts' 'server/**/*.test.ts'",
2950
- "test:coverage": "tsc --noEmit && SKILL_MAP_SKIP_BENCHMARK=1 node --experimental-default-config-file --import tsx --test --experimental-test-coverage 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts' 'server/**/*.test.ts'",
2951
- "test:coverage:html": "tsc --noEmit && SKILL_MAP_SKIP_BENCHMARK=1 c8 node --import tsx --test 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts' 'server/**/*.test.ts'",
3028
+ test: "tsc --noEmit && node --import tsx --test --test-reporter=spec '__tests__/**/*.spec.ts' 'kernel/**/__tests__/**/*.spec.ts' 'cli/**/__tests__/**/*.spec.ts' 'server/**/__tests__/**/*.spec.ts' 'plugins/**/__tests__/**/*.spec.ts' 'core/**/__tests__/**/*.spec.ts' 'conformance/**/__tests__/**/*.spec.ts'",
3029
+ "test:ci": "node --import tsx --test '__tests__/**/*.spec.ts' 'kernel/**/__tests__/**/*.spec.ts' 'cli/**/__tests__/**/*.spec.ts' 'server/**/__tests__/**/*.spec.ts' 'plugins/**/__tests__/**/*.spec.ts' 'core/**/__tests__/**/*.spec.ts' 'conformance/**/__tests__/**/*.spec.ts'",
3030
+ "test:coverage": "tsc --noEmit && SKILL_MAP_SKIP_BENCHMARK=1 node --experimental-default-config-file --import tsx --test --experimental-test-coverage '__tests__/**/*.spec.ts' 'kernel/**/__tests__/**/*.spec.ts' 'cli/**/__tests__/**/*.spec.ts' 'server/**/__tests__/**/*.spec.ts' 'plugins/**/__tests__/**/*.spec.ts' 'core/**/__tests__/**/*.spec.ts' 'conformance/**/__tests__/**/*.spec.ts'",
3031
+ "test:coverage:html": "tsc --noEmit && SKILL_MAP_SKIP_BENCHMARK=1 c8 node --import tsx --test '__tests__/**/*.spec.ts' 'kernel/**/__tests__/**/*.spec.ts' 'cli/**/__tests__/**/*.spec.ts' 'server/**/__tests__/**/*.spec.ts' 'plugins/**/__tests__/**/*.spec.ts' 'core/**/__tests__/**/*.spec.ts' 'conformance/**/__tests__/**/*.spec.ts'",
2952
3032
  clean: "rm -rf dist coverage"
2953
3033
  },
2954
3034
  dependencies: {
@@ -3237,15 +3317,13 @@ ${footer}
3237
3317
  `);
3238
3318
  }
3239
3319
 
3240
- // built-in-plugins/hooks/update-check/index.ts
3320
+ // plugins/core/hooks/update-check/index.ts
3241
3321
  var updateCheckHook = {
3242
3322
  id: "update-check",
3243
3323
  pluginId: "core",
3244
3324
  kind: "hook",
3245
3325
  version: "1.0.0",
3246
3326
  description: "Checks daily for a newer skill-map version on npm. Shows an `update available` banner when one is found.",
3247
- stability: "stable",
3248
- mode: "deterministic",
3249
3327
  triggers: ["boot"],
3250
3328
  async on(ctx) {
3251
3329
  const payload = ctx.event.data ?? {};
@@ -3259,14 +3337,41 @@ var updateCheckHook = {
3259
3337
  }
3260
3338
  };
3261
3339
 
3262
- // built-in-plugins/built-ins.ts
3340
+ // plugins/built-ins.ts
3341
+ var claudeProvider2 = { ...claudeProvider, pluginId: "claude" };
3342
+ var geminiProvider2 = { ...geminiProvider, pluginId: "gemini" };
3343
+ var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills" };
3344
+ var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core" };
3345
+ var annotationsExtractor2 = { ...annotationsExtractor, pluginId: "core" };
3346
+ var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "core" };
3347
+ var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core" };
3348
+ var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core" };
3349
+ var slashExtractor2 = { ...slashExtractor, pluginId: "core" };
3350
+ var toolsCountExtractor2 = { ...toolsCountExtractor, pluginId: "core" };
3351
+ var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core" };
3352
+ var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core" };
3353
+ var brokenRefAnalyzer2 = { ...brokenRefAnalyzer, pluginId: "core" };
3354
+ var contributionOrphanAnalyzer2 = { ...contributionOrphanAnalyzer, pluginId: "core" };
3355
+ var jobOrphanFileAnalyzer2 = { ...jobOrphanFileAnalyzer, pluginId: "core" };
3356
+ var linkConflictAnalyzer2 = { ...linkConflictAnalyzer, pluginId: "core" };
3357
+ var linkCountsAnalyzer2 = { ...linkCountsAnalyzer, pluginId: "core" };
3358
+ var stabilityAnalyzer2 = { ...stabilityAnalyzer, pluginId: "core" };
3359
+ var supersededAnalyzer2 = { ...supersededAnalyzer, pluginId: "core" };
3360
+ var triggerCollisionAnalyzer2 = { ...triggerCollisionAnalyzer, pluginId: "core" };
3361
+ var unknownFieldAnalyzer2 = { ...unknownFieldAnalyzer, pluginId: "core" };
3362
+ var validateAllAnalyzer2 = { ...validateAllAnalyzer, pluginId: "core" };
3363
+ var asciiFormatter2 = { ...asciiFormatter, pluginId: "core" };
3364
+ var jsonFormatter2 = { ...jsonFormatter, pluginId: "core" };
3365
+ var bumpAction2 = { ...bumpAction, pluginId: "core" };
3366
+ var markSupersededAction2 = { ...markSupersededAction, pluginId: "core" };
3367
+ var updateCheckHook2 = { ...updateCheckHook, pluginId: "core" };
3263
3368
  var builtInBundles = [
3264
3369
  {
3265
3370
  id: "claude",
3266
3371
  granularity: "bundle",
3267
3372
  description: "Claude Code platform integration. Classifies files under `.claude/{agents,commands,skills}` and parses Claude-flavored frontmatter.",
3268
3373
  extensions: [
3269
- claudeProvider
3374
+ claudeProvider2
3270
3375
  ]
3271
3376
  },
3272
3377
  {
@@ -3274,7 +3379,7 @@ var builtInBundles = [
3274
3379
  granularity: "bundle",
3275
3380
  description: "Gemini CLI platform integration. Classifies files under `.gemini/{agents,skills}` and parses Gemini-flavored frontmatter.",
3276
3381
  extensions: [
3277
- geminiProvider
3382
+ geminiProvider2
3278
3383
  ]
3279
3384
  },
3280
3385
  {
@@ -3282,7 +3387,7 @@ var builtInBundles = [
3282
3387
  granularity: "bundle",
3283
3388
  description: "Agent Skills open standard. Vendor-neutral path `.agents/skills/<name>/SKILL.md` (Anthropic, OpenAI, Google). See agentskills.io.",
3284
3389
  extensions: [
3285
- agentSkillsProvider
3390
+ agentSkillsProvider2
3286
3391
  ]
3287
3392
  },
3288
3393
  {
@@ -3290,39 +3395,30 @@ var builtInBundles = [
3290
3395
  granularity: "extension",
3291
3396
  description: "Core extensions shared across providers: extractors, analyzers, formatters, the bump action, and the universal `.md` fallback Provider.",
3292
3397
  extensions: [
3293
- // Provider FIRST within the core bundle so the kindRegistry
3294
- // composer picks it up alongside other providers; orchestration
3295
- // ordering (vendor providers first, core/markdown LAST) is
3296
- // enforced by the bundle list above (claude / gemini /
3297
- // agent-skills precede core). Within the core bundle, the
3298
- // provider's slot among extractors / analyzers / formatter is
3299
- // irrelevant, the orchestrator buckets by kind before
3300
- // iterating, so this list defines registration order, not
3301
- // execution order.
3302
- coreMarkdownProvider,
3303
- annotationsExtractor,
3304
- atDirectiveExtractor,
3305
- externalUrlCounterExtractor,
3306
- markdownLinkExtractor,
3307
- slashExtractor,
3308
- toolsCountExtractor,
3309
- triggerCollisionAnalyzer,
3310
- brokenRefAnalyzer,
3311
- supersededAnalyzer,
3312
- linkConflictAnalyzer,
3313
- annotationStaleAnalyzer,
3314
- annotationOrphanAnalyzer,
3315
- jobOrphanFileAnalyzer,
3316
- stabilityAnalyzer,
3317
- unknownFieldAnalyzer,
3318
- contributionOrphanAnalyzer,
3319
- asciiFormatter,
3320
- jsonFormatter,
3321
- validateAllAnalyzer,
3322
- linkCountsAnalyzer,
3323
- bumpAction,
3324
- markSupersededAction,
3325
- updateCheckHook
3398
+ coreMarkdownProvider2,
3399
+ annotationsExtractor2,
3400
+ atDirectiveExtractor2,
3401
+ externalUrlCounterExtractor2,
3402
+ markdownLinkExtractor2,
3403
+ slashExtractor2,
3404
+ toolsCountExtractor2,
3405
+ annotationOrphanAnalyzer2,
3406
+ annotationStaleAnalyzer2,
3407
+ brokenRefAnalyzer2,
3408
+ contributionOrphanAnalyzer2,
3409
+ jobOrphanFileAnalyzer2,
3410
+ linkConflictAnalyzer2,
3411
+ linkCountsAnalyzer2,
3412
+ stabilityAnalyzer2,
3413
+ supersededAnalyzer2,
3414
+ triggerCollisionAnalyzer2,
3415
+ unknownFieldAnalyzer2,
3416
+ validateAllAnalyzer2,
3417
+ asciiFormatter2,
3418
+ jsonFormatter2,
3419
+ bumpAction2,
3420
+ markSupersededAction2,
3421
+ updateCheckHook2
3326
3422
  ]
3327
3423
  }
3328
3424
  ];
@@ -3331,8 +3427,8 @@ function builtIns() {
3331
3427
  providers: [],
3332
3428
  extractors: [],
3333
3429
  analyzers: [],
3334
- actions: [],
3335
3430
  formatters: [],
3431
+ actions: [],
3336
3432
  hooks: []
3337
3433
  };
3338
3434
  for (const bundle of builtInBundles) {
@@ -3356,8 +3452,8 @@ function bucketBuiltIn(ext, out) {
3356
3452
  provider: out.providers,
3357
3453
  extractor: out.extractors,
3358
3454
  analyzer: out.analyzers,
3359
- action: out.actions,
3360
3455
  formatter: out.formatters,
3456
+ action: out.actions,
3361
3457
  hook: out.hooks
3362
3458
  });
3363
3459
  }
@@ -3366,12 +3462,9 @@ function toExtensionRow(x) {
3366
3462
  id: x.id,
3367
3463
  pluginId: x.pluginId,
3368
3464
  kind: x.kind,
3369
- version: x.version
3465
+ version: x.version,
3466
+ description: x.description ?? ""
3370
3467
  };
3371
- if (x.description !== void 0) row.description = x.description;
3372
- if (x.stability !== void 0) row.stability = x.stability;
3373
- if (x.preconditions !== void 0) row.preconditions = x.preconditions;
3374
- if (x.entry !== void 0) row.entry = x.entry;
3375
3468
  return row;
3376
3469
  }
3377
3470
 
@@ -4131,14 +4224,14 @@ function readJsonObjectOrEmpty(path) {
4131
4224
  }
4132
4225
  return {};
4133
4226
  }
4134
- function writeFileAtomicExclusive(path, content) {
4227
+ function writeFileAtomicExclusive(path, content, mode = 384) {
4135
4228
  const tmp = `${path}.tmp.${process.pid}.${randomBytes(8).toString("hex")}`;
4136
4229
  let fd = null;
4137
4230
  try {
4138
4231
  fd = openSync(
4139
4232
  tmp,
4140
4233
  fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW,
4141
- 384
4234
+ mode
4142
4235
  );
4143
4236
  writeSync(fd, content);
4144
4237
  closeSync(fd);
@@ -7124,7 +7217,8 @@ function invokeBumpFor(node, absPath, force) {
7124
7217
  node,
7125
7218
  nodeAbsolutePath: absPath,
7126
7219
  invoker: "cli",
7127
- now: () => /* @__PURE__ */ new Date()
7220
+ now: () => /* @__PURE__ */ new Date(),
7221
+ settings: {}
7128
7222
  });
7129
7223
  }
7130
7224
 
@@ -7644,8 +7738,8 @@ var PLUGIN_LOADER_TEXTS = {
7644
7738
 
7645
7739
  // kernel/adapters/plugin-loader/index.ts
7646
7740
  import { createRequire as createRequire5 } from "module";
7647
- import { existsSync as existsSync11, readFileSync as readFileSync11, readdirSync as readdirSync3 } from "fs";
7648
- import { join as join7, resolve as resolve16 } from "path";
7741
+ import { existsSync as existsSync12, readFileSync as readFileSync12, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
7742
+ import { join as join8, resolve as resolve16 } from "path";
7649
7743
  import { pathToFileURL } from "url";
7650
7744
  import semver from "semver";
7651
7745
 
@@ -7682,7 +7776,7 @@ function applyIdCollisions(plugins) {
7682
7776
  const buckets = /* @__PURE__ */ new Map();
7683
7777
  for (const p of plugins) {
7684
7778
  if (!p.manifest) continue;
7685
- const id = p.manifest.id;
7779
+ const id = p.id;
7686
7780
  const bucket = buckets.get(id);
7687
7781
  if (bucket) bucket.push(p);
7688
7782
  else buckets.set(id, [p]);
@@ -7733,39 +7827,22 @@ function extractDefault(mod) {
7733
7827
  if (!isRecord(mod)) return mod;
7734
7828
  return "default" in mod ? mod["default"] : mod;
7735
7829
  }
7736
- function stripFunctionsAndPluginId(input) {
7737
- if (!isRecord(input)) return input;
7738
- const out = {};
7739
- for (const [k, v] of Object.entries(input)) {
7740
- if (typeof v === "function") continue;
7741
- if (k === "pluginId") continue;
7742
- if (k === "kinds" && isRecord(v)) {
7743
- out[k] = stripKindsRuntimeFields(v);
7744
- continue;
7745
- }
7746
- out[k] = v;
7747
- }
7748
- return out;
7749
- }
7750
- function stripKindsRuntimeFields(kinds) {
7830
+ var LOADER_INJECTED_KEYS = /* @__PURE__ */ new Set(["pluginId", "id", "kind", "kinds", "formatId"]);
7831
+ function stripFunctionsAndPluginId(input) {
7832
+ if (!isRecord(input)) return input;
7751
7833
  const out = {};
7752
- for (const [kind, entry] of Object.entries(kinds)) {
7753
- if (!isRecord(entry)) {
7754
- out[kind] = entry;
7755
- continue;
7756
- }
7757
- const cleaned = {};
7758
- for (const [k, v] of Object.entries(entry)) {
7759
- if (k === "schemaJson") continue;
7760
- if (typeof v === "function") continue;
7761
- cleaned[k] = v;
7762
- }
7763
- out[kind] = cleaned;
7834
+ for (const [k, v] of Object.entries(input)) {
7835
+ if (typeof v === "function") continue;
7836
+ if (LOADER_INJECTED_KEYS.has(k)) continue;
7837
+ out[k] = v;
7764
7838
  }
7765
7839
  return out;
7766
7840
  }
7767
7841
 
7768
7842
  // kernel/adapters/plugin-loader/validation.ts
7843
+ import * as nodeFs from "fs";
7844
+ import { existsSync as existsSync11 } from "fs";
7845
+ import { dirname as dirname9, join as join7 } from "path";
7769
7846
  import { Ajv2020 as Ajv20205 } from "ajv/dist/2020.js";
7770
7847
 
7771
7848
  // kernel/extensions/hook.ts
@@ -7793,76 +7870,73 @@ var KNOWN_KINDS = /* @__PURE__ */ new Set([
7793
7870
  ]);
7794
7871
  var KNOWN_KINDS_LIST = [...KNOWN_KINDS].join(" / ");
7795
7872
  var HOOKABLE_TRIGGERS_LIST = HOOK_TRIGGERS.join(", ");
7796
- function validateAnnotationContributions(pluginPath, manifest, relEntry, manifestView) {
7873
+ function validateAnnotationContributions(pluginPath, pluginId, manifest, relEntry, manifestView) {
7797
7874
  if (!isRecord(manifestView)) return null;
7798
- const raw = manifestView["annotationContributions"];
7875
+ const raw = manifestView["annotation"];
7799
7876
  if (raw === void 0) return null;
7800
7877
  if (!isRecord(raw)) return null;
7801
- for (const [key, value] of Object.entries(raw)) {
7802
- if (!isRecord(value)) continue;
7803
- const location = value["location"] ?? "namespaced";
7804
- const ownership = value["ownership"] ?? "shared";
7805
- if (location === "root" && ownership !== "exclusive") {
7806
- return {
7807
- ...fail(
7808
- pluginPath,
7809
- manifest.id,
7810
- "invalid-manifest",
7811
- tx(PLUGIN_LOADER_TEXTS.invalidManifestRootSharedAnnotation, {
7812
- relEntry,
7813
- key,
7814
- ownership
7815
- })
7816
- ),
7817
- manifest
7818
- };
7819
- }
7820
- const schema = value["schema"];
7821
- if (!isRecord(schema)) {
7822
- return {
7823
- ...fail(
7824
- pluginPath,
7825
- manifest.id,
7826
- "invalid-manifest",
7827
- tx(PLUGIN_LOADER_TEXTS.invalidManifestAnnotationSchemaCompile, {
7828
- relEntry,
7829
- key,
7830
- errDescription: "schema must be an object literal"
7831
- })
7832
- ),
7833
- manifest
7834
- };
7835
- }
7836
- try {
7837
- const ajv = new Ajv20205({ strict: false, allErrors: true, allowUnionTypes: true });
7838
- applyAjvFormats(ajv);
7839
- ajv.compile(schema);
7840
- } catch (err) {
7841
- return {
7842
- ...fail(
7843
- pluginPath,
7844
- manifest.id,
7845
- "invalid-manifest",
7846
- tx(PLUGIN_LOADER_TEXTS.invalidManifestAnnotationSchemaCompile, {
7847
- relEntry,
7848
- key,
7849
- errDescription: describe(err)
7850
- })
7851
- ),
7852
- manifest
7853
- };
7854
- }
7878
+ const location = raw["location"] ?? "namespaced";
7879
+ const ownership = raw["ownership"] ?? "shared";
7880
+ if (location === "root" && ownership !== "exclusive") {
7881
+ return {
7882
+ ...fail(
7883
+ pluginPath,
7884
+ pluginId,
7885
+ "invalid-manifest",
7886
+ tx(PLUGIN_LOADER_TEXTS.invalidManifestRootSharedAnnotation, {
7887
+ relEntry,
7888
+ key: "<annotation>",
7889
+ ownership
7890
+ })
7891
+ ),
7892
+ manifest
7893
+ };
7894
+ }
7895
+ const schema = raw["schema"];
7896
+ if (!isRecord(schema)) {
7897
+ return {
7898
+ ...fail(
7899
+ pluginPath,
7900
+ pluginId,
7901
+ "invalid-manifest",
7902
+ tx(PLUGIN_LOADER_TEXTS.invalidManifestAnnotationSchemaCompile, {
7903
+ relEntry,
7904
+ key: "<annotation>",
7905
+ errDescription: "schema must be an object literal"
7906
+ })
7907
+ ),
7908
+ manifest
7909
+ };
7910
+ }
7911
+ try {
7912
+ const ajv = new Ajv20205({ strict: false, allErrors: true, allowUnionTypes: true });
7913
+ applyAjvFormats(ajv);
7914
+ ajv.compile(schema);
7915
+ } catch (err) {
7916
+ return {
7917
+ ...fail(
7918
+ pluginPath,
7919
+ pluginId,
7920
+ "invalid-manifest",
7921
+ tx(PLUGIN_LOADER_TEXTS.invalidManifestAnnotationSchemaCompile, {
7922
+ relEntry,
7923
+ key: "<annotation>",
7924
+ errDescription: describe(err)
7925
+ })
7926
+ ),
7927
+ manifest
7928
+ };
7855
7929
  }
7856
7930
  return null;
7857
7931
  }
7858
- function validateHookTriggers(pluginPath, manifest, relEntry, exported, manifestView) {
7932
+ function validateHookTriggers(pluginPath, pluginId, manifest, relEntry, exported, manifestView) {
7859
7933
  const triggers = manifestView["triggers"];
7860
7934
  const hookId = exported["id"] ?? "?";
7861
7935
  if (!Array.isArray(triggers) || triggers.length === 0) {
7862
7936
  return {
7863
7937
  ...fail(
7864
7938
  pluginPath,
7865
- manifest.id,
7939
+ pluginId,
7866
7940
  "invalid-manifest",
7867
7941
  tx(PLUGIN_LOADER_TEXTS.invalidManifestHookEmptyTriggers, { hookId })
7868
7942
  ),
@@ -7874,7 +7948,7 @@ function validateHookTriggers(pluginPath, manifest, relEntry, exported, manifest
7874
7948
  return {
7875
7949
  ...fail(
7876
7950
  pluginPath,
7877
- manifest.id,
7951
+ pluginId,
7878
7952
  "invalid-manifest",
7879
7953
  tx(PLUGIN_LOADER_TEXTS.invalidManifestHookUnknownTrigger, {
7880
7954
  hookId,
@@ -7888,9 +7962,135 @@ function validateHookTriggers(pluginPath, manifest, relEntry, exported, manifest
7888
7962
  }
7889
7963
  return null;
7890
7964
  }
7965
+ function validateActionFileConventions(pluginPath, pluginId, manifest, relEntry, entryAbsPath, manifestView) {
7966
+ const actionDir = dirname9(entryAbsPath);
7967
+ const reportSchemaPath = join7(actionDir, "report.schema.json");
7968
+ const promptPath = join7(actionDir, "prompt.md");
7969
+ const mode = isRecord(manifestView) && typeof manifestView["mode"] === "string" ? manifestView["mode"] : "deterministic";
7970
+ if (!existsSync11(reportSchemaPath)) {
7971
+ return {
7972
+ ...fail(
7973
+ pluginPath,
7974
+ pluginId,
7975
+ "load-error",
7976
+ `Action at \`${relEntry}\` is missing \`report.schema.json\` in its folder (structure-as-truth: every Action carries a report schema by convention).`
7977
+ ),
7978
+ manifest
7979
+ };
7980
+ }
7981
+ const promptExists = existsSync11(promptPath);
7982
+ if (mode === "probabilistic" && !promptExists) {
7983
+ return {
7984
+ ...fail(
7985
+ pluginPath,
7986
+ pluginId,
7987
+ "load-error",
7988
+ `Probabilistic Action at \`${relEntry}\` is missing \`prompt.md\` in its folder (structure-as-truth: probabilistic Actions carry a prompt template by convention).`
7989
+ ),
7990
+ manifest
7991
+ };
7992
+ }
7993
+ if (mode === "deterministic" && promptExists) {
7994
+ return {
7995
+ ...fail(
7996
+ pluginPath,
7997
+ pluginId,
7998
+ "invalid-manifest",
7999
+ `Deterministic Action at \`${relEntry}\` carries an unexpected \`prompt.md\` (delete the file or switch \`mode\` to \`'probabilistic'\`).`
8000
+ ),
8001
+ manifest
8002
+ };
8003
+ }
8004
+ return null;
8005
+ }
8006
+ function discoverProviderKinds(pluginPath, pluginId, manifest, relEntry, validatorForKind) {
8007
+ const kindsRoot = join7(pluginPath, "kinds");
8008
+ let entries;
8009
+ try {
8010
+ entries = nodeFs.readdirSync(kindsRoot);
8011
+ } catch {
8012
+ return { ok: true, kinds: {} };
8013
+ }
8014
+ const out = {};
8015
+ for (const entry of entries.sort()) {
8016
+ if (entry.startsWith(".")) continue;
8017
+ const kindDir = join7(kindsRoot, entry);
8018
+ if (!isDirectorySafe(kindDir, nodeFs.statSync)) continue;
8019
+ const result = loadOneProviderKind({
8020
+ pluginPath,
8021
+ pluginId,
8022
+ manifest,
8023
+ relEntry,
8024
+ entry,
8025
+ kindDir,
8026
+ validatorForKind
8027
+ });
8028
+ if (!result.ok) return result;
8029
+ out[entry] = result.kind;
8030
+ }
8031
+ return { ok: true, kinds: out };
8032
+ }
8033
+ function loadOneProviderKind(opts) {
8034
+ const schemaJson = readJsonFile(join7(opts.kindDir, "schema.json"));
8035
+ if ("error" in schemaJson) {
8036
+ return providerKindFailure(opts, "load-error", "schema.json", schemaJson.error);
8037
+ }
8038
+ const kindJson = readJsonFile(join7(opts.kindDir, "kind.json"));
8039
+ if ("error" in kindJson) {
8040
+ return providerKindFailure(opts, "invalid-manifest", "kind.json", kindJson.error);
8041
+ }
8042
+ const validation = opts.validatorForKind(kindJson.value);
8043
+ if (!validation.ok) {
8044
+ return {
8045
+ ok: false,
8046
+ failure: {
8047
+ ...fail(
8048
+ opts.pluginPath,
8049
+ opts.pluginId,
8050
+ "invalid-manifest",
8051
+ `Provider kind \`${opts.entry}\` (declared at \`${opts.relEntry}\`) failed validation in \`kinds/${opts.entry}/kind.json\`: ${validation.errors}. See spec/schemas/extensions/provider-kind.schema.json.`
8052
+ ),
8053
+ manifest: opts.manifest
8054
+ }
8055
+ };
8056
+ }
8057
+ const ui = isRecord(kindJson.value) ? kindJson.value["ui"] : void 0;
8058
+ return {
8059
+ ok: true,
8060
+ kind: { schema: `./kinds/${opts.entry}/schema.json`, schemaJson: schemaJson.value, ui }
8061
+ };
8062
+ }
8063
+ function readJsonFile(path) {
8064
+ try {
8065
+ return { value: JSON.parse(nodeFs.readFileSync(path, "utf8")) };
8066
+ } catch (err) {
8067
+ return { error: describe(err) };
8068
+ }
8069
+ }
8070
+ function providerKindFailure(opts, status, fileName, errDescription) {
8071
+ return {
8072
+ ok: false,
8073
+ failure: {
8074
+ ...fail(
8075
+ opts.pluginPath,
8076
+ opts.pluginId,
8077
+ status,
8078
+ `Provider kind \`${opts.entry}\` (declared at \`${opts.relEntry}\`) is missing or has an unparseable \`kinds/${opts.entry}/${fileName}\` (${errDescription}).`
8079
+ ),
8080
+ manifest: opts.manifest
8081
+ }
8082
+ };
8083
+ }
8084
+ function isDirectorySafe(path, statSync11) {
8085
+ try {
8086
+ return statSync11(path).isDirectory();
8087
+ } catch {
8088
+ return false;
8089
+ }
8090
+ }
7891
8091
 
7892
8092
  // kernel/adapters/plugin-loader/storage-schemas.ts
7893
- import { readFileSync as readFileSync10 } from "fs";
8093
+ import { readFileSync as readFileSync11 } from "fs";
7894
8094
  import { resolve as resolve15 } from "path";
7895
8095
  import { Ajv2020 as Ajv20206 } from "ajv/dist/2020.js";
7896
8096
 
@@ -7898,7 +8098,7 @@ import { Ajv2020 as Ajv20206 } from "ajv/dist/2020.js";
7898
8098
  var KV_SCHEMA_KEY = "__kv__";
7899
8099
 
7900
8100
  // kernel/adapters/plugin-loader/storage-schemas.ts
7901
- function loadStorageSchemas(pluginPath, manifest) {
8101
+ function loadStorageSchemas(pluginPath, pluginId, manifest) {
7902
8102
  const storage = manifest.storage;
7903
8103
  if (!storage) return { ok: true };
7904
8104
  if (storage.mode === "kv") {
@@ -7908,7 +8108,7 @@ function loadStorageSchemas(pluginPath, manifest) {
7908
8108
  const reason = tx(
7909
8109
  compiled.phase === "read" ? PLUGIN_LOADER_TEXTS.loadErrorStorageKvSchemaRead : PLUGIN_LOADER_TEXTS.loadErrorStorageKvSchemaCompile,
7910
8110
  {
7911
- pluginId: manifest.id,
8111
+ pluginId,
7912
8112
  schemaPath: storage.schema,
7913
8113
  errDescription: compiled.errDescription
7914
8114
  }
@@ -7935,7 +8135,7 @@ function loadStorageSchemas(pluginPath, manifest) {
7935
8135
  const reason = tx(
7936
8136
  compiled.phase === "read" ? PLUGIN_LOADER_TEXTS.loadErrorStorageSchemaRead : PLUGIN_LOADER_TEXTS.loadErrorStorageSchemaCompile,
7937
8137
  {
7938
- pluginId: manifest.id,
8138
+ pluginId,
7939
8139
  table,
7940
8140
  schemaPath: relPath,
7941
8141
  errDescription: compiled.errDescription
@@ -7958,7 +8158,7 @@ function compilePluginSchema(pluginPath, relPath) {
7958
8158
  const abs = resolve15(pluginPath, relPath);
7959
8159
  let raw;
7960
8160
  try {
7961
- raw = JSON.parse(readFileSync10(abs, "utf8"));
8161
+ raw = JSON.parse(readFileSync11(abs, "utf8"));
7962
8162
  } catch (err) {
7963
8163
  return { ok: false, phase: "read", errDescription: describe(err) };
7964
8164
  }
@@ -7992,11 +8192,11 @@ var PluginLoader = class {
7992
8192
  discoverPaths() {
7993
8193
  const out = [];
7994
8194
  for (const root of this.#options.searchPaths) {
7995
- if (!existsSync11(root)) continue;
7996
- for (const entry of readdirSync3(root, { withFileTypes: true })) {
8195
+ if (!existsSync12(root)) continue;
8196
+ for (const entry of readdirSync4(root, { withFileTypes: true })) {
7997
8197
  if (!entry.isDirectory()) continue;
7998
- const candidate = join7(root, entry.name);
7999
- if (existsSync11(join7(candidate, "plugin.json"))) {
8198
+ const candidate = join8(root, entry.name);
8199
+ if (existsSync12(join8(candidate, "plugin.json"))) {
8000
8200
  out.push(resolve16(candidate));
8001
8201
  }
8002
8202
  }
@@ -8006,7 +8206,7 @@ var PluginLoader = class {
8006
8206
  /**
8007
8207
  * Full pass, discover every plugin, attempt to load each, then apply
8008
8208
  * the cross-root id-collision pass over the results. Two plugins that
8009
- * survived their individual load with the same `manifest.id` both get
8209
+ * survived their individual load with the same `pluginId` both get
8010
8210
  * downgraded to status `id-collision` (no precedence, the spec is
8011
8211
  * explicit that "no extension is privileged"). Plugins that already
8012
8212
  * failed their individual load (`invalid-manifest` /
@@ -8026,61 +8226,69 @@ var PluginLoader = class {
8026
8226
  /**
8027
8227
  * Load a single plugin from its directory. Never throws, a failure is
8028
8228
  * reported via the returned status.
8229
+ *
8230
+ * Cyclomatic count covers the four sequential phases (manifest parse,
8231
+ * enabled resolution, per-extension load loop, storage output-schemas
8232
+ * compile) plus their failure short-circuits. Splitting each phase
8233
+ * into a helper would scatter the return-on-failure pattern without
8234
+ * making the orchestration clearer.
8029
8235
  */
8030
8236
  // eslint-disable-next-line complexity
8031
8237
  async loadOne(pluginPath) {
8032
- const manifestResult = this.#parseAndValidateManifest(pluginPath);
8238
+ const pluginId = pathId(pluginPath);
8239
+ const manifestResult = this.#parseAndValidateManifest(pluginPath, pluginId);
8033
8240
  if (!manifestResult.ok) return manifestResult.failure;
8034
8241
  const manifest = manifestResult.manifest;
8035
- if (this.#options.resolveEnabled && !this.#options.resolveEnabled(manifest.id)) {
8242
+ const granularity = manifest.granularity ?? "extension";
8243
+ if (this.#options.resolveEnabled && !this.#options.resolveEnabled(pluginId)) {
8036
8244
  return {
8037
8245
  path: pluginPath,
8038
- id: manifest.id,
8246
+ id: pluginId,
8039
8247
  status: "disabled",
8040
8248
  manifest,
8041
- granularity: manifest.granularity ?? "bundle",
8249
+ granularity,
8042
8250
  reason: PLUGIN_LOADER_TEXTS.disabledByConfig
8043
8251
  };
8044
8252
  }
8045
8253
  const loaded = [];
8046
- for (const relEntry of manifest.extensions) {
8047
- const result = await this.#loadAndValidateExtensionEntry(pluginPath, manifest, relEntry);
8254
+ for (const relEntry of discoverExtensionEntries(pluginPath)) {
8255
+ const result = await this.#loadAndValidateExtensionEntry(pluginPath, pluginId, manifest, relEntry);
8048
8256
  if (!result.ok) return result.failure;
8049
8257
  loaded.push(result.extension);
8050
8258
  }
8051
- const storageSchemasResult = loadStorageSchemas(pluginPath, manifest);
8259
+ const storageSchemasResult = loadStorageSchemas(pluginPath, pluginId, manifest);
8052
8260
  if (!storageSchemasResult.ok) {
8053
8261
  return {
8054
- ...fail(pluginPath, manifest.id, "load-error", storageSchemasResult.reason),
8262
+ ...fail(pluginPath, pluginId, "load-error", storageSchemasResult.reason),
8055
8263
  manifest
8056
8264
  };
8057
8265
  }
8058
8266
  return {
8059
8267
  path: pluginPath,
8060
- id: manifest.id,
8268
+ id: pluginId,
8061
8269
  status: "enabled",
8062
8270
  manifest,
8063
- granularity: manifest.granularity ?? "bundle",
8271
+ granularity,
8064
8272
  extensions: loaded,
8065
8273
  ...storageSchemasResult.schemas ? { storageSchemas: storageSchemasResult.schemas } : {}
8066
8274
  };
8067
8275
  }
8068
8276
  /**
8069
8277
  * Phase 1 of `loadOne`, read `plugin.json`, AJV-validate the manifest,
8070
- * enforce the directory-name == manifest.id structural rule, and check
8278
+ * enforce the directory-name == pluginId structural rule, and check
8071
8279
  * specCompat (range syntax + satisfies the installed spec version).
8072
8280
  * Returns either the validated manifest or an `IDiscoveredPlugin` with
8073
8281
  * the appropriate failure status.
8074
8282
  */
8075
- #parseAndValidateManifest(pluginPath) {
8076
- const manifestPath = join7(pluginPath, "plugin.json");
8283
+ #parseAndValidateManifest(pluginPath, pluginId) {
8284
+ const manifestPath = join8(pluginPath, "plugin.json");
8077
8285
  let raw;
8078
8286
  try {
8079
- raw = JSON.parse(readFileSync11(manifestPath, "utf8"));
8287
+ raw = JSON.parse(readFileSync12(manifestPath, "utf8"));
8080
8288
  } catch (err) {
8081
8289
  return { ok: false, failure: fail(
8082
8290
  pluginPath,
8083
- pathId(pluginPath),
8291
+ pluginId,
8084
8292
  "invalid-manifest",
8085
8293
  tx(PLUGIN_LOADER_TEXTS.invalidManifestJsonParse, {
8086
8294
  manifestPath,
@@ -8092,7 +8300,7 @@ var PluginLoader = class {
8092
8300
  if (!manifestResult.ok) {
8093
8301
  return { ok: false, failure: fail(
8094
8302
  pluginPath,
8095
- pathId(pluginPath),
8303
+ pluginId,
8096
8304
  "invalid-manifest",
8097
8305
  tx(PLUGIN_LOADER_TEXTS.invalidManifestAjv, {
8098
8306
  manifestPath,
@@ -8101,26 +8309,11 @@ var PluginLoader = class {
8101
8309
  ) };
8102
8310
  }
8103
8311
  const manifest = manifestResult.data;
8104
- const dirName = pathId(pluginPath);
8105
- if (dirName !== manifest.id) {
8106
- return { ok: false, failure: {
8107
- ...fail(
8108
- pluginPath,
8109
- manifest.id,
8110
- "invalid-manifest",
8111
- tx(PLUGIN_LOADER_TEXTS.invalidManifestDirMismatch, {
8112
- dirName,
8113
- manifestId: manifest.id
8114
- })
8115
- ),
8116
- manifest
8117
- } };
8118
- }
8119
8312
  if (!semver.validRange(manifest.specCompat)) {
8120
8313
  return { ok: false, failure: {
8121
8314
  ...fail(
8122
8315
  pluginPath,
8123
- manifest.id,
8316
+ pluginId,
8124
8317
  "invalid-manifest",
8125
8318
  tx(PLUGIN_LOADER_TEXTS.invalidSpecCompat, { specCompat: manifest.specCompat })
8126
8319
  ),
@@ -8130,10 +8323,10 @@ var PluginLoader = class {
8130
8323
  if (!semver.satisfies(this.#options.specVersion, manifest.specCompat, { includePrerelease: true })) {
8131
8324
  return { ok: false, failure: {
8132
8325
  path: pluginPath,
8133
- id: manifest.id,
8326
+ id: pluginId,
8134
8327
  status: "incompatible-spec",
8135
8328
  manifest,
8136
- granularity: manifest.granularity ?? "bundle",
8329
+ granularity: manifest.granularity ?? "extension",
8137
8330
  reason: tx(PLUGIN_LOADER_TEXTS.incompatibleSpec, {
8138
8331
  installedSpecVersion: this.#options.specVersion,
8139
8332
  specCompat: manifest.specCompat
@@ -8156,12 +8349,12 @@ var PluginLoader = class {
8156
8349
  // splitting per sub-check would multiply the discriminated-union
8157
8350
  // boilerplate without making the validation pipeline clearer.
8158
8351
  // eslint-disable-next-line complexity
8159
- async #loadAndValidateExtensionEntry(pluginPath, manifest, relEntry) {
8352
+ async #loadAndValidateExtensionEntry(pluginPath, pluginId, manifest, relEntry) {
8160
8353
  if (!isInsidePlugin(pluginPath, relEntry)) {
8161
8354
  return { ok: false, failure: {
8162
8355
  ...fail(
8163
8356
  pluginPath,
8164
- manifest.id,
8357
+ pluginId,
8165
8358
  "invalid-manifest",
8166
8359
  tx(PLUGIN_LOADER_TEXTS.loadErrorPathEscapesPlugin, { relEntry, pluginPath })
8167
8360
  ),
@@ -8169,11 +8362,11 @@ var PluginLoader = class {
8169
8362
  } };
8170
8363
  }
8171
8364
  const abs = resolve16(pluginPath, relEntry);
8172
- if (!existsSync11(abs)) {
8365
+ if (!existsSync12(abs)) {
8173
8366
  return { ok: false, failure: {
8174
8367
  ...fail(
8175
8368
  pluginPath,
8176
- manifest.id,
8369
+ pluginId,
8177
8370
  "load-error",
8178
8371
  tx(PLUGIN_LOADER_TEXTS.loadErrorFileNotFound, { relEntry, abs })
8179
8372
  ),
@@ -8187,7 +8380,7 @@ var PluginLoader = class {
8187
8380
  return { ok: false, failure: {
8188
8381
  ...fail(
8189
8382
  pluginPath,
8190
- manifest.id,
8383
+ pluginId,
8191
8384
  "load-error",
8192
8385
  tx(PLUGIN_LOADER_TEXTS.loadErrorImportFailed, {
8193
8386
  relEntry,
@@ -8198,11 +8391,11 @@ var PluginLoader = class {
8198
8391
  } };
8199
8392
  }
8200
8393
  const exported = extractDefault(mod);
8201
- if (!isRecord(exported) || typeof exported["kind"] !== "string") {
8394
+ if (!isRecord(exported)) {
8202
8395
  return { ok: false, failure: {
8203
8396
  ...fail(
8204
8397
  pluginPath,
8205
- manifest.id,
8398
+ pluginId,
8206
8399
  "load-error",
8207
8400
  tx(PLUGIN_LOADER_TEXTS.loadErrorMissingKind, {
8208
8401
  relEntry,
@@ -8212,16 +8405,32 @@ var PluginLoader = class {
8212
8405
  manifest
8213
8406
  } };
8214
8407
  }
8215
- const kind = exported["kind"];
8216
- if (!KNOWN_KINDS.has(kind)) {
8408
+ const [pathKindDir, pathId2] = relEntry.split("/");
8409
+ const kindFromPath = pathKindDir && pathKindDir.endsWith("s") ? pathKindDir.slice(0, -1) : void 0;
8410
+ if (!kindFromPath || !KNOWN_KINDS.has(kindFromPath)) {
8217
8411
  return { ok: false, failure: {
8218
8412
  ...fail(
8219
8413
  pluginPath,
8220
- manifest.id,
8221
- "load-error",
8414
+ pluginId,
8415
+ "invalid-manifest",
8222
8416
  tx(PLUGIN_LOADER_TEXTS.loadErrorUnknownKind, {
8223
8417
  relEntry,
8224
- kindReceived: String(exported["kind"]),
8418
+ kindReceived: String(pathKindDir ?? "(missing)"),
8419
+ knownKindsList: KNOWN_KINDS_LIST
8420
+ })
8421
+ ),
8422
+ manifest
8423
+ } };
8424
+ }
8425
+ const kind = kindFromPath;
8426
+ if (!pathId2) {
8427
+ return { ok: false, failure: {
8428
+ ...fail(
8429
+ pluginPath,
8430
+ pluginId,
8431
+ "invalid-manifest",
8432
+ tx(PLUGIN_LOADER_TEXTS.loadErrorMissingKind, {
8433
+ relEntry,
8225
8434
  knownKindsList: KNOWN_KINDS_LIST
8226
8435
  })
8227
8436
  ),
@@ -8229,16 +8438,16 @@ var PluginLoader = class {
8229
8438
  } };
8230
8439
  }
8231
8440
  const declaredPluginId = exported["pluginId"];
8232
- if (typeof declaredPluginId === "string" && declaredPluginId !== manifest.id) {
8441
+ if (typeof declaredPluginId === "string" && declaredPluginId !== pluginId) {
8233
8442
  return { ok: false, failure: {
8234
8443
  ...fail(
8235
8444
  pluginPath,
8236
- manifest.id,
8445
+ pluginId,
8237
8446
  "invalid-manifest",
8238
8447
  tx(PLUGIN_LOADER_TEXTS.loadErrorPluginIdMismatch, {
8239
8448
  relEntry,
8240
8449
  declared: declaredPluginId,
8241
- manifestId: manifest.id
8450
+ manifestId: pluginId
8242
8451
  })
8243
8452
  ),
8244
8453
  manifest
@@ -8246,7 +8455,7 @@ var PluginLoader = class {
8246
8455
  }
8247
8456
  const manifestView = stripFunctionsAndPluginId(exported);
8248
8457
  if (kind === "hook") {
8249
- const hookFailure = validateHookTriggers(pluginPath, manifest, relEntry, exported, manifestView);
8458
+ const hookFailure = validateHookTriggers(pluginPath, pluginId, manifest, relEntry, exported, manifestView);
8250
8459
  if (hookFailure) return { ok: false, failure: hookFailure };
8251
8460
  }
8252
8461
  const extValidator = this.#options.validators.validatorForExtension(kind);
@@ -8255,7 +8464,7 @@ var PluginLoader = class {
8255
8464
  return { ok: false, failure: {
8256
8465
  ...fail(
8257
8466
  pluginPath,
8258
- manifest.id,
8467
+ pluginId,
8259
8468
  "invalid-manifest",
8260
8469
  tx(PLUGIN_LOADER_TEXTS.invalidManifestExtensionShape, { relEntry, kind, errors })
8261
8470
  ),
@@ -8264,16 +8473,49 @@ var PluginLoader = class {
8264
8473
  }
8265
8474
  const contribFailure = validateAnnotationContributions(
8266
8475
  pluginPath,
8476
+ pluginId,
8267
8477
  manifest,
8268
8478
  relEntry,
8269
8479
  manifestView
8270
8480
  );
8271
8481
  if (contribFailure) return { ok: false, failure: contribFailure };
8272
- const instance = isRecord(exported) ? { ...exported, pluginId: manifest.id } : exported;
8482
+ if (kind === "action") {
8483
+ const actionFailure = validateActionFileConventions(
8484
+ pluginPath,
8485
+ pluginId,
8486
+ manifest,
8487
+ relEntry,
8488
+ abs,
8489
+ manifestView
8490
+ );
8491
+ if (actionFailure) return { ok: false, failure: actionFailure };
8492
+ }
8493
+ let discoveredKinds;
8494
+ if (kind === "provider") {
8495
+ const kindsResult = discoverProviderKinds(
8496
+ pluginPath,
8497
+ pluginId,
8498
+ manifest,
8499
+ relEntry,
8500
+ (data) => {
8501
+ const v = this.#options.validators.validate("extension-provider-kind", data);
8502
+ if (v.ok) return { ok: true, errors: "" };
8503
+ return { ok: false, errors: v.errors };
8504
+ }
8505
+ );
8506
+ if (!kindsResult.ok) return { ok: false, failure: kindsResult.failure };
8507
+ if (Object.keys(kindsResult.kinds).length > 0) discoveredKinds = kindsResult.kinds;
8508
+ }
8509
+ const instance = { ...exported, pluginId, id: pathId2, kind };
8510
+ if (kind === "formatter") instance["formatId"] = pathId2;
8511
+ if (kind === "provider" && discoveredKinds) {
8512
+ const inlineKinds = isRecord(exported["kinds"]) ? exported["kinds"] : {};
8513
+ instance["kinds"] = { ...inlineKinds, ...discoveredKinds };
8514
+ }
8273
8515
  return { ok: true, extension: {
8274
8516
  kind,
8275
- id: exported["id"],
8276
- pluginId: manifest.id,
8517
+ id: pathId2,
8518
+ pluginId,
8277
8519
  version: exported["version"],
8278
8520
  entryPath: abs,
8279
8521
  module: mod,
@@ -8281,11 +8523,64 @@ var PluginLoader = class {
8281
8523
  } };
8282
8524
  }
8283
8525
  };
8526
+ var KIND_DIR_NAMES = [
8527
+ "providers",
8528
+ "extractors",
8529
+ "analyzers",
8530
+ "actions",
8531
+ "formatters",
8532
+ "hooks"
8533
+ ];
8534
+ var INDEX_CANDIDATES = [
8535
+ "index.js",
8536
+ "index.mjs",
8537
+ "index.ts"
8538
+ ];
8539
+ function discoverExtensionEntries(pluginPath) {
8540
+ const out = [];
8541
+ for (const kindDir of KIND_DIR_NAMES) {
8542
+ collectKindEntries(pluginPath, kindDir, out);
8543
+ }
8544
+ return out;
8545
+ }
8546
+ function collectKindEntries(pluginPath, kindDir, out) {
8547
+ const kindAbs = resolve16(pluginPath, kindDir);
8548
+ if (!existsSync12(kindAbs)) return;
8549
+ let entries;
8550
+ try {
8551
+ entries = readdirSync4(kindAbs);
8552
+ } catch {
8553
+ return;
8554
+ }
8555
+ entries.sort();
8556
+ for (const entry of entries) {
8557
+ if (entry.startsWith(".")) continue;
8558
+ const entryAbs = resolve16(kindAbs, entry);
8559
+ if (!isDirectorySafe2(entryAbs)) continue;
8560
+ const candidate = findIndexCandidate(entryAbs);
8561
+ if (candidate !== null) {
8562
+ out.push(`${kindDir}/${entry}/${candidate}`);
8563
+ }
8564
+ }
8565
+ }
8566
+ function isDirectorySafe2(path) {
8567
+ try {
8568
+ return statSync2(path).isDirectory();
8569
+ } catch {
8570
+ return false;
8571
+ }
8572
+ }
8573
+ function findIndexCandidate(entryAbs) {
8574
+ for (const candidate of INDEX_CANDIDATES) {
8575
+ if (existsSync12(resolve16(entryAbs, candidate))) return candidate;
8576
+ }
8577
+ return null;
8578
+ }
8284
8579
  function installedSpecVersion() {
8285
8580
  const require2 = createRequire5(import.meta.url);
8286
8581
  const indexPath = require2.resolve("@skill-map/spec/index.json");
8287
8582
  const pkgPath = resolve16(indexPath, "..", "package.json");
8288
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
8583
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf8"));
8289
8584
  return pkg.version;
8290
8585
  }
8291
8586
 
@@ -8382,11 +8677,11 @@ async function buildEnabledResolver(ctx) {
8382
8677
 
8383
8678
  // kernel/scan/walk-content.ts
8384
8679
  import { readFile, readdir, lstat } from "fs/promises";
8385
- import { join as join8, relative as relative2, sep as sep2 } from "path";
8680
+ import { join as join9, relative as relative2, sep as sep2 } from "path";
8386
8681
 
8387
8682
  // kernel/scan/ignore.ts
8388
- import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
8389
- import { dirname as dirname9, resolve as resolve17 } from "path";
8683
+ import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
8684
+ import { dirname as dirname10, resolve as resolve17 } from "path";
8390
8685
  import { fileURLToPath as fileURLToPath2 } from "url";
8391
8686
  import ignoreFactory from "ignore";
8392
8687
  function buildIgnoreFilter(opts = {}) {
@@ -8416,9 +8711,9 @@ function loadBundledIgnoreText() {
8416
8711
  }
8417
8712
  function readIgnoreFileText(scopeRoot) {
8418
8713
  const path = resolve17(scopeRoot, ".skillmapignore");
8419
- if (!existsSync12(path)) return void 0;
8714
+ if (!existsSync13(path)) return void 0;
8420
8715
  try {
8421
- return readFileSync12(path, "utf8");
8716
+ return readFileSync13(path, "utf8");
8422
8717
  } catch {
8423
8718
  return void 0;
8424
8719
  }
@@ -8442,7 +8737,7 @@ function loadDefaultsText() {
8442
8737
  return cachedDefaults;
8443
8738
  }
8444
8739
  function readDefaultsFromDisk() {
8445
- const here = dirname9(fileURLToPath2(import.meta.url));
8740
+ const here = dirname10(fileURLToPath2(import.meta.url));
8446
8741
  const candidates = [
8447
8742
  resolve17(here, "../../config/defaults/skillmapignore"),
8448
8743
  // src/kernel/scan/ → src/config/defaults/
@@ -8451,9 +8746,9 @@ function readDefaultsFromDisk() {
8451
8746
  resolve17(here, "config/defaults/skillmapignore")
8452
8747
  ];
8453
8748
  for (const candidate of candidates) {
8454
- if (existsSync12(candidate)) {
8749
+ if (existsSync13(candidate)) {
8455
8750
  try {
8456
- return readFileSync12(candidate, "utf8");
8751
+ return readFileSync13(candidate, "utf8");
8457
8752
  } catch {
8458
8753
  }
8459
8754
  }
@@ -8461,7 +8756,7 @@ function readDefaultsFromDisk() {
8461
8756
  return "";
8462
8757
  }
8463
8758
 
8464
- // built-in-plugins/parsers/frontmatter-yaml/index.ts
8759
+ // plugins/core/parsers/frontmatter-yaml/index.ts
8465
8760
  import yaml3 from "js-yaml";
8466
8761
  var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
8467
8762
  var frontmatterYamlParser = {
@@ -8496,7 +8791,7 @@ function sanitiseParseErrorMessage(err) {
8496
8791
  return raw.replace(/[-]+/g, " ").replace(/\s+/g, " ").trim();
8497
8792
  }
8498
8793
 
8499
- // built-in-plugins/parsers/plain/index.ts
8794
+ // plugins/core/parsers/plain/index.ts
8500
8795
  var plainParser = {
8501
8796
  id: "plain",
8502
8797
  parse(raw, _path) {
@@ -8559,7 +8854,7 @@ async function* walkRoot(root, current, filter, extensions) {
8559
8854
  }
8560
8855
  for (const entry of entries) {
8561
8856
  const name = entry.name;
8562
- const full = join8(current, name);
8857
+ const full = join9(current, name);
8563
8858
  const rel = relative2(root, full).split(sep2).join("/");
8564
8859
  if (filter.ignores(rel)) continue;
8565
8860
  if (entry.isSymbolicLink()) continue;
@@ -8605,7 +8900,7 @@ function resolveProviderWalk(provider) {
8605
8900
  // kernel/extensions/collect-view-contributions.ts
8606
8901
  function collectViewContributions(pluginId, extensionId, instance, out, options = {}) {
8607
8902
  if (typeof instance !== "object" || instance === null) return;
8608
- const raw = instance["viewContributions"];
8903
+ const raw = instance["ui"];
8609
8904
  if (typeof raw !== "object" || raw === null) return;
8610
8905
  const exclude = options.excludeQualifiedIds;
8611
8906
  for (const [contributionId, value] of Object.entries(raw)) {
@@ -8649,6 +8944,7 @@ function bucketLoaded(loaded, bundle) {
8649
8944
  pluginId: ext.pluginId,
8650
8945
  kind: ext.kind,
8651
8946
  version: ext.version,
8947
+ description: instance.description ?? "",
8652
8948
  ...ext.entryPath ? { entry: ext.entryPath } : {}
8653
8949
  });
8654
8950
  collectAnnotationContributions(ext.pluginId, instance, bundle.annotationContributions);
@@ -8656,21 +8952,25 @@ function bucketLoaded(loaded, bundle) {
8656
8952
  }
8657
8953
  }
8658
8954
  function collectAnnotationContributions(pluginId, instance, out) {
8659
- if (typeof instance !== "object" || instance === null) return;
8660
- const raw = instance["annotationContributions"];
8661
- if (typeof raw !== "object" || raw === null) return;
8662
- for (const [key, value] of Object.entries(raw)) {
8663
- if (typeof value !== "object" || value === null) continue;
8664
- const entry = value;
8665
- if (typeof entry.schema !== "object" || entry.schema === null) continue;
8666
- out.push({
8667
- pluginId,
8668
- key,
8669
- location: entry.location ?? "namespaced",
8670
- ownership: entry.ownership ?? "shared",
8671
- schema: entry.schema
8672
- });
8673
- }
8955
+ const row = tryReadAnnotationRow(pluginId, instance);
8956
+ if (row !== null) out.push(row);
8957
+ }
8958
+ function tryReadAnnotationRow(pluginId, instance) {
8959
+ if (typeof instance !== "object" || instance === null) return null;
8960
+ const inst = instance;
8961
+ const raw = inst["annotation"];
8962
+ if (typeof raw !== "object" || raw === null) return null;
8963
+ const entry = raw;
8964
+ if (typeof entry.schema !== "object" || entry.schema === null) return null;
8965
+ const extId = inst["id"];
8966
+ if (typeof extId !== "string" || extId.length === 0) return null;
8967
+ return {
8968
+ pluginId,
8969
+ key: extId,
8970
+ location: entry.location ?? "namespaced",
8971
+ ownership: entry.ownership ?? "shared",
8972
+ schema: entry.schema
8973
+ };
8674
8974
  }
8675
8975
  function isExtensionInstance(v) {
8676
8976
  return typeof v === "object" && v !== null && typeof v["id"] === "string" && typeof v["kind"] === "string" && typeof v["version"] === "string";
@@ -8821,7 +9121,7 @@ function collectRegisteredContributionKeys(composed) {
8821
9121
  const keys = /* @__PURE__ */ new Set();
8822
9122
  if (!composed) return keys;
8823
9123
  for (const ext of [...composed.extractors, ...composed.analyzers]) {
8824
- const raw = ext.viewContributions;
9124
+ const raw = ext.ui;
8825
9125
  if (typeof raw !== "object" || raw === null) continue;
8826
9126
  for (const [contributionId, value] of Object.entries(raw)) {
8827
9127
  if (typeof value !== "object" || value === null) continue;
@@ -9162,7 +9462,7 @@ function trimRedundantPath(message, primary) {
9162
9462
  }
9163
9463
 
9164
9464
  // cli/commands/config.ts
9165
- import { existsSync as existsSync13 } from "fs";
9465
+ import { existsSync as existsSync14 } from "fs";
9166
9466
  import { Command as Command4, Option as Option4 } from "clipanion";
9167
9467
 
9168
9468
  // cli/util/path-display.ts
@@ -9690,7 +9990,7 @@ var ConfigResetCommand = class extends SmCommand {
9690
9990
  const path = targetSettingsPath2(target, ctx.cwd);
9691
9991
  const ansi = this.ansiFor("stdout");
9692
9992
  const okGlyph = ansi.green("\u2713");
9693
- if (!existsSync13(path)) {
9993
+ if (!existsSync14(path)) {
9694
9994
  this.printer.data(
9695
9995
  tx(CONFIG_TEXTS.unsetNoOverride, {
9696
9996
  glyph: okGlyph,
@@ -9765,16 +10065,16 @@ var CONFIG_COMMANDS = [
9765
10065
  ];
9766
10066
 
9767
10067
  // cli/commands/conformance.ts
9768
- import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
9769
- import { dirname as dirname11, resolve as resolve21 } from "path";
10068
+ import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
10069
+ import { dirname as dirname12, resolve as resolve21 } from "path";
9770
10070
  import { fileURLToPath as fileURLToPath4 } from "url";
9771
10071
  import { Command as Command5, Option as Option5 } from "clipanion";
9772
10072
 
9773
10073
  // conformance/index.ts
9774
10074
  import { spawnSync as spawnSync2 } from "child_process";
9775
- import { cpSync, existsSync as existsSync14, mkdtempSync, readdirSync as readdirSync4, readFileSync as readFileSync13, rmSync, statSync } from "fs";
10075
+ import { cpSync, existsSync as existsSync15, mkdtempSync, readdirSync as readdirSync5, readFileSync as readFileSync14, rmSync, statSync as statSync3 } from "fs";
9776
10076
  import { tmpdir } from "os";
9777
- import { isAbsolute as isAbsolute5, join as join9, relative as relative3, resolve as resolve19 } from "path";
10077
+ import { isAbsolute as isAbsolute5, join as join10, relative as relative3, resolve as resolve19 } from "path";
9778
10078
 
9779
10079
  // conformance/i18n/runner.texts.ts
9780
10080
  var CONFORMANCE_RUNNER_TEXTS = {
@@ -9808,11 +10108,11 @@ function disableEnv(setup) {
9808
10108
  return env;
9809
10109
  }
9810
10110
  function runConformanceCase(options) {
9811
- const raw = readFileSync13(options.casePath, "utf8");
10111
+ const raw = readFileSync14(options.casePath, "utf8");
9812
10112
  const c = JSON.parse(raw);
9813
- const fixturesRoot = options.fixturesRoot ?? join9(options.specRoot, "conformance", "fixtures");
10113
+ const fixturesRoot = options.fixturesRoot ?? join10(options.specRoot, "conformance", "fixtures");
9814
10114
  const safeId = c.id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 32);
9815
- const scope = mkdtempSync(join9(tmpdir(), `sm-conformance-${safeId}-`));
10115
+ const scope = mkdtempSync(join10(tmpdir(), `sm-conformance-${safeId}-`));
9816
10116
  const setupEnv = disableEnv(c.setup);
9817
10117
  try {
9818
10118
  const priorFailure = runPriorScansSetup(c, options, scope, fixturesRoot, setupEnv);
@@ -9882,11 +10182,11 @@ function runPriorScansSetup(c, options, scope, fixturesRoot, setupEnv) {
9882
10182
  }
9883
10183
  function replaceFixture(scope, fixturesRoot, fixture) {
9884
10184
  assertContained2(fixturesRoot, fixture, "fixture");
9885
- for (const entry of readdirSync4(scope)) {
10185
+ for (const entry of readdirSync5(scope)) {
9886
10186
  if (entry === KERNEL_SKILL_MAP_DIR) continue;
9887
- rmSync(join9(scope, entry), { recursive: true, force: true });
10187
+ rmSync(join10(scope, entry), { recursive: true, force: true });
9888
10188
  }
9889
- const src = join9(fixturesRoot, fixture);
10189
+ const src = join10(fixturesRoot, fixture);
9890
10190
  cpSync(src, scope, { recursive: true });
9891
10191
  }
9892
10192
  function assertContained2(root, rel, label) {
@@ -9923,7 +10223,7 @@ function evaluateAssertion(a, ctx) {
9923
10223
  return { ok: false, type: a.type, reason: formatErrorMessage(err) };
9924
10224
  }
9925
10225
  const abs = resolve19(ctx.scope, a.path);
9926
- return existsSync14(abs) ? { ok: true, type: a.type } : {
10226
+ return existsSync15(abs) ? { ok: true, type: a.type } : {
9927
10227
  ok: false,
9928
10228
  type: a.type,
9929
10229
  reason: tx(CONFORMANCE_RUNNER_TEXTS.fileNotFound, { path: a.path })
@@ -9936,17 +10236,17 @@ function evaluateAssertion(a, ctx) {
9936
10236
  } catch (err) {
9937
10237
  return { ok: false, type: a.type, reason: formatErrorMessage(err) };
9938
10238
  }
9939
- const fixturePath = join9(ctx.fixturesRoot, a.fixture);
10239
+ const fixturePath = join10(ctx.fixturesRoot, a.fixture);
9940
10240
  const targetPath = resolve19(ctx.scope, a.path);
9941
- if (!existsSync14(targetPath)) {
10241
+ if (!existsSync15(targetPath)) {
9942
10242
  return {
9943
10243
  ok: false,
9944
10244
  type: a.type,
9945
10245
  reason: tx(CONFORMANCE_RUNNER_TEXTS.targetNotFound, { path: a.path })
9946
10246
  };
9947
10247
  }
9948
- const needle = readFileSync13(fixturePath);
9949
- const haystack = readFileSync13(targetPath);
10248
+ const needle = readFileSync14(fixturePath);
10249
+ const haystack = readFileSync14(targetPath);
9950
10250
  return haystack.includes(needle) ? { ok: true, type: a.type } : {
9951
10251
  ok: false,
9952
10252
  type: a.type,
@@ -10119,15 +10419,15 @@ var CONFORMANCE_TEXTS = {
10119
10419
  };
10120
10420
 
10121
10421
  // cli/util/conformance-scopes.ts
10122
- import { existsSync as existsSync15, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
10123
- import { dirname as dirname10, resolve as resolve20 } from "path";
10422
+ import { existsSync as existsSync16, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
10423
+ import { dirname as dirname11, resolve as resolve20 } from "path";
10124
10424
  import { createRequire as createRequire6 } from "module";
10125
10425
  import { fileURLToPath as fileURLToPath3 } from "url";
10126
10426
  function resolveSpecRoot4() {
10127
10427
  const require2 = createRequire6(import.meta.url);
10128
10428
  try {
10129
10429
  const indexPath = require2.resolve("@skill-map/spec/index.json");
10130
- return dirname10(indexPath);
10430
+ return dirname11(indexPath);
10131
10431
  } catch {
10132
10432
  throw new Error(
10133
10433
  "@skill-map/spec not resolvable: ensure the workspace is linked or the package is installed."
@@ -10135,19 +10435,19 @@ function resolveSpecRoot4() {
10135
10435
  }
10136
10436
  }
10137
10437
  function resolveCliWorkspaceRoot() {
10138
- const here = dirname10(fileURLToPath3(import.meta.url));
10438
+ const here = dirname11(fileURLToPath3(import.meta.url));
10139
10439
  let cursor = here;
10140
10440
  for (let depth = 0; depth < 6; depth += 1) {
10141
- const candidate = resolve20(cursor, "built-in-plugins", "providers");
10142
- if (existsSync15(candidate) && statSync2(candidate).isDirectory()) {
10441
+ const candidate = resolve20(cursor, "plugins");
10442
+ if (existsSync16(candidate) && statSync4(candidate).isDirectory()) {
10143
10443
  return cursor;
10144
10444
  }
10145
- const parent = dirname10(cursor);
10445
+ const parent = dirname11(cursor);
10146
10446
  if (parent === cursor) break;
10147
10447
  cursor = parent;
10148
10448
  }
10149
10449
  throw new Error(
10150
- `sm conformance: built-in Provider conformance assets not found (expected a 'built-in-plugins/providers/' directory above ${here}). The bundled CLI may not yet copy the assets; run from the source workspace, or rebuild after enabling the asset-copy step.`
10450
+ `sm conformance: built-in Provider conformance assets not found (expected a 'plugins/' directory above ${here}). The bundled CLI may not yet copy the assets; run from the source workspace, or rebuild after enabling the asset-copy step.`
10151
10451
  );
10152
10452
  }
10153
10453
  function collectProviderScopes(specRoot) {
@@ -10158,16 +10458,33 @@ function collectProviderScopes(specRoot) {
10158
10458
  } catch {
10159
10459
  return out;
10160
10460
  }
10161
- const providersRoot = resolve20(workspaceRoot, "built-in-plugins", "providers");
10162
- if (!existsSync15(providersRoot)) return out;
10163
- for (const entry of readdirSync5(providersRoot)) {
10461
+ const pluginsRoot = resolve20(workspaceRoot, "plugins");
10462
+ if (!existsSync16(pluginsRoot)) return out;
10463
+ for (const bundleEntry of readdirSync6(pluginsRoot)) {
10464
+ const bundleDir = resolve20(pluginsRoot, bundleEntry);
10465
+ if (!isDir(bundleDir)) continue;
10466
+ const providersRoot = resolve20(bundleDir, "providers");
10467
+ if (!isDir(providersRoot)) continue;
10468
+ collectBundleProviderScopes(providersRoot, specRoot, out);
10469
+ }
10470
+ return out;
10471
+ }
10472
+ function isDir(path) {
10473
+ try {
10474
+ return existsSync16(path) && statSync4(path).isDirectory();
10475
+ } catch {
10476
+ return false;
10477
+ }
10478
+ }
10479
+ function collectBundleProviderScopes(providersRoot, specRoot, out) {
10480
+ for (const entry of readdirSync6(providersRoot)) {
10164
10481
  const providerDir = resolve20(providersRoot, entry);
10165
- if (!statSync2(providerDir).isDirectory()) continue;
10482
+ if (!isDir(providerDir)) continue;
10166
10483
  const conformanceDir = resolve20(providerDir, "conformance");
10167
- if (!existsSync15(conformanceDir)) continue;
10484
+ if (!existsSync16(conformanceDir)) continue;
10168
10485
  const casesDir = resolve20(conformanceDir, "cases");
10169
10486
  const fixturesDir = resolve20(conformanceDir, "fixtures");
10170
- if (!existsSync15(casesDir) || !existsSync15(fixturesDir)) continue;
10487
+ if (!existsSync16(casesDir) || !existsSync16(fixturesDir)) continue;
10171
10488
  out.push({
10172
10489
  id: `provider:${entry}`,
10173
10490
  kind: "provider",
@@ -10177,7 +10494,6 @@ function collectProviderScopes(specRoot) {
10177
10494
  specRoot
10178
10495
  });
10179
10496
  }
10180
- return out;
10181
10497
  }
10182
10498
  function specScope(specRoot) {
10183
10499
  return {
@@ -10206,8 +10522,8 @@ function selectConformanceScopes(scope) {
10206
10522
  return [match];
10207
10523
  }
10208
10524
  function listCaseFiles(scope) {
10209
- if (!existsSync15(scope.casesDir)) return [];
10210
- return readdirSync5(scope.casesDir).filter((entry) => entry.endsWith(".json")).sort().map((entry) => resolve20(scope.casesDir, entry));
10525
+ if (!existsSync16(scope.casesDir)) return [];
10526
+ return readdirSync6(scope.casesDir).filter((entry) => entry.endsWith(".json")).sort().map((entry) => resolve20(scope.casesDir, entry));
10211
10527
  }
10212
10528
 
10213
10529
  // cli/commands/conformance.ts
@@ -10221,12 +10537,12 @@ function formatAssertionFailureDetail(type, reason) {
10221
10537
  });
10222
10538
  }
10223
10539
  function resolveBinary() {
10224
- const here = dirname11(fileURLToPath4(import.meta.url));
10540
+ const here = dirname12(fileURLToPath4(import.meta.url));
10225
10541
  let cursor = here;
10226
10542
  for (let depth = 0; depth < 6; depth += 1) {
10227
10543
  const candidate = resolve21(cursor, "bin", "sm.js");
10228
- if (existsSync16(candidate)) return candidate;
10229
- const parent = dirname11(cursor);
10544
+ if (existsSync17(candidate)) return candidate;
10545
+ const parent = dirname12(cursor);
10230
10546
  if (parent === cursor) break;
10231
10547
  cursor = parent;
10232
10548
  }
@@ -10291,7 +10607,7 @@ var ConformanceRunCommand = class extends SmCommand {
10291
10607
  return ExitCode.Error;
10292
10608
  }
10293
10609
  const binary = resolveBinary();
10294
- if (!existsSync16(binary)) {
10610
+ if (!existsSync17(binary)) {
10295
10611
  if (this.json) {
10296
10612
  this.#emitJsonError(
10297
10613
  "internal",
@@ -10465,7 +10781,7 @@ function projectAssertionFailures(assertions) {
10465
10781
  }
10466
10782
  function readCaseId(casePath) {
10467
10783
  try {
10468
- const raw = readFileSync14(casePath, "utf8");
10784
+ const raw = readFileSync15(casePath, "utf8");
10469
10785
  const parsed = JSON.parse(raw);
10470
10786
  if (typeof parsed.id === "string") return parsed.id;
10471
10787
  } catch {
@@ -10483,7 +10799,7 @@ function writeStreamSnippet(stream, header, text) {
10483
10799
  var CONFORMANCE_COMMANDS = [ConformanceRunCommand];
10484
10800
 
10485
10801
  // cli/commands/db/backup.ts
10486
- import { dirname as dirname12, join as join10, resolve as resolve22 } from "path";
10802
+ import { dirname as dirname13, join as join11, resolve as resolve22 } from "path";
10487
10803
  import { Command as Command6, Option as Option6 } from "clipanion";
10488
10804
 
10489
10805
  // cli/i18n/db.texts.ts
@@ -10569,7 +10885,7 @@ var DbBackupCommand = class extends SmCommand {
10569
10885
  const exit = requireDbOrExit(path, this.context.stderr);
10570
10886
  if (exit !== null) return exit;
10571
10887
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
10572
- const outPath = this.out ? resolve22(this.out) : join10(dirname12(path), "backups", `${ts}.db`);
10888
+ const outPath = this.out ? resolve22(this.out) : join11(dirname13(path), "backups", `${ts}.db`);
10573
10889
  await withSqlite({ databasePath: path, autoMigrate: false }, async (storage) => {
10574
10890
  storage.migrations.writeBackup(outPath);
10575
10891
  });
@@ -10586,7 +10902,7 @@ var DbBackupCommand = class extends SmCommand {
10586
10902
 
10587
10903
  // cli/commands/db/restore.ts
10588
10904
  import { chmod, copyFile, mkdir, rm } from "fs/promises";
10589
- import { dirname as dirname13, resolve as resolve23 } from "path";
10905
+ import { dirname as dirname14, resolve as resolve23 } from "path";
10590
10906
  import { Command as Command7, Option as Option7 } from "clipanion";
10591
10907
 
10592
10908
  // cli/util/fs.ts
@@ -10669,7 +10985,7 @@ var DbRestoreCommand = class extends SmCommand {
10669
10985
  return ExitCode.Error;
10670
10986
  }
10671
10987
  }
10672
- await mkdir(dirname13(target), { recursive: true });
10988
+ await mkdir(dirname14(target), { recursive: true });
10673
10989
  await copyFile(sourcePath, target);
10674
10990
  await chmodOwnerOnlyBestEffort(target);
10675
10991
  for (const sidecar of [`${target}-wal`, `${target}-shm`]) {
@@ -11018,7 +11334,7 @@ function formatSqlValue(value) {
11018
11334
 
11019
11335
  // cli/commands/db/migrate.ts
11020
11336
  import { mkdir as mkdir2 } from "fs/promises";
11021
- import { dirname as dirname14 } from "path";
11337
+ import { dirname as dirname15 } from "path";
11022
11338
  import { Command as Command12, Option as Option11 } from "clipanion";
11023
11339
 
11024
11340
  // cli/i18n/option-validators.texts.ts
@@ -11105,7 +11421,7 @@ var DbMigrateCommand = class extends SmCommand {
11105
11421
  return ExitCode.Error;
11106
11422
  }
11107
11423
  const path = resolveDbPath({ db: this.db, ...defaultRuntimeContext() });
11108
- if (path !== ":memory:") await mkdir2(dirname14(path), { recursive: true });
11424
+ if (path !== ":memory:") await mkdir2(dirname15(path), { recursive: true });
11109
11425
  const adapter = createSqliteStorage({
11110
11426
  databasePath: path,
11111
11427
  autoMigrate: false
@@ -11758,7 +12074,7 @@ var GraphCommand = class extends SmCommand {
11758
12074
  };
11759
12075
 
11760
12076
  // cli/commands/help.ts
11761
- import { readFileSync as readFileSync15 } from "fs";
12077
+ import { readFileSync as readFileSync16 } from "fs";
11762
12078
  import { createRequire as createRequire7 } from "module";
11763
12079
  import { resolve as resolve25 } from "path";
11764
12080
  import { Command as Command15, Option as Option14 } from "clipanion";
@@ -11981,7 +12297,7 @@ function resolveSpecVersion() {
11981
12297
  const req = createRequire7(import.meta.url);
11982
12298
  const indexPath = req.resolve("@skill-map/spec/index.json");
11983
12299
  const pkgPath = resolve25(indexPath, "..", "package.json");
11984
- const pkg = JSON.parse(readFileSync15(pkgPath, "utf8"));
12300
+ const pkg = JSON.parse(readFileSync16(pkgPath, "utf8"));
11985
12301
  return pkg.version;
11986
12302
  } catch {
11987
12303
  return "unknown";
@@ -12233,13 +12549,13 @@ function registeredVerbPaths(cli2) {
12233
12549
  // cli/commands/hooks.ts
12234
12550
  import {
12235
12551
  chmodSync,
12236
- existsSync as existsSync17,
12552
+ existsSync as existsSync18,
12237
12553
  mkdirSync as mkdirSync5,
12238
- readFileSync as readFileSync16,
12239
- statSync as statSync3,
12554
+ readFileSync as readFileSync17,
12555
+ statSync as statSync5,
12240
12556
  writeFileSync as writeFileSync2
12241
12557
  } from "fs";
12242
- import { dirname as dirname15, resolve as resolve26 } from "path";
12558
+ import { dirname as dirname16, resolve as resolve26 } from "path";
12243
12559
  import { Command as Command16, Option as Option15 } from "clipanion";
12244
12560
 
12245
12561
  // cli/i18n/hooks.texts.ts
@@ -12332,7 +12648,7 @@ var HooksInstallCommand = class extends SmCommand {
12332
12648
  }
12333
12649
  const hooksDir = resolve26(repoRoot, ".git", "hooks");
12334
12650
  const hookPath = resolve26(hooksDir, "pre-commit");
12335
- const existing = existsSync17(hookPath) ? readFileSync16(hookPath, "utf8") : null;
12651
+ const existing = existsSync18(hookPath) ? readFileSync17(hookPath, "utf8") : null;
12336
12652
  const planned2 = computePlannedHookContent(existing);
12337
12653
  if (planned2.kind === "already-installed") {
12338
12654
  this.printer.info(tx(HOOKS_TEXTS.alreadyInstalled, { glyph: okGlyph, hookPath }));
@@ -12358,7 +12674,7 @@ var HooksInstallCommand = class extends SmCommand {
12358
12674
  return ExitCode.Ok;
12359
12675
  }
12360
12676
  try {
12361
- if (!existsSync17(hooksDir)) mkdirSync5(hooksDir, { recursive: true });
12677
+ if (!existsSync18(hooksDir)) mkdirSync5(hooksDir, { recursive: true });
12362
12678
  writeFileSync2(hookPath, planned2.content, { encoding: "utf8" });
12363
12679
  ensureExecutableBit(hookPath);
12364
12680
  } catch (err) {
@@ -12389,8 +12705,8 @@ var HooksInstallCommand = class extends SmCommand {
12389
12705
  function findGitRepoRoot(cwd) {
12390
12706
  let current = cwd;
12391
12707
  while (true) {
12392
- if (existsSync17(resolve26(current, ".git"))) return current;
12393
- const parent = dirname15(current);
12708
+ if (existsSync18(resolve26(current, ".git"))) return current;
12709
+ const parent = dirname16(current);
12394
12710
  if (parent === current) return null;
12395
12711
  current = parent;
12396
12712
  }
@@ -12404,18 +12720,18 @@ function computePlannedHookContent(existing) {
12404
12720
  return { kind: "chained", content: existing + sep6 + "\n" + SKILL_MAP_BLOCK };
12405
12721
  }
12406
12722
  function ensureExecutableBit(path) {
12407
- const mode = statSync3(path).mode;
12723
+ const mode = statSync5(path).mode;
12408
12724
  chmodSync(path, mode | 73);
12409
12725
  }
12410
12726
  var HOOKS_COMMANDS = [HooksInstallCommand];
12411
12727
 
12412
12728
  // cli/commands/init.ts
12413
12729
  import { mkdir as mkdir3, readFile as readFile2, writeFile } from "fs/promises";
12414
- import { join as join14 } from "path";
12730
+ import { join as join15 } from "path";
12415
12731
  import { Command as Command17, Option as Option16 } from "clipanion";
12416
12732
 
12417
12733
  // kernel/orchestrator/index.ts
12418
- import { existsSync as existsSync20, statSync as statSync5 } from "fs";
12734
+ import { existsSync as existsSync21, statSync as statSync7 } from "fs";
12419
12735
  import { Tiktoken as Tiktoken2 } from "js-tiktoken/lite";
12420
12736
  import cl100k_base from "js-tiktoken/ranks/cl100k_base";
12421
12737
 
@@ -12533,7 +12849,7 @@ async function runExtractorsForNode(opts) {
12533
12849
  }
12534
12850
  function readDeclaredContributions(extension) {
12535
12851
  const out = /* @__PURE__ */ new Map();
12536
- const raw = extension.viewContributions;
12852
+ const raw = extension.ui;
12537
12853
  if (typeof raw !== "object" || raw === null) return out;
12538
12854
  for (const [id, value] of Object.entries(raw)) {
12539
12855
  if (typeof value !== "object" || value === null) continue;
@@ -12554,11 +12870,13 @@ function emitExtensionError(emitter, qualifiedId2, nodePath, data) {
12554
12870
  );
12555
12871
  }
12556
12872
  function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, emitContribution, store) {
12557
- const scope = extractor.scope;
12873
+ const scope = extractor.scope ?? "both";
12874
+ const settings = extractor.resolvedSettings ?? {};
12558
12875
  return {
12559
12876
  node,
12560
12877
  body: scope === "frontmatter" ? "" : body,
12561
12878
  frontmatter: scope === "body" ? {} : frontmatter,
12879
+ settings,
12562
12880
  emitLink,
12563
12881
  enrichNode,
12564
12882
  emitContribution,
@@ -12566,25 +12884,26 @@ function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enr
12566
12884
  };
12567
12885
  }
12568
12886
  function validateLink(extractor, link2, emitter) {
12569
- if (!extractor.emitsLinkKinds.includes(link2.kind)) {
12887
+ const knownKinds = ["invokes", "references", "mentions", "supersedes"];
12888
+ if (!knownKinds.includes(link2.kind)) {
12570
12889
  const qualifiedId2 = `${extractor.pluginId}/${extractor.id}`;
12571
12890
  emitter.emit(
12572
12891
  makeEvent("extension.error", {
12573
12892
  kind: "link-kind-not-declared",
12574
12893
  extensionId: qualifiedId2,
12575
12894
  linkKind: link2.kind,
12576
- declaredKinds: extractor.emitsLinkKinds,
12895
+ declaredKinds: knownKinds,
12577
12896
  link: { source: link2.source, target: link2.target, kind: link2.kind },
12578
12897
  message: tx(ORCHESTRATOR_TEXTS.extensionErrorLinkKindNotDeclared, {
12579
12898
  extractorId: qualifiedId2,
12580
12899
  linkKind: link2.kind,
12581
- declaredKinds: extractor.emitsLinkKinds.join(", ")
12900
+ declaredKinds: knownKinds.join(", ")
12582
12901
  })
12583
12902
  })
12584
12903
  );
12585
12904
  return null;
12586
12905
  }
12587
- const confidence = link2.confidence ?? extractor.defaultConfidence;
12906
+ const confidence = link2.confidence ?? "medium";
12588
12907
  return { ...link2, confidence };
12589
12908
  }
12590
12909
  function dedupeLinks(links) {
@@ -12642,7 +12961,7 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
12642
12961
  const issues = [];
12643
12962
  const contributions = [];
12644
12963
  const validators = loadSchemaValidators();
12645
- validateRecommendedActions(analyzers, registeredActionIds, emitter);
12964
+ void registeredActionIds;
12646
12965
  const analyzerOrphans = orphanSidecars.map((o) => ({
12647
12966
  relativePath: o.relativePath,
12648
12967
  expectedMdPath: o.expectedMdPath
@@ -12714,27 +13033,6 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
12714
13033
  }
12715
13034
  return { issues, contributions };
12716
13035
  }
12717
- function validateRecommendedActions(analyzers, registeredActionIds, emitter) {
12718
- for (const analyzer of analyzers) {
12719
- const refs = analyzer.recommendedActions;
12720
- if (refs === void 0 || refs.length === 0) continue;
12721
- const analyzerId = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
12722
- for (const actionId of refs) {
12723
- if (registeredActionIds.has(actionId)) continue;
12724
- emitter.emit(
12725
- makeEvent("extension.error", {
12726
- kind: "recommended-action-missing",
12727
- extensionId: analyzerId,
12728
- actionId,
12729
- message: tx(ORCHESTRATOR_TEXTS.extensionErrorRecommendedActionMissing, {
12730
- analyzerId,
12731
- actionId
12732
- })
12733
- })
12734
- );
12735
- }
12736
- }
12737
- }
12738
13036
  function validateIssue(analyzer, issue, emitter) {
12739
13037
  const severity = issue.severity;
12740
13038
  if (severity !== "error" && severity !== "warn" && severity !== "info") {
@@ -12812,9 +13110,15 @@ function originatingNodeOf(link2, priorNodePaths) {
12812
13110
  return link2.source;
12813
13111
  }
12814
13112
  function computeCacheDecision(opts) {
12815
- const applicableExtractors = opts.extractors.filter(
12816
- (ex) => ex.applicableKinds === void 0 || ex.applicableKinds.includes(opts.kind)
12817
- );
13113
+ const applicableExtractors = opts.extractors.filter((ex) => {
13114
+ const kinds = ex.precondition?.kind;
13115
+ if (!kinds || kinds.length === 0) return true;
13116
+ return kinds.some((qualified) => {
13117
+ const slashIdx = qualified.indexOf("/");
13118
+ const kindOnly = slashIdx === -1 ? qualified : qualified.slice(slashIdx + 1);
13119
+ return kindOnly === opts.kind;
13120
+ });
13121
+ });
12818
13122
  const applicableQualifiedIds = new Set(
12819
13123
  applicableExtractors.map((ex) => qualifiedExtensionId(ex.pluginId, ex.id))
12820
13124
  );
@@ -13005,6 +13309,7 @@ function flagAmbiguousRenames(opts) {
13005
13309
  function flagOrphans(opts) {
13006
13310
  for (const fromPath of opts.deletedPaths) {
13007
13311
  if (opts.claimedDeleted.has(fromPath)) continue;
13312
+ if (opts.silenced?.(fromPath)) continue;
13008
13313
  opts.issues.push({
13009
13314
  analyzerId: "orphan",
13010
13315
  severity: "info",
@@ -13014,7 +13319,7 @@ function flagOrphans(opts) {
13014
13319
  });
13015
13320
  }
13016
13321
  }
13017
- function detectRenamesAndOrphans(prior, current, issues) {
13322
+ function detectRenamesAndOrphans(prior, current, issues, silenced) {
13018
13323
  const priorByPath = /* @__PURE__ */ new Map();
13019
13324
  for (const n of prior.nodes) priorByPath.set(n.path, n);
13020
13325
  const currentByPath = /* @__PURE__ */ new Map();
@@ -13048,7 +13353,12 @@ function detectRenamesAndOrphans(prior, current, issues) {
13048
13353
  issues
13049
13354
  }));
13050
13355
  flagAmbiguousRenames({ newPaths, candidatesByNew, claimedDeleted, claimedNew, issues });
13051
- flagOrphans({ deletedPaths, claimedDeleted, issues });
13356
+ flagOrphans({
13357
+ deletedPaths,
13358
+ claimedDeleted,
13359
+ issues,
13360
+ ...silenced ? { silenced } : {}
13361
+ });
13052
13362
  return ops;
13053
13363
  }
13054
13364
 
@@ -13063,8 +13373,8 @@ function computeDriftStatus(args2) {
13063
13373
  }
13064
13374
 
13065
13375
  // kernel/sidecar/discover-orphans.ts
13066
- import { existsSync as existsSync18, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
13067
- import { join as join11, relative as relative4, sep as sep3 } from "path";
13376
+ import { existsSync as existsSync19, readdirSync as readdirSync7, statSync as statSync6 } from "fs";
13377
+ import { join as join12, relative as relative4, sep as sep3 } from "path";
13068
13378
  function discoverOrphanSidecars(roots, shouldSkip) {
13069
13379
  const out = [];
13070
13380
  for (const root of roots) {
@@ -13075,12 +13385,12 @@ function discoverOrphanSidecars(roots, shouldSkip) {
13075
13385
  function walk(root, current, shouldSkip, out) {
13076
13386
  let entries;
13077
13387
  try {
13078
- entries = readdirSync6(current, { withFileTypes: true, encoding: "utf8" });
13388
+ entries = readdirSync7(current, { withFileTypes: true, encoding: "utf8" });
13079
13389
  } catch {
13080
13390
  return;
13081
13391
  }
13082
13392
  for (const entry of entries) {
13083
- const full = join11(current, entry.name);
13393
+ const full = join12(current, entry.name);
13084
13394
  const rel = relative4(root, full).split(sep3).join("/");
13085
13395
  if (shouldSkip(rel)) continue;
13086
13396
  if (entry.isSymbolicLink()) continue;
@@ -13091,13 +13401,13 @@ function walk(root, current, shouldSkip, out) {
13091
13401
  if (!entry.isFile()) continue;
13092
13402
  if (!entry.name.endsWith(".sm")) continue;
13093
13403
  const expectedMd = `${full.slice(0, -".sm".length)}.md`;
13094
- if (existsSync18(expectedMd) && safeIsFile(expectedMd)) continue;
13404
+ if (existsSync19(expectedMd) && safeIsFile(expectedMd)) continue;
13095
13405
  out.push({ sidecarPath: full, relativePath: rel, expectedMdPath: expectedMd });
13096
13406
  }
13097
13407
  }
13098
13408
  function safeIsFile(path) {
13099
13409
  try {
13100
- return statSync4(path).isFile();
13410
+ return statSync6(path).isFile();
13101
13411
  } catch {
13102
13412
  return false;
13103
13413
  }
@@ -13105,7 +13415,7 @@ function safeIsFile(path) {
13105
13415
 
13106
13416
  // kernel/orchestrator/node-build.ts
13107
13417
  import { createHash } from "crypto";
13108
- import { existsSync as existsSync19 } from "fs";
13418
+ import { existsSync as existsSync20 } from "fs";
13109
13419
  import { isAbsolute as isAbsolute6, resolve as resolvePath } from "path";
13110
13420
  import "js-tiktoken/lite";
13111
13421
  import yaml4 from "js-yaml";
@@ -13269,11 +13579,11 @@ function resolveSidecarOverlay(relativePath2, nodePathForIssue, roots, liveBodyH
13269
13579
  }
13270
13580
  function resolveAbsoluteMdPath(relativePath2, roots) {
13271
13581
  if (isAbsolute6(relativePath2)) {
13272
- return existsSync19(relativePath2) ? relativePath2 : null;
13582
+ return existsSync20(relativePath2) ? relativePath2 : null;
13273
13583
  }
13274
13584
  for (const root of roots) {
13275
13585
  const candidate = resolvePath(root, relativePath2);
13276
- if (existsSync19(candidate)) return candidate;
13586
+ if (existsSync20(candidate)) return candidate;
13277
13587
  }
13278
13588
  return null;
13279
13589
  }
@@ -13386,6 +13696,9 @@ function buildWalkContext(opts) {
13386
13696
  async function processRawNode(raw, provider, wctx, accum, claimedPaths, nextIndex) {
13387
13697
  const bodyHash = sha256(raw.body);
13388
13698
  const frontmatterHash = sha256(canonicalFrontmatter(raw.frontmatter, raw.frontmatterRaw));
13699
+ if (Array.isArray(provider.roots) && provider.roots.length > 0) {
13700
+ if (!matchesAnyRoot(raw.path, provider.roots)) return false;
13701
+ }
13389
13702
  const kind = provider.classify(raw.path, raw.frontmatter);
13390
13703
  if (kind === null) {
13391
13704
  return false;
@@ -13554,6 +13867,26 @@ function recordExtractorRuns(nodePath, ctx, accum) {
13554
13867
  });
13555
13868
  }
13556
13869
  }
13870
+ function matchesAnyRoot(relPath, roots) {
13871
+ for (const r of roots) {
13872
+ if (matchesOneRoot(relPath, r)) return true;
13873
+ }
13874
+ return false;
13875
+ }
13876
+ function matchesOneRoot(relPath, pattern) {
13877
+ if (pattern.endsWith("/**")) return matchesDeepGlob(relPath, pattern.slice(0, -3));
13878
+ if (pattern.endsWith("/*")) return matchesShallowGlob(relPath, pattern.slice(0, -2));
13879
+ return relPath === pattern;
13880
+ }
13881
+ function matchesDeepGlob(relPath, prefix) {
13882
+ if (prefix.length === 0) return true;
13883
+ return relPath === prefix || relPath.startsWith(`${prefix}/`);
13884
+ }
13885
+ function matchesShallowGlob(relPath, prefix) {
13886
+ if (!relPath.startsWith(`${prefix}/`)) return false;
13887
+ const tail = relPath.slice(prefix.length + 1);
13888
+ return tail.length > 0 && !tail.includes("/");
13889
+ }
13557
13890
 
13558
13891
  // kernel/orchestrator/index.ts
13559
13892
  var SCANNED_BY = {
@@ -13622,7 +13955,8 @@ async function runScanInternal(_kernel, options) {
13622
13955
  mergeAnalyzerEmissions(walked, analyzerResult, exts.analyzers);
13623
13956
  const issues = analyzerResult.issues;
13624
13957
  for (const issue of walked.frontmatterIssues) issues.push(issue);
13625
- const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues) : [];
13958
+ const silenced = options.ignoreFilter ? (path) => options.ignoreFilter.ignores(path) : void 0;
13959
+ const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues, silenced) : [];
13626
13960
  const stats = buildScanStats(walked, issues, start);
13627
13961
  const scanCompletedEvent = makeEvent("scan.completed", { stats });
13628
13962
  emitter.emit(scanCompletedEvent);
@@ -13665,7 +13999,7 @@ async function dispatchExtractorCompleted(extractors, emitter, hookDispatcher) {
13665
13999
  function mergeAnalyzerEmissions(walked, analyzerResult, analyzers) {
13666
14000
  for (const c of analyzerResult.contributions) walked.contributions.push(c);
13667
14001
  for (const analyzer of analyzers ?? []) {
13668
- if (analyzer.viewContributions === void 0) continue;
14002
+ if (analyzer.ui === void 0) continue;
13669
14003
  for (const node of walked.nodes) {
13670
14004
  walked.freshlyRunTuples.add(`${analyzer.pluginId}\0${analyzer.id}\0${node.path}`);
13671
14005
  }
@@ -13712,7 +14046,7 @@ function validateRoots(roots) {
13712
14046
  throw new Error(ORCHESTRATOR_TEXTS.runScanRootEmptyArray);
13713
14047
  }
13714
14048
  for (const root of roots) {
13715
- if (!existsSync20(root) || !statSync5(root).isDirectory()) {
14049
+ if (!existsSync21(root) || !statSync7(root).isDirectory()) {
13716
14050
  throw new Error(tx(ORCHESTRATOR_TEXTS.runScanRootMissing, { root }));
13717
14051
  }
13718
14052
  }
@@ -13940,16 +14274,16 @@ function createKernel() {
13940
14274
  }
13941
14275
 
13942
14276
  // kernel/jobs/orphan-files.ts
13943
- import { readdirSync as readdirSync7, statSync as statSync6 } from "fs";
13944
- import { join as join12, resolve as resolve28 } from "path";
14277
+ import { readdirSync as readdirSync8, statSync as statSync8 } from "fs";
14278
+ import { join as join13, resolve as resolve28 } from "path";
13945
14279
  function findOrphanJobFiles(jobsDir, referencedPaths) {
13946
14280
  let entries;
13947
14281
  try {
13948
- const stat2 = statSync6(jobsDir);
14282
+ const stat2 = statSync8(jobsDir);
13949
14283
  if (!stat2.isDirectory()) {
13950
14284
  return { orphanFilePaths: [], referencedCount: referencedPaths.size };
13951
14285
  }
13952
- entries = readdirSync7(jobsDir, { withFileTypes: true });
14286
+ entries = readdirSync8(jobsDir, { withFileTypes: true });
13953
14287
  } catch {
13954
14288
  return { orphanFilePaths: [], referencedCount: referencedPaths.size };
13955
14289
  }
@@ -13959,7 +14293,7 @@ function findOrphanJobFiles(jobsDir, referencedPaths) {
13959
14293
  if (!entry.isFile()) continue;
13960
14294
  const name = entry.name;
13961
14295
  if (!name.endsWith(".md")) continue;
13962
- const abs = resolve28(join12(jobsDir, name));
14296
+ const abs = resolve28(join13(jobsDir, name));
13963
14297
  if (!referencedPaths.has(abs)) orphans.push(abs);
13964
14298
  }
13965
14299
  orphans.sort();
@@ -14033,9 +14367,9 @@ var SCAN_RUNNER_TEXTS = {
14033
14367
  };
14034
14368
 
14035
14369
  // core/runtime/reference-paths-walker.ts
14036
- import { readdirSync as readdirSync8, statSync as statSync7 } from "fs";
14370
+ import { readdirSync as readdirSync9, statSync as statSync9 } from "fs";
14037
14371
  import { homedir as osHomedir2 } from "os";
14038
- import { isAbsolute as isAbsolute7, join as join13, resolve as resolve29 } from "path";
14372
+ import { isAbsolute as isAbsolute7, join as join14, resolve as resolve29 } from "path";
14039
14373
  var REFERENCE_WALK_MAX_FILES = 5e4;
14040
14374
  var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
14041
14375
  "node_modules",
@@ -14043,7 +14377,7 @@ var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
14043
14377
  SKILL_MAP_DIR
14044
14378
  ]);
14045
14379
  function resolveScanPath(raw, cwd) {
14046
- if (raw.startsWith("~/")) return resolve29(join13(osHomedir2(), raw.slice(2)));
14380
+ if (raw.startsWith("~/")) return resolve29(join14(osHomedir2(), raw.slice(2)));
14047
14381
  if (raw === "~") return resolve29(osHomedir2());
14048
14382
  if (isAbsolute7(raw)) return resolve29(raw);
14049
14383
  return resolve29(cwd, raw);
@@ -14068,14 +14402,14 @@ function walkInto(dir, out) {
14068
14402
  if (out.size >= REFERENCE_WALK_MAX_FILES) return true;
14069
14403
  let entries;
14070
14404
  try {
14071
- entries = readdirSync8(dir, { withFileTypes: true });
14405
+ entries = readdirSync9(dir, { withFileTypes: true });
14072
14406
  } catch {
14073
14407
  return false;
14074
14408
  }
14075
14409
  for (const entry of entries) {
14076
14410
  if (out.size >= REFERENCE_WALK_MAX_FILES) return true;
14077
14411
  if (entry.isSymbolicLink()) continue;
14078
- const full = join13(dir, entry.name);
14412
+ const full = join14(dir, entry.name);
14079
14413
  if (entry.isDirectory()) {
14080
14414
  if (SKIPPED_DIR_NAMES.has(entry.name)) continue;
14081
14415
  if (walkInto(full, out)) return true;
@@ -14087,7 +14421,7 @@ function walkInto(dir, out) {
14087
14421
  }
14088
14422
  function safeStat(path) {
14089
14423
  try {
14090
- return statSync7(path);
14424
+ return statSync9(path);
14091
14425
  } catch {
14092
14426
  return null;
14093
14427
  }
@@ -14399,7 +14733,7 @@ var InitCommand = class extends SmCommand {
14399
14733
  async run() {
14400
14734
  const ctx = defaultRuntimeContext();
14401
14735
  const scopeRoot = ctx.cwd;
14402
- const skillMapDir = join14(scopeRoot, SKILL_MAP_DIR);
14736
+ const skillMapDir = join15(scopeRoot, SKILL_MAP_DIR);
14403
14737
  const settingsPath = defaultSettingsPath(scopeRoot);
14404
14738
  const localPath = defaultLocalSettingsPath(scopeRoot);
14405
14739
  const ignorePath = defaultIgnoreFilePath(scopeRoot);
@@ -14439,13 +14773,13 @@ var InitCommand = class extends SmCommand {
14439
14773
  writeFileAtomicExclusive(localPath, "{}\n");
14440
14774
  }
14441
14775
  if (!await pathExists(ignorePath) || this.force) {
14442
- writeFileAtomicExclusive(ignorePath, loadBundledIgnoreText());
14776
+ writeFileAtomicExclusive(ignorePath, loadBundledIgnoreText(), 420);
14443
14777
  }
14444
14778
  const ansi = this.ansiFor("stdout");
14445
14779
  const okGlyph = ansi.green("\u2713");
14446
14780
  const updated = await ensureGitignoreEntries(scopeRoot, GITIGNORE_ENTRIES);
14447
14781
  if (updated) {
14448
- const gitignorePath = join14(scopeRoot, ".gitignore");
14782
+ const gitignorePath = join15(scopeRoot, ".gitignore");
14449
14783
  printer.info(
14450
14784
  GITIGNORE_ENTRIES.length === 1 ? tx(INIT_TEXTS.gitignoreUpdatedSingular, { glyph: okGlyph, path: gitignorePath }) : tx(INIT_TEXTS.gitignoreUpdatedPlural, {
14451
14785
  glyph: okGlyph,
@@ -14484,7 +14818,7 @@ async function dryRunFileMessage(path) {
14484
14818
  }
14485
14819
  async function writeDryRunGitignorePlan(printer, scopeRoot) {
14486
14820
  const wouldAdd = await previewGitignoreEntries(scopeRoot, GITIGNORE_ENTRIES);
14487
- const gitignorePath = join14(scopeRoot, ".gitignore");
14821
+ const gitignorePath = join15(scopeRoot, ".gitignore");
14488
14822
  if (wouldAdd.length === 0) {
14489
14823
  printer.info(tx(INIT_TEXTS.dryRunWouldLeaveGitignoreUnchanged, { path: gitignorePath }));
14490
14824
  } else if (wouldAdd.length === 1) {
@@ -14559,7 +14893,7 @@ async function runFirstScan(scopeRoot, strict, printer, stderr, ansi) {
14559
14893
  return hasErrors ? ExitCode.Issues : ExitCode.Ok;
14560
14894
  }
14561
14895
  async function previewGitignoreEntries(scopeRoot, entries) {
14562
- const path = join14(scopeRoot, ".gitignore");
14896
+ const path = join15(scopeRoot, ".gitignore");
14563
14897
  const body = await pathExists(path) ? await readFile2(path, "utf8") : "";
14564
14898
  const present = new Set(
14565
14899
  body.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"))
@@ -14567,7 +14901,7 @@ async function previewGitignoreEntries(scopeRoot, entries) {
14567
14901
  return entries.filter((entry) => !present.has(entry));
14568
14902
  }
14569
14903
  async function ensureGitignoreEntries(scopeRoot, entries) {
14570
- const path = join14(scopeRoot, ".gitignore");
14904
+ const path = join15(scopeRoot, ".gitignore");
14571
14905
  let body = "";
14572
14906
  if (await pathExists(path)) {
14573
14907
  body = await readFile2(path, "utf8");
@@ -16222,7 +16556,7 @@ var PLUGINS_TEXTS = {
16222
16556
  * Success block printed after scaffolding. Follows the no-em-dash rule
16223
16557
  * across every line.
16224
16558
  */
16225
- createSuccess: "Created {{targetDir}}\nNext:\n - Edit {{pluginId}}/extensions/extractor.js (the extract() body)\n - Run sm scan to see the contribution surface\n - sm plugins slots list: browse other slots\n",
16559
+ createSuccess: "Created {{targetDir}}\nNext:\n - Edit {{pluginId}}/extractors/{{pluginId}}-extractor/index.js (the extract() body)\n - Run sm scan to see the contribution surface\n - sm plugins slots list: browse other slots\n",
16226
16560
  // --- slots list verb -------------------------------------------------
16227
16561
  /** Section header for the view-slots catalogue. */
16228
16562
  slotsListHeaderViewSlots: " View slots ({{count}})\n",
@@ -16282,11 +16616,9 @@ function extensionRowFromBuiltIn(ext, bundle, bundleEnabled, resolveEnabled) {
16282
16616
  id: ext.id,
16283
16617
  kind: ext.kind,
16284
16618
  version: ext.version,
16285
- enabled: bundle.granularity === "bundle" ? bundleEnabled : resolveEnabled(qualifiedExtensionId(bundle.id, ext.id))
16619
+ enabled: bundle.granularity === "bundle" ? bundleEnabled : resolveEnabled(qualifiedExtensionId(bundle.id, ext.id)),
16620
+ description: ext.description ?? ""
16286
16621
  };
16287
- if (ext.description !== void 0) row.description = ext.description;
16288
- if (ext.stability !== void 0) row.stability = ext.stability;
16289
- if (ext.preconditions !== void 0) row.preconditions = ext.preconditions;
16290
16622
  if (ext.entry !== void 0) row.entry = ext.entry;
16291
16623
  return row;
16292
16624
  }
@@ -16688,9 +17020,7 @@ function renderBuiltInExtensionDetail(bundleId, ext, ansi) {
16688
17020
  source: ansi.dim(PLUGINS_TEXTS.sourceBuiltIn)
16689
17021
  });
16690
17022
  const meta = { kind: ext.kind, version: ext.version };
16691
- if (ext.stability !== void 0) meta.stability = ext.stability;
16692
- if (ext.description !== void 0) meta.description = ext.description;
16693
- if (ext.preconditions !== void 0) meta.preconditions = ext.preconditions;
17023
+ if (ext.description) meta.description = ext.description;
16694
17024
  if (ext.entry !== void 0) meta.entry = ext.entry;
16695
17025
  return header + "\n" + renderExtensionFields(meta);
16696
17026
  }
@@ -16991,11 +17321,12 @@ function collectBuiltInApplicableKindWarnings(out, knownKinds) {
16991
17321
  for (const ext of bundle.extensions) {
16992
17322
  if (ext.kind !== "extractor") continue;
16993
17323
  const extractor = ext;
16994
- if (!extractor.applicableKinds) continue;
17324
+ const kinds = extractor.precondition?.kind;
17325
+ if (!kinds || kinds.length === 0) continue;
16995
17326
  appendUnknownKindWarnings(
16996
17327
  out,
16997
17328
  qualifiedExtensionId(bundle.id, extractor.id),
16998
- extractor.applicableKinds,
17329
+ kinds,
16999
17330
  knownKinds
17000
17331
  );
17001
17332
  }
@@ -17005,19 +17336,24 @@ function collectUserApplicableKindWarnings(out, plugins, knownKinds) {
17005
17336
  for (const p of plugins) {
17006
17337
  if (p.status !== "enabled" || !p.extensions) continue;
17007
17338
  for (const ext of p.extensions) {
17008
- if (ext.kind !== "extractor") continue;
17009
- const inst = extensionInstance(ext);
17010
- if (!inst) continue;
17011
- const ak = inst["applicableKinds"];
17012
- if (!Array.isArray(ak)) continue;
17013
- appendUnknownKindWarnings(
17014
- out,
17015
- qualifiedExtensionId(ext.pluginId, ext.id),
17016
- ak,
17017
- knownKinds
17018
- );
17019
- }
17020
- }
17339
+ collectKindsFromExtension(ext, knownKinds, out);
17340
+ }
17341
+ }
17342
+ }
17343
+ function collectKindsFromExtension(ext, knownKinds, out) {
17344
+ if (ext.kind !== "extractor") return;
17345
+ const inst = extensionInstance(ext);
17346
+ if (!inst) return;
17347
+ const pre = inst["precondition"];
17348
+ if (!pre || typeof pre !== "object") return;
17349
+ const kinds = pre.kind;
17350
+ if (!Array.isArray(kinds)) return;
17351
+ appendUnknownKindWarnings(
17352
+ out,
17353
+ qualifiedExtensionId(ext.pluginId, ext.id),
17354
+ kinds,
17355
+ knownKinds
17356
+ );
17021
17357
  }
17022
17358
  function appendUnknownKindWarnings(out, extractorQualifiedId, applicableKinds, knownKinds) {
17023
17359
  for (const k of applicableKinds) {
@@ -17370,8 +17706,8 @@ function resolveBareToggle(id, catalogue, verb, ansi) {
17370
17706
  }
17371
17707
 
17372
17708
  // cli/commands/plugins/create.ts
17373
- import { existsSync as existsSync21, mkdirSync as mkdirSync6, writeFileSync as writeFileSync3 } from "fs";
17374
- import { join as join15, resolve as resolve31 } from "path";
17709
+ import { existsSync as existsSync22, mkdirSync as mkdirSync6, writeFileSync as writeFileSync3 } from "fs";
17710
+ import { join as join16, resolve as resolve31 } from "path";
17375
17711
  import { Command as Command26, Option as Option25 } from "clipanion";
17376
17712
  var PluginsCreateCommand = class extends SmCommand {
17377
17713
  static paths = [["plugins", "create"]];
@@ -17397,8 +17733,8 @@ var PluginsCreateCommand = class extends SmCommand {
17397
17733
  }
17398
17734
  const ctx = defaultRuntimeContext();
17399
17735
  const baseDir = defaultProjectPluginsDir(ctx);
17400
- const targetDir = this.at ? resolve31(this.at) : join15(baseDir, this.pluginId);
17401
- if (existsSync21(targetDir) && !this.force) {
17736
+ const targetDir = this.at ? resolve31(this.at) : join16(baseDir, this.pluginId);
17737
+ if (existsSync22(targetDir) && !this.force) {
17402
17738
  this.printer.error(
17403
17739
  tx(PLUGINS_TEXTS.createRefuseOverwrite, {
17404
17740
  glyph: errGlyph,
@@ -17407,14 +17743,15 @@ var PluginsCreateCommand = class extends SmCommand {
17407
17743
  );
17408
17744
  return ExitCode.Error;
17409
17745
  }
17410
- mkdirSync6(join15(targetDir, "extensions"), { recursive: true });
17746
+ const extractorName = `${this.pluginId}-extractor`;
17747
+ mkdirSync6(join16(targetDir, "extractors", extractorName), { recursive: true });
17411
17748
  const specVersion = installedSpecVersion();
17412
17749
  const manifest = {
17413
17750
  id: this.pluginId,
17414
17751
  version: "0.1.0",
17415
17752
  specCompat: `^${specVersion}`,
17416
17753
  catalogCompat: "^1.0.0",
17417
- extensions: ["./extensions/extractor.js"],
17754
+ granularity: "bundle",
17418
17755
  description: "Generated by `sm plugins create`. Edit to taste.",
17419
17756
  settings: {
17420
17757
  keywords: {
@@ -17427,14 +17764,14 @@ var PluginsCreateCommand = class extends SmCommand {
17427
17764
  }
17428
17765
  };
17429
17766
  writeFileSync3(
17430
- join15(targetDir, "plugin.json"),
17767
+ join16(targetDir, "plugin.json"),
17431
17768
  JSON.stringify(manifest, null, 2) + "\n"
17432
17769
  );
17433
17770
  writeFileSync3(
17434
- join15(targetDir, "extensions", "extractor.js"),
17435
- scaffolderExtractorStub(this.pluginId)
17771
+ join16(targetDir, "extractors", extractorName, "index.js"),
17772
+ scaffolderExtractorStub(extractorName)
17436
17773
  );
17437
- writeFileSync3(join15(targetDir, "README.md"), scaffolderReadme(this.pluginId));
17774
+ writeFileSync3(join16(targetDir, "README.md"), scaffolderReadme(this.pluginId));
17438
17775
  this.printer.data(
17439
17776
  tx(PLUGINS_TEXTS.createSuccess, {
17440
17777
  targetDir: sanitizeForTerminal(targetDir),
@@ -17444,7 +17781,7 @@ var PluginsCreateCommand = class extends SmCommand {
17444
17781
  return ExitCode.Ok;
17445
17782
  }
17446
17783
  };
17447
- function scaffolderExtractorStub(pluginId) {
17784
+ function scaffolderExtractorStub(extractorId) {
17448
17785
  return `/**
17449
17786
  * Generated by \`sm plugins create\`. Edit the extract() body.
17450
17787
  *
@@ -17453,6 +17790,11 @@ function scaffolderExtractorStub(pluginId) {
17453
17790
  * splitting into a named export will surface as \`load-error: default
17454
17791
  * export missing a string \\\`kind\\\` field\`.
17455
17792
  *
17793
+ * Folder convention: this file lives at
17794
+ * \`extractors/${extractorId}/index.js\`. The bundle's plugin.json#/id
17795
+ * provides the qualified id \`<plugin-id>/${extractorId}\`; the loader
17796
+ * injects \`pluginId\` automatically, do NOT hardcode it here.
17797
+ *
17456
17798
  * Declared view contributions (in plugin.json):
17457
17799
  * - 'count' \u2192 slot \`card.footer.left\` (renders as a chip
17458
17800
  * in the left footer of the node card)
@@ -17464,13 +17806,11 @@ function scaffolderExtractorStub(pluginId) {
17464
17806
  * spec/view-slots.md
17465
17807
  */
17466
17808
  export default {
17467
- id: '${pluginId}-extractor',
17468
- pluginId: '${pluginId}',
17809
+ id: '${extractorId}',
17469
17810
  kind: 'extractor',
17470
17811
  version: '0.1.0',
17471
17812
  description: 'Counts configured keywords per node.',
17472
17813
  stability: 'experimental',
17473
- mode: 'deterministic',
17474
17814
  emitsLinkKinds: [],
17475
17815
  defaultConfidence: 'high',
17476
17816
  scope: 'body',
@@ -17937,9 +18277,15 @@ var RefreshCommand = class extends SmCommand {
17937
18277
  continue;
17938
18278
  }
17939
18279
  const fm = node.frontmatter ?? {};
17940
- const applicable = allExtractors.filter(
17941
- (ex) => ex.applicableKinds === void 0 || ex.applicableKinds.includes(node.kind)
17942
- );
18280
+ const applicable = allExtractors.filter((ex) => {
18281
+ const kinds = ex.precondition?.kind;
18282
+ if (!kinds || kinds.length === 0) return true;
18283
+ return kinds.some((qualified) => {
18284
+ const slashIdx = qualified.indexOf("/");
18285
+ const kindOnly = slashIdx === -1 ? qualified : qualified.slice(slashIdx + 1);
18286
+ return kindOnly === node.kind;
18287
+ });
18288
+ });
17943
18289
  for (const extractor of applicable) {
17944
18290
  const records = await runExtractorForEnrichment(extractor, node, body, fm);
17945
18291
  for (const record of records) nodeEnrichments.push(record);
@@ -18049,7 +18395,7 @@ var SCAN_TEXTS = {
18049
18395
  import { Command as Command30, Option as Option28 } from "clipanion";
18050
18396
 
18051
18397
  // core/watcher/runtime.ts
18052
- import { dirname as dirname16 } from "path";
18398
+ import { dirname as dirname17 } from "path";
18053
18399
 
18054
18400
  // core/runtime/fresh-resolver.ts
18055
18401
  async function buildFreshResolver(deps) {
@@ -18274,7 +18620,7 @@ function createWatcherRuntime(opts) {
18274
18620
  roots: [
18275
18621
  cwd,
18276
18622
  // parent of `.skillmapignore`
18277
- dirname16(settingsPath)
18623
+ dirname17(settingsPath)
18278
18624
  // parent of `.skill-map/settings.json`
18279
18625
  ],
18280
18626
  cwd,
@@ -18830,7 +19176,7 @@ function plural(count, word) {
18830
19176
  }
18831
19177
 
18832
19178
  // cli/commands/scan-compare.ts
18833
- import { existsSync as existsSync22, readFileSync as readFileSync17 } from "fs";
19179
+ import { existsSync as existsSync23, readFileSync as readFileSync18 } from "fs";
18834
19180
  import { Command as Command32, Option as Option30 } from "clipanion";
18835
19181
  var ScanCompareCommand = class extends SmCommand {
18836
19182
  static paths = [["scan", "compare-with"]];
@@ -18942,12 +19288,12 @@ var ScanCompareCommand = class extends SmCommand {
18942
19288
  }
18943
19289
  };
18944
19290
  function loadAndValidateDump(path) {
18945
- if (!existsSync22(path)) {
19291
+ if (!existsSync23(path)) {
18946
19292
  throw new Error(tx(SCAN_TEXTS.compareDumpNotFound, { path }));
18947
19293
  }
18948
19294
  let raw;
18949
19295
  try {
18950
- raw = readFileSync17(path, "utf8");
19296
+ raw = readFileSync18(path, "utf8");
18951
19297
  } catch (err) {
18952
19298
  const message = formatErrorMessage(err);
18953
19299
  throw new Error(tx(SCAN_TEXTS.compareDumpReadFailed, { path, message }), { cause: err });
@@ -19072,7 +19418,7 @@ function renderDeltaIssues(issues) {
19072
19418
 
19073
19419
  // cli/commands/serve.ts
19074
19420
  import { spawn as spawn2 } from "child_process";
19075
- import { existsSync as existsSync26 } from "fs";
19421
+ import { existsSync as existsSync27 } from "fs";
19076
19422
  import { Command as Command33, Option as Option31 } from "clipanion";
19077
19423
 
19078
19424
  // cli/util/browser-launch.ts
@@ -19725,7 +20071,7 @@ function contentTypeFor(format) {
19725
20071
  }
19726
20072
 
19727
20073
  // server/health.ts
19728
- import { existsSync as existsSync23 } from "fs";
20074
+ import { existsSync as existsSync24 } from "fs";
19729
20075
  var FALLBACK_SCHEMA_VERSION = "1";
19730
20076
  function buildHealth(deps) {
19731
20077
  return {
@@ -19733,7 +20079,7 @@ function buildHealth(deps) {
19733
20079
  schemaVersion: FALLBACK_SCHEMA_VERSION,
19734
20080
  specVersion: deps.specVersion,
19735
20081
  implVersion: VERSION,
19736
- db: existsSync23(deps.dbPath) ? "present" : "missing",
20082
+ db: existsSync24(deps.dbPath) ? "present" : "missing",
19737
20083
  cwd: deps.cwd,
19738
20084
  dbPath: deps.dbPath
19739
20085
  };
@@ -21097,7 +21443,8 @@ function invokeBump2(node, absPath, body) {
21097
21443
  node,
21098
21444
  nodeAbsolutePath: absPath,
21099
21445
  invoker: "ui",
21100
- now: () => /* @__PURE__ */ new Date()
21446
+ now: () => /* @__PURE__ */ new Date(),
21447
+ settings: {}
21101
21448
  });
21102
21449
  }
21103
21450
  function pickExistingVersion(node) {
@@ -21134,9 +21481,9 @@ function registerUpdateStatusRoute(app, deps) {
21134
21481
  }
21135
21482
 
21136
21483
  // server/static.ts
21137
- import { existsSync as existsSync24 } from "fs";
21484
+ import { existsSync as existsSync25 } from "fs";
21138
21485
  import { readFile as readFile5 } from "fs/promises";
21139
- import { extname, join as join16 } from "path";
21486
+ import { extname, join as join17 } from "path";
21140
21487
  import { serveStatic } from "@hono/node-server/serve-static";
21141
21488
  var INDEX_HTML = "index.html";
21142
21489
  var PLACEHOLDER_HTML = `<!doctype html>
@@ -21188,8 +21535,8 @@ function createSpaFallback(opts) {
21188
21535
  return async (c, _next) => {
21189
21536
  if (c.req.method !== "GET" && c.req.method !== "HEAD") return c.notFound();
21190
21537
  if (opts.uiDist === null) return htmlResponse(c, placeholder);
21191
- const indexPath = join16(opts.uiDist, INDEX_HTML);
21192
- if (!existsSync24(indexPath)) return htmlResponse(c, placeholder);
21538
+ const indexPath = join17(opts.uiDist, INDEX_HTML);
21539
+ if (!existsSync25(indexPath)) return htmlResponse(c, placeholder);
21193
21540
  return fileResponse(c, indexPath);
21194
21541
  };
21195
21542
  }
@@ -21758,10 +22105,10 @@ function validateNoUi(noUi, uiDist) {
21758
22105
  }
21759
22106
 
21760
22107
  // server/paths.ts
21761
- import { existsSync as existsSync25, statSync as statSync8 } from "fs";
21762
- import { dirname as dirname17, isAbsolute as isAbsolute9, join as join17, resolve as resolve34 } from "path";
22108
+ import { existsSync as existsSync26, statSync as statSync10 } from "fs";
22109
+ import { dirname as dirname18, isAbsolute as isAbsolute9, join as join18, resolve as resolve34 } from "path";
21763
22110
  import { fileURLToPath as fileURLToPath5 } from "url";
21764
- var DEFAULT_UI_REL = join17("ui", "dist", "ui", "browser");
22111
+ var DEFAULT_UI_REL = join18("ui", "dist", "ui", "browser");
21765
22112
  var PACKAGE_UI_REL = "ui";
21766
22113
  var INDEX_HTML2 = "index.html";
21767
22114
  function resolveDefaultUiDist(ctx) {
@@ -21773,10 +22120,10 @@ function resolveExplicitUiDist(ctx, raw) {
21773
22120
  return isAbsolute9(raw) ? raw : resolve34(ctx.cwd, raw);
21774
22121
  }
21775
22122
  function isUiBundleDir(path) {
21776
- if (!existsSync25(path)) return false;
22123
+ if (!existsSync26(path)) return false;
21777
22124
  try {
21778
- if (!statSync8(path).isDirectory()) return false;
21779
- return existsSync25(join17(path, INDEX_HTML2));
22125
+ if (!statSync10(path).isDirectory()) return false;
22126
+ return existsSync26(join18(path, INDEX_HTML2));
21780
22127
  } catch {
21781
22128
  return false;
21782
22129
  }
@@ -21784,7 +22131,7 @@ function isUiBundleDir(path) {
21784
22131
  function resolvePackageBundledUi() {
21785
22132
  let here;
21786
22133
  try {
21787
- here = dirname17(fileURLToPath5(import.meta.url));
22134
+ here = dirname18(fileURLToPath5(import.meta.url));
21788
22135
  } catch {
21789
22136
  return null;
21790
22137
  }
@@ -21793,11 +22140,11 @@ function resolvePackageBundledUi() {
21793
22140
  function resolvePackageBundledUiFrom(here) {
21794
22141
  let current = here;
21795
22142
  for (let i = 0; i < 8; i++) {
21796
- const candidate = join17(current, PACKAGE_UI_REL);
22143
+ const candidate = join18(current, PACKAGE_UI_REL);
21797
22144
  if (isUiBundleDir(candidate)) return candidate;
21798
- const distHere = join17(current, "dist", PACKAGE_UI_REL);
22145
+ const distHere = join18(current, "dist", PACKAGE_UI_REL);
21799
22146
  if (isUiBundleDir(distHere)) return distHere;
21800
- const parent = dirname17(current);
22147
+ const parent = dirname18(current);
21801
22148
  if (parent === current) return null;
21802
22149
  current = parent;
21803
22150
  }
@@ -21806,9 +22153,9 @@ function resolvePackageBundledUiFrom(here) {
21806
22153
  function walkUpForUi(startDir) {
21807
22154
  let current = resolve34(startDir);
21808
22155
  for (let i = 0; i < 64; i++) {
21809
- const candidate = join17(current, DEFAULT_UI_REL);
22156
+ const candidate = join18(current, DEFAULT_UI_REL);
21810
22157
  if (isUiBundleDir(candidate)) return candidate;
21811
- const parent = dirname17(current);
22158
+ const parent = dirname18(current);
21812
22159
  if (parent === current) return null;
21813
22160
  current = parent;
21814
22161
  }
@@ -22224,7 +22571,7 @@ var ServeCommand = class extends SmCommand {
22224
22571
  return ExitCode.Error;
22225
22572
  }
22226
22573
  const dbPath = resolveDbPath({ db: this.db, ...runtimeCtx });
22227
- if (this.db !== void 0 && !existsSync26(dbPath)) {
22574
+ if (this.db !== void 0 && !existsSync27(dbPath)) {
22228
22575
  this.printer.info(
22229
22576
  tx(SERVE_TEXTS.dbNotFound, { path: sanitizeForTerminal(dbPath) })
22230
22577
  );
@@ -22720,7 +23067,7 @@ function rankConfidenceForGrouping(c) {
22720
23067
  }
22721
23068
 
22722
23069
  // cli/commands/sidecar.ts
22723
- import { existsSync as existsSync27, unlinkSync as unlinkSync2 } from "fs";
23070
+ import { existsSync as existsSync28, unlinkSync as unlinkSync2 } from "fs";
22724
23071
  import { resolve as resolve35 } from "path";
22725
23072
  import { Command as Command35, Option as Option33 } from "clipanion";
22726
23073
 
@@ -23161,7 +23508,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
23161
23508
  return ExitCode.Error;
23162
23509
  }
23163
23510
  const sidecarAbsPath = sidecarPathFor(absPath);
23164
- if (existsSync27(sidecarAbsPath) && this.force !== true) {
23511
+ if (existsSync28(sidecarAbsPath) && this.force !== true) {
23165
23512
  this.printer.error(
23166
23513
  tx(SIDECAR_TEXTS.annotateExists, {
23167
23514
  glyph: errGlyph,
@@ -23171,7 +23518,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
23171
23518
  );
23172
23519
  return ExitCode.Error;
23173
23520
  }
23174
- if (existsSync27(sidecarAbsPath) && this.force === true) {
23521
+ if (existsSync28(sidecarAbsPath) && this.force === true) {
23175
23522
  try {
23176
23523
  unlinkSync2(sidecarAbsPath);
23177
23524
  } catch (err) {
@@ -23401,9 +23748,9 @@ var STUB_COMMANDS = [
23401
23748
  ];
23402
23749
 
23403
23750
  // cli/commands/tutorial.ts
23404
- import { existsSync as existsSync28, readFileSync as readFileSync18 } from "fs";
23751
+ import { existsSync as existsSync29, readFileSync as readFileSync19 } from "fs";
23405
23752
  import { writeFile as writeFile2 } from "fs/promises";
23406
- import { dirname as dirname18, join as join18, resolve as resolve36 } from "path";
23753
+ import { dirname as dirname19, join as join19, resolve as resolve36 } from "path";
23407
23754
  import { fileURLToPath as fileURLToPath6 } from "url";
23408
23755
  import { Command as Command37, Option as Option35 } from "clipanion";
23409
23756
 
@@ -23500,7 +23847,7 @@ var TutorialCommand = class extends SmCommand {
23500
23847
  }
23501
23848
  const variant = rawVariant ?? DEFAULT_VARIANT;
23502
23849
  const spec = VARIANT_SPECS[variant];
23503
- const target = join18(ctx.cwd, spec.filename);
23850
+ const target = join19(ctx.cwd, spec.filename);
23504
23851
  if (await pathExists(target) && !this.force) {
23505
23852
  this.printer.error(
23506
23853
  tx(TUTORIAL_TEXTS.alreadyExists, {
@@ -23574,7 +23921,7 @@ function loadBundledTutorialText(variant) {
23574
23921
  }
23575
23922
  function readTutorialFromDisk(variant) {
23576
23923
  const spec = VARIANT_SPECS[variant];
23577
- const here = dirname18(fileURLToPath6(import.meta.url));
23924
+ const here = dirname19(fileURLToPath6(import.meta.url));
23578
23925
  const candidates = [
23579
23926
  // dev: src/cli/commands/ → repo-root .claude/skills/<slug>/SKILL.md
23580
23927
  resolve36(here, "../../..", spec.sourcePath),
@@ -23584,8 +23931,8 @@ function readTutorialFromDisk(variant) {
23584
23931
  resolve36(here, "../cli/tutorial", spec.bundledName)
23585
23932
  ];
23586
23933
  for (const candidate of candidates) {
23587
- if (existsSync28(candidate)) {
23588
- return readFileSync18(candidate, "utf8");
23934
+ if (existsSync29(candidate)) {
23935
+ return readFileSync19(candidate, "utf8");
23589
23936
  }
23590
23937
  }
23591
23938
  throw new Error(`SKILL.md not found in any candidate location (last tried: ${candidates[candidates.length - 1]})`);
@@ -23758,7 +24105,7 @@ await lifecycleDispatcher.dispatch(
23758
24105
  process.exit(exitCode);
23759
24106
  function resolveBareDefault() {
23760
24107
  const ctx = defaultRuntimeContext();
23761
- if (existsSync29(defaultProjectDbPath(ctx))) {
24108
+ if (existsSync30(defaultProjectDbPath(ctx))) {
23762
24109
  return ["serve"];
23763
24110
  }
23764
24111
  process.stderr.write(tx(ENTRY_TEXTS.bareNoProject, { cwd: ctx.cwd }));