@skill-map/cli 0.17.0 → 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.
@@ -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.17.0",
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.17.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,28 +523,28 @@ 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);
387
549
  registerProviderAuxiliarySchemas(ajv, providers);
388
550
  const compiled = /* @__PURE__ */ new Map();
@@ -419,11 +581,11 @@ function registerProviderAuxiliarySchemas(ajv, providers) {
419
581
  }
420
582
  }
421
583
  }
422
- function resolveSpecRoot() {
423
- const require2 = createRequire2(import.meta.url);
584
+ function resolveSpecRoot2() {
585
+ const require2 = createRequire4(import.meta.url);
424
586
  try {
425
587
  const indexPath = require2.resolve("@skill-map/spec/index.json");
426
- return dirname(indexPath);
588
+ return dirname3(indexPath);
427
589
  } catch {
428
590
  throw new Error(
429
591
  "@skill-map/spec not resolvable \u2014 ensure the workspace is linked or the package is installed."
@@ -443,6 +605,195 @@ var ORCHESTRATOR_TEXTS = {
443
605
  runScanRootMissing: "runScan: root path '{{root}}' does not exist or is not a directory"
444
606
  };
445
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
+
446
797
  // kernel/orchestrator.ts
447
798
  var SCANNED_BY = {
448
799
  name: "skill-map",
@@ -505,14 +856,23 @@ async function runScanInternal(_kernel, options) {
505
856
  emitter.emit(evt);
506
857
  await hookDispatcher.dispatch("extractor.completed", evt);
507
858
  }
508
- 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
+ );
509
869
  for (const issue of walked.frontmatterIssues) issues.push(issue);
510
870
  const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues) : [];
511
871
  const stats = {
512
872
  // `filesSkipped` is "files walked but not classified by any Provider".
513
873
  // Today every walked file IS classified by its Provider (the `claude`
514
874
  // Provider's `classify()` always returns a kind, falling back to
515
- // `'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
516
876
  // spec-conformant; meaningful once multiple Providers compete.
517
877
  filesWalked: walked.filesWalked,
518
878
  filesSkipped: 0,
@@ -547,7 +907,7 @@ function validateRoots(roots) {
547
907
  throw new Error(ORCHESTRATOR_TEXTS.runScanRootEmptyArray);
548
908
  }
549
909
  for (const root of roots) {
550
- if (!existsSync2(root) || !statSync(root).isDirectory()) {
910
+ if (!existsSync6(root) || !statSync2(root).isDirectory()) {
551
911
  throw new Error(tx(ORCHESTRATOR_TEXTS.runScanRootMissing, { root }));
552
912
  }
553
913
  }
@@ -751,6 +1111,7 @@ async function walkAndExtract(opts) {
751
1111
  const frontmatterIssues = [];
752
1112
  const enrichmentBuffer = /* @__PURE__ */ new Map();
753
1113
  const extractorRuns = [];
1114
+ const sidecarRoots = /* @__PURE__ */ new Map();
754
1115
  let filesWalked = 0;
755
1116
  let index = 0;
756
1117
  const walkOptions = ignoreFilter ? { ignoreFilter } : {};
@@ -762,13 +1123,16 @@ async function walkAndExtract(opts) {
762
1123
  else shortIdToQualified.set(ex.id, [qualified]);
763
1124
  }
764
1125
  for (const provider of providers) {
765
- for await (const raw of provider.walk(roots, walkOptions)) {
1126
+ for await (const raw of resolveProviderWalk(provider)(roots, walkOptions)) {
766
1127
  filesWalked += 1;
767
1128
  const bodyHash = sha256(raw.body);
768
1129
  const frontmatterHash = sha256(canonicalFrontmatter(raw.frontmatter, raw.frontmatterRaw));
769
1130
  const priorNode = priorNodesByPath.get(raw.path);
770
1131
  const nodeHashCacheEligible = enableCache && prior !== null && priorNode !== void 0 && priorNode.bodyHash === bodyHash && priorNode.frontmatterHash === frontmatterHash;
771
1132
  const kind = provider.classify(raw.path, raw.frontmatter);
1133
+ if (kind === null) {
1134
+ continue;
1135
+ }
772
1136
  index += 1;
773
1137
  const cacheDecision = computeCacheDecision({
774
1138
  extractors,
@@ -796,10 +1160,21 @@ async function walkAndExtract(opts) {
796
1160
  priorLinksByOriginating,
797
1161
  priorFrontmatterIssuesByNode
798
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
+ );
799
1173
  nodes.push(reused.node);
800
1174
  cachedPaths.add(reused.node.path);
801
1175
  for (const link of reused.internalLinks) internalLinks.push(link);
802
1176
  for (const issue of reused.frontmatterIssues) frontmatterIssues.push(issue);
1177
+ for (const issue of reusedSidecarIssues) frontmatterIssues.push(issue);
803
1178
  for (const run of reused.extractorRuns) extractorRuns.push(run);
804
1179
  emitter.emit(makeEvent("scan.progress", { index, path: raw.path, kind, cached: true }));
805
1180
  continue;
@@ -835,6 +1210,15 @@ async function walkAndExtract(opts) {
835
1210
  nodes.push(node);
836
1211
  for (const issue of fresh.frontmatterIssues) frontmatterIssues.push(issue);
837
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);
838
1222
  emitter.emit(makeEvent("scan.progress", {
839
1223
  index,
840
1224
  path: raw.path,
@@ -869,6 +1253,7 @@ async function walkAndExtract(opts) {
869
1253
  }
870
1254
  }
871
1255
  }
1256
+ const orphanSidecars = discoverOrphanSidecars(roots);
872
1257
  return {
873
1258
  nodes,
874
1259
  internalLinks,
@@ -877,7 +1262,9 @@ async function walkAndExtract(opts) {
877
1262
  frontmatterIssues,
878
1263
  filesWalked,
879
1264
  enrichments: [...enrichmentBuffer.values()],
880
- extractorRuns
1265
+ extractorRuns,
1266
+ orphanSidecars,
1267
+ sidecarRoots
881
1268
  };
882
1269
  }
883
1270
  function reuseCachedLink(link, shortIdToQualified, cachedQualifiedIds, applicableQualifiedIds) {
@@ -906,10 +1293,20 @@ function reuseCachedLink(link, shortIdToQualified, cachedQualifiedIds, applicabl
906
1293
  if (obsoleteSources.length === 0) return link;
907
1294
  return { ...link, sources: cachedSources };
908
1295
  }
909
- async function runRules(rules, nodes, internalLinks, emitter, hookDispatcher) {
1296
+ async function runRules(rules, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, emitter, hookDispatcher) {
910
1297
  const issues = [];
1298
+ const ruleOrphans = orphanSidecars.map((o) => ({
1299
+ relativePath: o.relativePath,
1300
+ expectedMdPath: o.expectedMdPath
1301
+ }));
911
1302
  for (const rule of rules) {
912
- 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
+ });
913
1310
  for (const issue of emitted) {
914
1311
  const validated = validateIssue(rule, issue, emitter);
915
1312
  if (validated) issues.push(validated);
@@ -1090,7 +1487,7 @@ function makeHookDispatcher(hooks, emitter) {
1090
1487
  await hook.on(ctx);
1091
1488
  } catch (err) {
1092
1489
  const qualifiedId = qualifiedExtensionId(hook.pluginId, hook.id);
1093
- const message = err instanceof Error ? err.message : String(err);
1490
+ const message = formatErrorMessage(err);
1094
1491
  emitter.emit(
1095
1492
  makeEvent("extension.error", {
1096
1493
  kind: "hook-error",
@@ -1135,7 +1532,6 @@ function buildHookContext(_hook, trigger, event) {
1135
1532
  function buildNode(args) {
1136
1533
  const bytesFrontmatter = Buffer.byteLength(args.frontmatterRaw, "utf8");
1137
1534
  const bytesBody = Buffer.byteLength(args.body, "utf8");
1138
- const metadata = pickMetadata(args.frontmatter);
1139
1535
  const node = {
1140
1536
  path: args.path,
1141
1537
  kind: args.kind,
@@ -1153,9 +1549,8 @@ function buildNode(args) {
1153
1549
  frontmatter: args.frontmatter,
1154
1550
  title: pickString(args.frontmatter["name"]),
1155
1551
  description: pickString(args.frontmatter["description"]),
1156
- stability: pickStability(metadata?.["stability"]),
1157
- version: pickString(metadata?.["version"]),
1158
- author: pickString(args.frontmatter["author"])
1552
+ stability: null,
1553
+ version: null
1159
1554
  };
1160
1555
  if (args.encoder) {
1161
1556
  node.tokens = countTokens(args.encoder, args.frontmatterRaw, args.body);
@@ -1176,24 +1571,88 @@ function canonicalFrontmatter(parsed, raw) {
1176
1571
  if (!hasParsedKeys && hasRawText) {
1177
1572
  return raw;
1178
1573
  }
1179
- return yaml.dump(parsed, {
1574
+ return yaml4.dump(parsed, {
1180
1575
  sortKeys: true,
1181
1576
  lineWidth: -1,
1182
1577
  noRefs: true,
1183
1578
  noCompatMode: true
1184
1579
  });
1185
1580
  }
1186
- function pickMetadata(fm) {
1187
- const m = fm["metadata"];
1188
- 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;
1189
1621
  }
1190
- function pickString(value) {
1191
- return typeof value === "string" && value.length > 0 ? value : null;
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
+ }
1192
1633
  }
1193
- function pickStability(value) {
1194
- if (value === "experimental" || value === "stable" || value === "deprecated") return value;
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
+ }
1195
1642
  return null;
1196
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;
1652
+ }
1653
+ function pickString(value) {
1654
+ return typeof value === "string" && value.length > 0 ? value : null;
1655
+ }
1197
1656
  function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, store) {
1198
1657
  const scope = extractor.scope;
1199
1658
  return {
@@ -1340,16 +1799,16 @@ function assignSafe(target, source) {
1340
1799
  }
1341
1800
 
1342
1801
  // kernel/scan/watcher.ts
1343
- import { resolve as resolve3, relative as relative2, sep } from "path";
1802
+ import { resolve as resolve6, relative as relative4, sep as sep3 } from "path";
1344
1803
  import chokidar from "chokidar";
1345
1804
  function createChokidarWatcher(opts) {
1346
- const absRoots = opts.roots.map((r) => resolve3(opts.cwd, r));
1805
+ const absRoots = opts.roots.map((r) => resolve6(opts.cwd, r));
1347
1806
  const ignoreFilterOpt = opts.ignoreFilter;
1348
1807
  const getFilter = ignoreFilterOpt === void 0 ? void 0 : typeof ignoreFilterOpt === "function" ? ignoreFilterOpt : () => ignoreFilterOpt;
1349
1808
  const ignored = getFilter ? (path) => {
1350
1809
  const filter = getFilter();
1351
1810
  if (!filter) return false;
1352
- const rel = relativePathFromRoots(path, absRoots);
1811
+ const rel = relativePathFromRoots2(path, absRoots);
1353
1812
  if (rel === null) return false;
1354
1813
  return filter.ignores(rel);
1355
1814
  } : void 0;
@@ -1433,12 +1892,12 @@ function createChokidarWatcher(opts) {
1433
1892
  };
1434
1893
  return { ready, close };
1435
1894
  }
1436
- function relativePathFromRoots(absolute, absRoots) {
1895
+ function relativePathFromRoots2(absolute, absRoots) {
1437
1896
  for (const root of absRoots) {
1438
- const rel = relative2(root, absolute);
1897
+ const rel = relative4(root, absolute);
1439
1898
  if (rel === "" || rel === ".") return "";
1440
- if (!rel.startsWith("..") && !rel.startsWith(`..${sep}`)) {
1441
- return rel.split(sep).join("/");
1899
+ if (!rel.startsWith("..") && !rel.startsWith(`..${sep3}`)) {
1900
+ return rel.split(sep3).join("/");
1442
1901
  }
1443
1902
  }
1444
1903
  return null;
@@ -1689,7 +2148,16 @@ function parseLogLevel(value) {
1689
2148
 
1690
2149
  // kernel/index.ts
1691
2150
  function createKernel() {
1692
- 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
+ };
1693
2161
  }
1694
2162
  export {
1695
2163
  DuplicateExtensionError,