@skill-map/cli 0.16.6 → 0.18.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 (41) hide show
  1. package/dist/cli/tutorial/sm-tutorial.md +8 -0
  2. package/dist/cli.js +8324 -5644
  3. package/dist/cli.js.map +1 -1
  4. package/dist/conformance/index.js +36 -14
  5. package/dist/conformance/index.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +540 -61
  8. package/dist/index.js.map +1 -1
  9. package/dist/kernel/index.d.ts +443 -87
  10. package/dist/kernel/index.js +540 -61
  11. package/dist/kernel/index.js.map +1 -1
  12. package/dist/migrations/002_sidecar_columns.sql +53 -0
  13. package/dist/migrations/003_drop_node_author.sql +20 -0
  14. package/dist/migrations/004_sidecar_root_json.sql +23 -0
  15. package/dist/migrations/005_node_favorites.sql +20 -0
  16. package/dist/ui/chunk-3R7E3HPC.js +7 -0
  17. package/dist/ui/chunk-JKJGGXCS.js +1025 -0
  18. package/dist/ui/chunk-SX2A3WBX.js +247 -0
  19. package/dist/ui/chunk-TWZHUCAT.js +237 -0
  20. package/dist/ui/chunk-UJOZYR5I.js +1 -0
  21. package/dist/ui/chunk-WTAL2RK4.js +1 -0
  22. package/dist/ui/chunk-Z3UJHHTC.js +3091 -0
  23. package/dist/ui/index.html +2 -2
  24. package/dist/ui/main-AAYGMON4.js +1 -0
  25. package/dist/ui/skill-map-mark-dark.svg +8 -0
  26. package/dist/ui/skill-map-mark-light.svg +8 -0
  27. package/dist/ui/{styles-TCK5JUQE.css → styles-CBPFNGXA.css} +1 -1
  28. package/migrations/002_sidecar_columns.sql +53 -0
  29. package/migrations/003_drop_node_author.sql +20 -0
  30. package/migrations/004_sidecar_root_json.sql +23 -0
  31. package/migrations/005_node_favorites.sql +20 -0
  32. package/package.json +6 -6
  33. package/dist/ui/chunk-7PFTODKS.js +0 -1031
  34. package/dist/ui/chunk-A4RBO3TD.js +0 -38
  35. package/dist/ui/chunk-KPEISNOV.js +0 -819
  36. package/dist/ui/chunk-NKC42FI7.js +0 -210
  37. package/dist/ui/chunk-S5C4U3I3.js +0 -2403
  38. package/dist/ui/chunk-TG6IWVEC.js +0 -54
  39. package/dist/ui/chunk-TGJQE3TH.js +0 -54
  40. package/dist/ui/chunk-UGEECDPV.js +0 -1
  41. package/dist/ui/main-XSGTD7FQ.js +0 -1
package/dist/index.js CHANGED
@@ -94,15 +94,16 @@ var Registry = class {
94
94
 
95
95
  // kernel/orchestrator.ts
96
96
  import { createHash } from "crypto";
97
- import { existsSync as existsSync2, statSync } from "fs";
97
+ import { existsSync as existsSync6, statSync as statSync2 } from "fs";
98
+ import { isAbsolute as isAbsolute2, resolve as resolvePath } from "path";
98
99
  import { Tiktoken } from "js-tiktoken/lite";
99
100
  import cl100k_base from "js-tiktoken/ranks/cl100k_base";
100
- import yaml from "js-yaml";
101
+ import yaml4 from "js-yaml";
101
102
 
102
103
  // package.json
103
104
  var package_default = {
104
105
  name: "@skill-map/cli",
105
- version: "0.16.6",
106
+ version: "0.18.0",
106
107
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
107
108
  license: "MIT",
108
109
  type: "module",
@@ -160,15 +161,15 @@ var package_default = {
160
161
  "pretest:ci": "tsup",
161
162
  "pretest:coverage": "tsup",
162
163
  "pretest:coverage:html": "tsup",
163
- test: "tsc --noEmit && node --import tsx --test --test-reporter=spec 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts'",
164
- "test:ci": "tsc --noEmit && node --import tsx --test 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts'",
165
- "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'",
166
- "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'",
164
+ test: "tsc --noEmit && node --import tsx --test --test-reporter=spec 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts' 'server/**/*.test.ts'",
165
+ "test:ci": "tsc --noEmit && node --import tsx --test 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts' 'server/**/*.test.ts'",
166
+ "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'",
167
+ "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'",
167
168
  clean: "rm -rf dist coverage"
168
169
  },
169
170
  dependencies: {
170
171
  "@hono/node-server": "2.0.1",
171
- "@skill-map/spec": "0.16.0",
172
+ "@skill-map/spec": "0.18.0",
172
173
  ajv: "8.18.0",
173
174
  "ajv-formats": "3.0.1",
174
175
  chokidar: "5.0.0",
@@ -205,6 +206,174 @@ var package_default = {
205
206
  }
206
207
  };
207
208
 
209
+ // kernel/sidecar/parse.ts
210
+ import { existsSync, readFileSync } from "fs";
211
+ import { dirname, resolve } from "path";
212
+ import { createRequire } from "module";
213
+ import { Ajv2020 } from "ajv/dist/2020.js";
214
+ import yaml from "js-yaml";
215
+
216
+ // kernel/util/ajv-interop.ts
217
+ import addFormatsModule from "ajv-formats";
218
+ var addFormats = addFormatsModule.default ?? addFormatsModule;
219
+ function applyAjvFormats(ajv) {
220
+ addFormats(ajv);
221
+ }
222
+
223
+ // kernel/sidecar/parse.ts
224
+ function readSidecarFor(mdAbsolutePath) {
225
+ const sidecarPath = sidecarPathFor(mdAbsolutePath);
226
+ if (!existsSync(sidecarPath)) {
227
+ return { parsed: null, present: false, issues: [] };
228
+ }
229
+ let raw;
230
+ try {
231
+ raw = readFileSync(sidecarPath, "utf8");
232
+ } catch (err) {
233
+ return {
234
+ parsed: null,
235
+ present: true,
236
+ issues: [{ message: `cannot read ${sidecarPath}: ${err.message}` }]
237
+ };
238
+ }
239
+ let parsedYaml;
240
+ try {
241
+ parsedYaml = yaml.load(raw);
242
+ } catch (err) {
243
+ return {
244
+ parsed: null,
245
+ present: true,
246
+ issues: [{ message: `malformed YAML in ${sidecarPath}: ${err.message}` }]
247
+ };
248
+ }
249
+ if (!isPlainObject(parsedYaml)) {
250
+ return {
251
+ parsed: null,
252
+ present: true,
253
+ issues: [{ message: `sidecar root must be a YAML mapping at ${sidecarPath}` }]
254
+ };
255
+ }
256
+ const sidecarValidator = getSidecarValidator();
257
+ if (!sidecarValidator(parsedYaml)) {
258
+ const errors = (sidecarValidator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
259
+ return {
260
+ parsed: null,
261
+ present: true,
262
+ issues: [{ message: `sidecar schema validation failed at ${sidecarPath}: ${errors}` }]
263
+ };
264
+ }
265
+ const root = parsedYaml;
266
+ const forBlock = root["for"];
267
+ const annotationsRaw = root["annotations"];
268
+ const annotations = isPlainObject(annotationsRaw) ? Object.keys(annotationsRaw).length === 0 ? null : annotationsRaw : null;
269
+ return {
270
+ parsed: {
271
+ filePath: sidecarPath,
272
+ forBodyHash: String(forBlock["bodyHash"]),
273
+ forFrontmatterHash: String(forBlock["frontmatterHash"]),
274
+ forPath: String(forBlock["path"]),
275
+ annotations,
276
+ raw: root
277
+ },
278
+ present: true,
279
+ issues: []
280
+ };
281
+ }
282
+ function sidecarPathFor(mdAbsolutePath) {
283
+ if (mdAbsolutePath.endsWith(".md")) {
284
+ return `${mdAbsolutePath.slice(0, -".md".length)}.sm`;
285
+ }
286
+ return `${mdAbsolutePath}.sm`;
287
+ }
288
+ function isPlainObject(value) {
289
+ return value !== null && typeof value === "object" && !Array.isArray(value);
290
+ }
291
+ var cachedSidecarValidator = null;
292
+ function getSidecarValidator() {
293
+ if (cachedSidecarValidator) return cachedSidecarValidator;
294
+ const ajv = new Ajv2020({ strict: false, allErrors: true, allowUnionTypes: true });
295
+ applyAjvFormats(ajv);
296
+ const specRoot = resolveSpecRoot();
297
+ const annotationsSchema = JSON.parse(
298
+ readFileSync(resolve(specRoot, "schemas/annotations.schema.json"), "utf8")
299
+ );
300
+ const sidecarSchema = JSON.parse(
301
+ readFileSync(resolve(specRoot, "schemas/sidecar.schema.json"), "utf8")
302
+ );
303
+ ajv.addSchema(annotationsSchema);
304
+ cachedSidecarValidator = ajv.compile(sidecarSchema);
305
+ return cachedSidecarValidator;
306
+ }
307
+ function resolveSpecRoot() {
308
+ const require2 = createRequire(import.meta.url);
309
+ try {
310
+ const indexPath = require2.resolve("@skill-map/spec/index.json");
311
+ return dirname(indexPath);
312
+ } catch {
313
+ throw new Error(
314
+ "@skill-map/spec not resolvable \u2014 sidecar reader cannot load schemas."
315
+ );
316
+ }
317
+ }
318
+
319
+ // kernel/sidecar/drift.ts
320
+ function computeDriftStatus(args) {
321
+ const bodyDrift = args.storedBodyHash !== args.liveBodyHash;
322
+ const fmDrift = args.storedFrontmatterHash !== args.liveFrontmatterHash;
323
+ if (bodyDrift && fmDrift) return "stale-both";
324
+ if (bodyDrift) return "stale-body";
325
+ if (fmDrift) return "stale-frontmatter";
326
+ return "fresh";
327
+ }
328
+
329
+ // kernel/sidecar/discover-orphans.ts
330
+ import { existsSync as existsSync2, readdirSync, statSync } from "fs";
331
+ import { join, relative, sep } from "path";
332
+ function discoverOrphanSidecars(roots, shouldSkip) {
333
+ const out = [];
334
+ for (const root of roots) {
335
+ walk(root, root, shouldSkip ?? (() => false), out);
336
+ }
337
+ return out;
338
+ }
339
+ function walk(root, current, shouldSkip, out) {
340
+ let entries;
341
+ try {
342
+ entries = readdirSync(current, { withFileTypes: true, encoding: "utf8" });
343
+ } catch {
344
+ return;
345
+ }
346
+ for (const entry of entries) {
347
+ const full = join(current, entry.name);
348
+ const rel = relative(root, full).split(sep).join("/");
349
+ if (shouldSkip(rel)) continue;
350
+ if (entry.isSymbolicLink()) continue;
351
+ if (entry.isDirectory()) {
352
+ walk(root, full, shouldSkip, out);
353
+ continue;
354
+ }
355
+ if (!entry.isFile()) continue;
356
+ if (!entry.name.endsWith(".sm")) continue;
357
+ const expectedMd = `${full.slice(0, -".sm".length)}.md`;
358
+ if (existsSync2(expectedMd) && safeIsFile(expectedMd)) continue;
359
+ out.push({ sidecarPath: full, relativePath: rel, expectedMdPath: expectedMd });
360
+ }
361
+ }
362
+ function safeIsFile(path) {
363
+ try {
364
+ return statSync(path).isFile();
365
+ } catch {
366
+ return false;
367
+ }
368
+ }
369
+
370
+ // kernel/sidecar/store.ts
371
+ import { existsSync as existsSync3, readFileSync as readFileSync2, renameSync, writeFileSync, unlinkSync } from "fs";
372
+ import { dirname as dirname2, resolve as resolve2 } from "path";
373
+ import { createRequire as createRequire2 } from "module";
374
+ import { Ajv2020 as Ajv20202 } from "ajv/dist/2020.js";
375
+ import yaml2 from "js-yaml";
376
+
208
377
  // kernel/adapters/in-memory-progress.ts
209
378
  var InMemoryProgressEmitter = class {
210
379
  #listeners = /* @__PURE__ */ new Set();
@@ -253,20 +422,13 @@ function getActiveLogger() {
253
422
  }
254
423
 
255
424
  // kernel/adapters/plugin-loader.ts
256
- import { createRequire } from "module";
257
- import { existsSync, readFileSync, readdirSync } from "fs";
258
- import { isAbsolute, join, relative, resolve } from "path";
425
+ import { createRequire as createRequire3 } from "module";
426
+ import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
427
+ import { isAbsolute, join as join2, relative as relative2, resolve as resolve3 } from "path";
259
428
  import { pathToFileURL } from "url";
260
- import { Ajv2020 } from "ajv/dist/2020.js";
429
+ import { Ajv2020 as Ajv20203 } from "ajv/dist/2020.js";
261
430
  import semver from "semver";
262
431
 
263
- // kernel/util/ajv-interop.ts
264
- import addFormatsModule from "ajv-formats";
265
- var addFormats = addFormatsModule.default ?? addFormatsModule;
266
- function applyAjvFormats(ajv) {
267
- addFormats(ajv);
268
- }
269
-
270
432
  // kernel/i18n/plugin-store.texts.ts
271
433
  var PLUGIN_STORE_TEXTS = {
272
434
  kvValidationFailed: "plugin '{{pluginId}}' ctx.store.set('{{key}}', value): value violates declared schema ({{schemaPath}}) \u2014 {{errors}}",
@@ -361,29 +523,30 @@ var KNOWN_KINDS = /* @__PURE__ */ new Set(["provider", "extractor", "rule", "act
361
523
  var KNOWN_KINDS_LIST = [...KNOWN_KINDS].join(" / ");
362
524
  var HOOKABLE_TRIGGERS_LIST = HOOK_TRIGGERS.join(", ");
363
525
  function installedSpecVersion() {
364
- const require2 = createRequire(import.meta.url);
526
+ const require2 = createRequire3(import.meta.url);
365
527
  const indexPath = require2.resolve("@skill-map/spec/index.json");
366
- const pkgPath = resolve(indexPath, "..", "package.json");
367
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
528
+ const pkgPath = resolve3(indexPath, "..", "package.json");
529
+ const pkg = JSON.parse(readFileSync3(pkgPath, "utf8"));
368
530
  return pkg.version;
369
531
  }
370
532
 
371
533
  // kernel/adapters/schema-validators.ts
372
- import { readFileSync as readFileSync2 } from "fs";
373
- import { dirname, resolve as resolve2 } from "path";
374
- import { createRequire as createRequire2 } from "module";
375
- import { Ajv2020 as Ajv20202 } from "ajv/dist/2020.js";
534
+ import { readFileSync as readFileSync4 } from "fs";
535
+ import { dirname as dirname3, resolve as resolve4 } from "path";
536
+ import { createRequire as createRequire4 } from "module";
537
+ import { Ajv2020 as Ajv20204 } from "ajv/dist/2020.js";
376
538
  function buildProviderFrontmatterValidator(providers) {
377
- const specRoot = resolveSpecRoot();
378
- const ajv = new Ajv20202({
539
+ const specRoot = resolveSpecRoot2();
540
+ const ajv = new Ajv20204({
379
541
  strict: false,
380
542
  allErrors: true,
381
543
  allowUnionTypes: true
382
544
  });
383
545
  applyAjvFormats(ajv);
384
- const baseFile = resolve2(specRoot, "schemas/frontmatter/base.schema.json");
385
- const baseSchema = JSON.parse(readFileSync2(baseFile, "utf8"));
546
+ const baseFile = resolve4(specRoot, "schemas/frontmatter/base.schema.json");
547
+ const baseSchema = JSON.parse(readFileSync4(baseFile, "utf8"));
386
548
  ajv.addSchema(baseSchema);
549
+ registerProviderAuxiliarySchemas(ajv, providers);
387
550
  const compiled = /* @__PURE__ */ new Map();
388
551
  for (const provider of providers) {
389
552
  for (const [kind, entry] of Object.entries(provider.kinds)) {
@@ -408,11 +571,21 @@ function formatError(err) {
408
571
  const path = err.instancePath || "(root)";
409
572
  return `${path} ${err.message ?? err.keyword}`;
410
573
  }
411
- function resolveSpecRoot() {
412
- const require2 = createRequire2(import.meta.url);
574
+ function registerProviderAuxiliarySchemas(ajv, providers) {
575
+ for (const provider of providers) {
576
+ if (!provider.schemas) continue;
577
+ for (const aux of provider.schemas) {
578
+ const auxJson = aux;
579
+ if (typeof auxJson.$id === "string" && ajv.getSchema(auxJson.$id)) continue;
580
+ ajv.addSchema(aux);
581
+ }
582
+ }
583
+ }
584
+ function resolveSpecRoot2() {
585
+ const require2 = createRequire4(import.meta.url);
413
586
  try {
414
587
  const indexPath = require2.resolve("@skill-map/spec/index.json");
415
- return dirname(indexPath);
588
+ return dirname3(indexPath);
416
589
  } catch {
417
590
  throw new Error(
418
591
  "@skill-map/spec not resolvable \u2014 ensure the workspace is linked or the package is installed."
@@ -432,6 +605,195 @@ var ORCHESTRATOR_TEXTS = {
432
605
  runScanRootMissing: "runScan: root path '{{root}}' does not exist or is not a directory"
433
606
  };
434
607
 
608
+ // kernel/util/format-error.ts
609
+ function formatErrorMessage(err) {
610
+ return err instanceof Error ? err.message : String(err);
611
+ }
612
+
613
+ // kernel/scan/walk-content.ts
614
+ import { readFile, readdir, stat } from "fs/promises";
615
+ import { join as join3, relative as relative3, sep as sep2 } from "path";
616
+
617
+ // kernel/scan/ignore.ts
618
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
619
+ import { dirname as dirname4, resolve as resolve5 } from "path";
620
+ import { fileURLToPath } from "url";
621
+ import ignoreFactory from "ignore";
622
+ function buildIgnoreFilter(opts = {}) {
623
+ const ig = ignoreFactory();
624
+ if (opts.includeDefaults !== false) {
625
+ ig.add(loadDefaultsText());
626
+ }
627
+ if (opts.configIgnore && opts.configIgnore.length > 0) {
628
+ ig.add(opts.configIgnore);
629
+ }
630
+ if (opts.ignoreFileText && opts.ignoreFileText.length > 0) {
631
+ ig.add(opts.ignoreFileText);
632
+ }
633
+ return {
634
+ ignores(relativePath) {
635
+ if (relativePath === "" || relativePath === "." || relativePath === "./") {
636
+ return false;
637
+ }
638
+ const normalised = relativePath.replace(/^\.\//, "").replace(/\\/g, "/").replace(/^\//, "");
639
+ if (normalised === "") return false;
640
+ return ig.ignores(normalised);
641
+ }
642
+ };
643
+ }
644
+ var cachedDefaults = null;
645
+ function loadDefaultsText() {
646
+ if (cachedDefaults !== null) return cachedDefaults;
647
+ cachedDefaults = readDefaultsFromDisk();
648
+ return cachedDefaults;
649
+ }
650
+ function readDefaultsFromDisk() {
651
+ const here = dirname4(fileURLToPath(import.meta.url));
652
+ const candidates = [
653
+ resolve5(here, "../../config/defaults/skillmapignore"),
654
+ // src/kernel/scan/ → src/config/defaults/
655
+ resolve5(here, "../config/defaults/skillmapignore"),
656
+ // dist/cli.js → dist/config/defaults/ (siblings)
657
+ resolve5(here, "config/defaults/skillmapignore")
658
+ ];
659
+ for (const candidate of candidates) {
660
+ if (existsSync5(candidate)) {
661
+ try {
662
+ return readFileSync5(candidate, "utf8");
663
+ } catch {
664
+ }
665
+ }
666
+ }
667
+ return "";
668
+ }
669
+
670
+ // kernel/scan/parsers/frontmatter-yaml.ts
671
+ import yaml3 from "js-yaml";
672
+ var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
673
+ var FORBIDDEN_FRONTMATTER_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
674
+ var frontmatterYamlParser = {
675
+ id: "frontmatter-yaml",
676
+ parse(raw, _path) {
677
+ const match = FRONTMATTER_RE.exec(raw);
678
+ if (!match) return { frontmatterRaw: "", frontmatter: {}, body: raw };
679
+ const frontmatterRaw = match[1];
680
+ const body = match[2];
681
+ const parsed = {};
682
+ try {
683
+ const doc = yaml3.load(frontmatterRaw, { schema: yaml3.JSON_SCHEMA });
684
+ if (doc && typeof doc === "object" && !Array.isArray(doc)) {
685
+ for (const [k, v] of Object.entries(doc)) {
686
+ if (FORBIDDEN_FRONTMATTER_KEYS.has(k)) continue;
687
+ parsed[k] = v;
688
+ }
689
+ }
690
+ } catch {
691
+ }
692
+ return { frontmatterRaw, frontmatter: parsed, body };
693
+ }
694
+ };
695
+
696
+ // kernel/scan/parsers/plain.ts
697
+ var plainParser = {
698
+ id: "plain",
699
+ parse(raw, _path) {
700
+ return { frontmatter: {}, frontmatterRaw: "", body: raw };
701
+ }
702
+ };
703
+
704
+ // kernel/scan/parsers/index.ts
705
+ var REGISTRY = /* @__PURE__ */ new Map([
706
+ [frontmatterYamlParser.id, frontmatterYamlParser],
707
+ [plainParser.id, plainParser]
708
+ ]);
709
+ var FROZEN_IDS = new Set(REGISTRY.keys());
710
+ function getParser(id) {
711
+ return REGISTRY.get(id);
712
+ }
713
+
714
+ // kernel/scan/walk-content.ts
715
+ var UnknownParserError = class extends Error {
716
+ constructor(parserId) {
717
+ super(`Unknown parser id '${parserId}'. Built-in parsers: 'frontmatter-yaml', 'plain'.`);
718
+ this.name = "UnknownParserError";
719
+ }
720
+ };
721
+ async function* walkContent(roots, options) {
722
+ const parser = getParser(options.parser);
723
+ if (!parser) throw new UnknownParserError(options.parser);
724
+ const filter = options.ignoreFilter ?? buildIgnoreFilter();
725
+ const extensions = options.extensions;
726
+ for (const root of roots) {
727
+ for await (const file of walkRoot(root, root, filter, extensions)) {
728
+ const relPath = relative3(root, file).split(sep2).join("/");
729
+ let raw;
730
+ try {
731
+ raw = await readFile(file, "utf8");
732
+ } catch {
733
+ continue;
734
+ }
735
+ const parsed = parser.parse(raw, relPath);
736
+ yield {
737
+ path: relPath,
738
+ body: parsed.body,
739
+ frontmatterRaw: parsed.frontmatterRaw,
740
+ frontmatter: parsed.frontmatter
741
+ };
742
+ }
743
+ }
744
+ }
745
+ async function* walkRoot(root, current, filter, extensions) {
746
+ let entries;
747
+ try {
748
+ entries = await readdir(current, { withFileTypes: true, encoding: "utf8" });
749
+ } catch {
750
+ return;
751
+ }
752
+ for (const entry of entries) {
753
+ const name = entry.name;
754
+ const full = join3(current, name);
755
+ const rel = relative3(root, full).split(sep2).join("/");
756
+ if (filter.ignores(rel)) continue;
757
+ if (entry.isSymbolicLink()) continue;
758
+ if (entry.isDirectory()) {
759
+ yield* walkRoot(root, full, filter, extensions);
760
+ } else if (entry.isFile() && hasMatchingExtension(name, extensions)) {
761
+ try {
762
+ const s = await stat(full);
763
+ if (s.isFile()) yield full;
764
+ } catch {
765
+ }
766
+ }
767
+ }
768
+ }
769
+ function hasMatchingExtension(name, extensions) {
770
+ for (const ext of extensions) {
771
+ if (name.endsWith(ext)) return true;
772
+ }
773
+ return false;
774
+ }
775
+
776
+ // kernel/extensions/provider.ts
777
+ var DEFAULT_READ_CONFIG = Object.freeze({
778
+ extensions: Object.freeze([".md"]),
779
+ parser: "frontmatter-yaml"
780
+ });
781
+ function resolveProviderWalk(provider) {
782
+ if (provider.walk) {
783
+ const walk2 = provider.walk.bind(provider);
784
+ return walk2;
785
+ }
786
+ const read = provider.read ?? DEFAULT_READ_CONFIG;
787
+ return (roots, options) => {
788
+ const walkOptions = {
789
+ extensions: read.extensions,
790
+ parser: read.parser
791
+ };
792
+ if (options?.ignoreFilter) walkOptions.ignoreFilter = options.ignoreFilter;
793
+ return walkContent(roots, walkOptions);
794
+ };
795
+ }
796
+
435
797
  // kernel/orchestrator.ts
436
798
  var SCANNED_BY = {
437
799
  name: "skill-map",
@@ -494,14 +856,23 @@ async function runScanInternal(_kernel, options) {
494
856
  emitter.emit(evt);
495
857
  await hookDispatcher.dispatch("extractor.completed", evt);
496
858
  }
497
- const issues = await runRules(exts.rules, walked.nodes, walked.internalLinks, emitter, hookDispatcher);
859
+ const issues = await runRules(
860
+ exts.rules,
861
+ walked.nodes,
862
+ walked.internalLinks,
863
+ walked.orphanSidecars,
864
+ walked.sidecarRoots,
865
+ options.annotationContributions ?? [],
866
+ emitter,
867
+ hookDispatcher
868
+ );
498
869
  for (const issue of walked.frontmatterIssues) issues.push(issue);
499
870
  const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues) : [];
500
871
  const stats = {
501
872
  // `filesSkipped` is "files walked but not classified by any Provider".
502
873
  // Today every walked file IS classified by its Provider (the `claude`
503
874
  // Provider's `classify()` always returns a kind, falling back to
504
- // `'note'`), so this is always 0. Wired now so the field shape is
875
+ // `'markdown'`), so this is always 0. Wired now so the field shape is
505
876
  // spec-conformant; meaningful once multiple Providers compete.
506
877
  filesWalked: walked.filesWalked,
507
878
  filesSkipped: 0,
@@ -536,7 +907,7 @@ function validateRoots(roots) {
536
907
  throw new Error(ORCHESTRATOR_TEXTS.runScanRootEmptyArray);
537
908
  }
538
909
  for (const root of roots) {
539
- if (!existsSync2(root) || !statSync(root).isDirectory()) {
910
+ if (!existsSync6(root) || !statSync2(root).isDirectory()) {
540
911
  throw new Error(tx(ORCHESTRATOR_TEXTS.runScanRootMissing, { root }));
541
912
  }
542
913
  }
@@ -740,6 +1111,7 @@ async function walkAndExtract(opts) {
740
1111
  const frontmatterIssues = [];
741
1112
  const enrichmentBuffer = /* @__PURE__ */ new Map();
742
1113
  const extractorRuns = [];
1114
+ const sidecarRoots = /* @__PURE__ */ new Map();
743
1115
  let filesWalked = 0;
744
1116
  let index = 0;
745
1117
  const walkOptions = ignoreFilter ? { ignoreFilter } : {};
@@ -751,13 +1123,16 @@ async function walkAndExtract(opts) {
751
1123
  else shortIdToQualified.set(ex.id, [qualified]);
752
1124
  }
753
1125
  for (const provider of providers) {
754
- for await (const raw of provider.walk(roots, walkOptions)) {
1126
+ for await (const raw of resolveProviderWalk(provider)(roots, walkOptions)) {
755
1127
  filesWalked += 1;
756
1128
  const bodyHash = sha256(raw.body);
757
1129
  const frontmatterHash = sha256(canonicalFrontmatter(raw.frontmatter, raw.frontmatterRaw));
758
1130
  const priorNode = priorNodesByPath.get(raw.path);
759
1131
  const nodeHashCacheEligible = enableCache && prior !== null && priorNode !== void 0 && priorNode.bodyHash === bodyHash && priorNode.frontmatterHash === frontmatterHash;
760
1132
  const kind = provider.classify(raw.path, raw.frontmatter);
1133
+ if (kind === null) {
1134
+ continue;
1135
+ }
761
1136
  index += 1;
762
1137
  const cacheDecision = computeCacheDecision({
763
1138
  extractors,
@@ -785,10 +1160,21 @@ async function walkAndExtract(opts) {
785
1160
  priorLinksByOriginating,
786
1161
  priorFrontmatterIssuesByNode
787
1162
  });
1163
+ reused.node.stability = null;
1164
+ reused.node.version = null;
1165
+ const reusedSidecarIssues = resolveAndApplySidecar(
1166
+ reused.node,
1167
+ raw.path,
1168
+ roots,
1169
+ bodyHash,
1170
+ frontmatterHash,
1171
+ sidecarRoots
1172
+ );
788
1173
  nodes.push(reused.node);
789
1174
  cachedPaths.add(reused.node.path);
790
1175
  for (const link of reused.internalLinks) internalLinks.push(link);
791
1176
  for (const issue of reused.frontmatterIssues) frontmatterIssues.push(issue);
1177
+ for (const issue of reusedSidecarIssues) frontmatterIssues.push(issue);
792
1178
  for (const run of reused.extractorRuns) extractorRuns.push(run);
793
1179
  emitter.emit(makeEvent("scan.progress", { index, path: raw.path, kind, cached: true }));
794
1180
  continue;
@@ -824,6 +1210,15 @@ async function walkAndExtract(opts) {
824
1210
  nodes.push(node);
825
1211
  for (const issue of fresh.frontmatterIssues) frontmatterIssues.push(issue);
826
1212
  }
1213
+ const sidecarIssues = resolveAndApplySidecar(
1214
+ node,
1215
+ raw.path,
1216
+ roots,
1217
+ bodyHash,
1218
+ frontmatterHash,
1219
+ sidecarRoots
1220
+ );
1221
+ for (const issue of sidecarIssues) frontmatterIssues.push(issue);
827
1222
  emitter.emit(makeEvent("scan.progress", {
828
1223
  index,
829
1224
  path: raw.path,
@@ -858,6 +1253,7 @@ async function walkAndExtract(opts) {
858
1253
  }
859
1254
  }
860
1255
  }
1256
+ const orphanSidecars = discoverOrphanSidecars(roots);
861
1257
  return {
862
1258
  nodes,
863
1259
  internalLinks,
@@ -866,7 +1262,9 @@ async function walkAndExtract(opts) {
866
1262
  frontmatterIssues,
867
1263
  filesWalked,
868
1264
  enrichments: [...enrichmentBuffer.values()],
869
- extractorRuns
1265
+ extractorRuns,
1266
+ orphanSidecars,
1267
+ sidecarRoots
870
1268
  };
871
1269
  }
872
1270
  function reuseCachedLink(link, shortIdToQualified, cachedQualifiedIds, applicableQualifiedIds) {
@@ -895,10 +1293,20 @@ function reuseCachedLink(link, shortIdToQualified, cachedQualifiedIds, applicabl
895
1293
  if (obsoleteSources.length === 0) return link;
896
1294
  return { ...link, sources: cachedSources };
897
1295
  }
898
- async function runRules(rules, nodes, internalLinks, emitter, hookDispatcher) {
1296
+ async function runRules(rules, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, emitter, hookDispatcher) {
899
1297
  const issues = [];
1298
+ const ruleOrphans = orphanSidecars.map((o) => ({
1299
+ relativePath: o.relativePath,
1300
+ expectedMdPath: o.expectedMdPath
1301
+ }));
900
1302
  for (const rule of rules) {
901
- const emitted = await rule.evaluate({ nodes, links: internalLinks });
1303
+ const emitted = await rule.evaluate({
1304
+ nodes,
1305
+ links: internalLinks,
1306
+ orphanSidecars: ruleOrphans,
1307
+ sidecarRoots,
1308
+ annotationContributions
1309
+ });
902
1310
  for (const issue of emitted) {
903
1311
  const validated = validateIssue(rule, issue, emitter);
904
1312
  if (validated) issues.push(validated);
@@ -1079,7 +1487,7 @@ function makeHookDispatcher(hooks, emitter) {
1079
1487
  await hook.on(ctx);
1080
1488
  } catch (err) {
1081
1489
  const qualifiedId = qualifiedExtensionId(hook.pluginId, hook.id);
1082
- const message = err instanceof Error ? err.message : String(err);
1490
+ const message = formatErrorMessage(err);
1083
1491
  emitter.emit(
1084
1492
  makeEvent("extension.error", {
1085
1493
  kind: "hook-error",
@@ -1124,7 +1532,6 @@ function buildHookContext(_hook, trigger, event) {
1124
1532
  function buildNode(args) {
1125
1533
  const bytesFrontmatter = Buffer.byteLength(args.frontmatterRaw, "utf8");
1126
1534
  const bytesBody = Buffer.byteLength(args.body, "utf8");
1127
- const metadata = pickMetadata(args.frontmatter);
1128
1535
  const node = {
1129
1536
  path: args.path,
1130
1537
  kind: args.kind,
@@ -1142,9 +1549,8 @@ function buildNode(args) {
1142
1549
  frontmatter: args.frontmatter,
1143
1550
  title: pickString(args.frontmatter["name"]),
1144
1551
  description: pickString(args.frontmatter["description"]),
1145
- stability: pickStability(metadata?.["stability"]),
1146
- version: pickString(metadata?.["version"]),
1147
- author: pickString(args.frontmatter["author"])
1552
+ stability: null,
1553
+ version: null
1148
1554
  };
1149
1555
  if (args.encoder) {
1150
1556
  node.tokens = countTokens(args.encoder, args.frontmatterRaw, args.body);
@@ -1165,24 +1571,88 @@ function canonicalFrontmatter(parsed, raw) {
1165
1571
  if (!hasParsedKeys && hasRawText) {
1166
1572
  return raw;
1167
1573
  }
1168
- return yaml.dump(parsed, {
1574
+ return yaml4.dump(parsed, {
1169
1575
  sortKeys: true,
1170
1576
  lineWidth: -1,
1171
1577
  noRefs: true,
1172
1578
  noCompatMode: true
1173
1579
  });
1174
1580
  }
1175
- function pickMetadata(fm) {
1176
- const m = fm["metadata"];
1177
- return m && typeof m === "object" && !Array.isArray(m) ? m : null;
1581
+ function resolveAndApplySidecar(node, relativePath, roots, liveBodyHash, liveFrontmatterHash, sidecarRoots) {
1582
+ const issues = [];
1583
+ const mdAbs = resolveAbsoluteMdPath(relativePath, roots);
1584
+ if (mdAbs === null) {
1585
+ node.sidecar = { present: false };
1586
+ return issues;
1587
+ }
1588
+ const result = readSidecarFor(mdAbs);
1589
+ if (!result.present) {
1590
+ node.sidecar = { present: false };
1591
+ return issues;
1592
+ }
1593
+ if (result.parsed === null) {
1594
+ node.sidecar = { present: true, status: null, annotations: null, root: null };
1595
+ for (const parseIssue of result.issues) {
1596
+ issues.push({
1597
+ ruleId: "invalid-sidecar",
1598
+ severity: "warn",
1599
+ nodeIds: [node.path],
1600
+ message: parseIssue.message,
1601
+ data: { sidecarPath: relativePathFromRoots(mdAbs, roots) }
1602
+ });
1603
+ }
1604
+ return issues;
1605
+ }
1606
+ const status = computeDriftStatus({
1607
+ storedBodyHash: result.parsed.forBodyHash,
1608
+ storedFrontmatterHash: result.parsed.forFrontmatterHash,
1609
+ liveBodyHash,
1610
+ liveFrontmatterHash
1611
+ });
1612
+ applyAnnotationsOverlay(node, result.parsed);
1613
+ node.sidecar = {
1614
+ present: true,
1615
+ status,
1616
+ annotations: result.parsed.annotations,
1617
+ root: result.parsed.raw
1618
+ };
1619
+ sidecarRoots.set(node.path, result.parsed.raw);
1620
+ return issues;
1621
+ }
1622
+ function applyAnnotationsOverlay(node, parsed) {
1623
+ const annotations = parsed.annotations;
1624
+ if (annotations === null) return;
1625
+ const stability = annotations["stability"];
1626
+ if (stability === "experimental" || stability === "stable" || stability === "deprecated") {
1627
+ node.stability = stability;
1628
+ }
1629
+ const version = annotations["version"];
1630
+ if (typeof version === "number" && Number.isInteger(version) && version >= 1) {
1631
+ node.version = version;
1632
+ }
1633
+ }
1634
+ function resolveAbsoluteMdPath(relativePath, roots) {
1635
+ if (isAbsolute2(relativePath)) {
1636
+ return existsSync6(relativePath) ? relativePath : null;
1637
+ }
1638
+ for (const root of roots) {
1639
+ const candidate = resolvePath(root, relativePath);
1640
+ if (existsSync6(candidate)) return candidate;
1641
+ }
1642
+ return null;
1643
+ }
1644
+ function relativePathFromRoots(absolutePath, roots) {
1645
+ for (const root of roots) {
1646
+ const abs = resolvePath(root);
1647
+ if (absolutePath.startsWith(`${abs}/`) || absolutePath.startsWith(`${abs}\\`)) {
1648
+ return absolutePath.slice(abs.length + 1).split(/[\\/]/).join("/");
1649
+ }
1650
+ }
1651
+ return absolutePath;
1178
1652
  }
1179
1653
  function pickString(value) {
1180
1654
  return typeof value === "string" && value.length > 0 ? value : null;
1181
1655
  }
1182
- function pickStability(value) {
1183
- if (value === "experimental" || value === "stable" || value === "deprecated") return value;
1184
- return null;
1185
- }
1186
1656
  function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, store) {
1187
1657
  const scope = extractor.scope;
1188
1658
  return {
@@ -1329,16 +1799,16 @@ function assignSafe(target, source) {
1329
1799
  }
1330
1800
 
1331
1801
  // kernel/scan/watcher.ts
1332
- import { resolve as resolve3, relative as relative2, sep } from "path";
1802
+ import { resolve as resolve6, relative as relative4, sep as sep3 } from "path";
1333
1803
  import chokidar from "chokidar";
1334
1804
  function createChokidarWatcher(opts) {
1335
- const absRoots = opts.roots.map((r) => resolve3(opts.cwd, r));
1805
+ const absRoots = opts.roots.map((r) => resolve6(opts.cwd, r));
1336
1806
  const ignoreFilterOpt = opts.ignoreFilter;
1337
1807
  const getFilter = ignoreFilterOpt === void 0 ? void 0 : typeof ignoreFilterOpt === "function" ? ignoreFilterOpt : () => ignoreFilterOpt;
1338
1808
  const ignored = getFilter ? (path) => {
1339
1809
  const filter = getFilter();
1340
1810
  if (!filter) return false;
1341
- const rel = relativePathFromRoots(path, absRoots);
1811
+ const rel = relativePathFromRoots2(path, absRoots);
1342
1812
  if (rel === null) return false;
1343
1813
  return filter.ignores(rel);
1344
1814
  } : void 0;
@@ -1422,12 +1892,12 @@ function createChokidarWatcher(opts) {
1422
1892
  };
1423
1893
  return { ready, close };
1424
1894
  }
1425
- function relativePathFromRoots(absolute, absRoots) {
1895
+ function relativePathFromRoots2(absolute, absRoots) {
1426
1896
  for (const root of absRoots) {
1427
- const rel = relative2(root, absolute);
1897
+ const rel = relative4(root, absolute);
1428
1898
  if (rel === "" || rel === ".") return "";
1429
- if (!rel.startsWith("..") && !rel.startsWith(`..${sep}`)) {
1430
- return rel.split(sep).join("/");
1899
+ if (!rel.startsWith("..") && !rel.startsWith(`..${sep3}`)) {
1900
+ return rel.split(sep3).join("/");
1431
1901
  }
1432
1902
  }
1433
1903
  return null;
@@ -1678,7 +2148,16 @@ function parseLogLevel(value) {
1678
2148
 
1679
2149
  // kernel/index.ts
1680
2150
  function createKernel() {
1681
- return { registry: new Registry() };
2151
+ let annotationKeys = Object.freeze([]);
2152
+ return {
2153
+ registry: new Registry(),
2154
+ getRegisteredAnnotationKeys() {
2155
+ return annotationKeys;
2156
+ },
2157
+ setRegisteredAnnotationKeys(entries) {
2158
+ annotationKeys = Object.freeze([...entries]);
2159
+ }
2160
+ };
1682
2161
  }
1683
2162
  export {
1684
2163
  DuplicateExtensionError,