@skill-map/cli 0.17.0 → 0.19.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.19.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.19.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 identityBlock = root["identity"];
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
+ identityBodyHash: String(identityBlock["bodyHash"]),
273
+ identityFrontmatterHash: String(identityBlock["frontmatterHash"]),
274
+ identityPath: String(identityBlock["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,140 @@ 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";
538
+ var SCHEMA_FILES = {
539
+ node: "schemas/node.schema.json",
540
+ link: "schemas/link.schema.json",
541
+ issue: "schemas/issue.schema.json",
542
+ "scan-result": "schemas/scan-result.schema.json",
543
+ "execution-record": "schemas/execution-record.schema.json",
544
+ "project-config": "schemas/project-config.schema.json",
545
+ "plugins-registry": "schemas/plugins-registry.schema.json",
546
+ job: "schemas/job.schema.json",
547
+ "report-base": "schemas/report-base.schema.json",
548
+ "conformance-case": "schemas/conformance-case.schema.json",
549
+ "history-stats": "schemas/history-stats.schema.json",
550
+ "extension-provider": "schemas/extensions/provider.schema.json",
551
+ "extension-extractor": "schemas/extensions/extractor.schema.json",
552
+ "extension-rule": "schemas/extensions/rule.schema.json",
553
+ "extension-action": "schemas/extensions/action.schema.json",
554
+ "extension-formatter": "schemas/extensions/formatter.schema.json",
555
+ "extension-hook": "schemas/extensions/hook.schema.json",
556
+ "frontmatter-base": "schemas/frontmatter/base.schema.json"
557
+ };
558
+ var SUPPORTING_SCHEMAS = [
559
+ "schemas/extensions/base.schema.json",
560
+ "schemas/frontmatter/base.schema.json",
561
+ "schemas/summaries/security-scanner.schema.json",
562
+ "schemas/view-contracts.schema.json",
563
+ "schemas/input-types.schema.json"
564
+ ];
565
+ var cachedValidators = null;
566
+ function loadSchemaValidators() {
567
+ if (cachedValidators !== null) return cachedValidators;
568
+ cachedValidators = buildSchemaValidators();
569
+ return cachedValidators;
570
+ }
571
+ function buildSchemaValidators() {
572
+ const specRoot = resolveSpecRoot2();
573
+ const ajv = new Ajv20204({
574
+ strict: false,
575
+ allErrors: true,
576
+ allowUnionTypes: true
577
+ });
578
+ applyAjvFormats(ajv);
579
+ for (const rel of SUPPORTING_SCHEMAS) {
580
+ const file = resolve4(specRoot, rel);
581
+ if (!existsSyncSafe(file)) continue;
582
+ const schema = JSON.parse(readFileSync4(file, "utf8"));
583
+ ajv.addSchema(schema);
584
+ }
585
+ const validators = /* @__PURE__ */ new Map();
586
+ for (const [name, rel] of Object.entries(SCHEMA_FILES)) {
587
+ const file = resolve4(specRoot, rel);
588
+ const schema = JSON.parse(readFileSync4(file, "utf8"));
589
+ const byId = typeof schema.$id === "string" ? ajv.getSchema(schema.$id) : void 0;
590
+ validators.set(name, byId ?? ajv.compile(schema));
591
+ }
592
+ const extensionByKind = {
593
+ provider: "extension-provider",
594
+ extractor: "extension-extractor",
595
+ rule: "extension-rule",
596
+ action: "extension-action",
597
+ formatter: "extension-formatter",
598
+ hook: "extension-hook"
599
+ };
600
+ const pluginManifestValidator = ajv.compile({
601
+ $ref: "https://skill-map.dev/spec/v0/plugins-registry.schema.json#/$defs/PluginManifest"
602
+ });
603
+ const contributionValidators = /* @__PURE__ */ new Map();
604
+ const VIEW_CONTRACTS_ID = "https://skill-map.dev/spec/v0/view-contracts.schema.json";
605
+ function getContributionValidator(contract) {
606
+ const existing = contributionValidators.get(contract);
607
+ if (existing) return existing;
608
+ const ref = `${VIEW_CONTRACTS_ID}#/$defs/payloads/${contract}`;
609
+ let compiled;
610
+ try {
611
+ compiled = ajv.compile({ $ref: ref });
612
+ } catch {
613
+ return null;
614
+ }
615
+ contributionValidators.set(contract, compiled);
616
+ return compiled;
617
+ }
618
+ return {
619
+ getValidator(name) {
620
+ const v = validators.get(name);
621
+ if (!v) throw new Error(`Unknown schema: ${name}`);
622
+ return v;
623
+ },
624
+ validatorForExtension(kind) {
625
+ return validators.get(extensionByKind[kind]);
626
+ },
627
+ validate(name, data) {
628
+ const v = validators.get(name);
629
+ if (!v) throw new Error(`Unknown schema: ${name}`);
630
+ if (v(data)) return { ok: true, data };
631
+ const errors = (v.errors ?? []).map(formatError).join("; ");
632
+ return { ok: false, errors };
633
+ },
634
+ validatePluginManifest(data) {
635
+ if (pluginManifestValidator(data)) return { ok: true, data };
636
+ const errors = (pluginManifestValidator.errors ?? []).map(formatError).join("; ");
637
+ return { ok: false, errors };
638
+ },
639
+ validateContributionPayload(contract, payload) {
640
+ const validator = getContributionValidator(contract);
641
+ if (!validator) {
642
+ return { ok: false, errors: "unknown-contract" };
643
+ }
644
+ if (validator(payload)) return { ok: true };
645
+ const errors = (validator.errors ?? []).map(formatError).join("; ");
646
+ return { ok: false, errors };
647
+ }
648
+ };
649
+ }
376
650
  function buildProviderFrontmatterValidator(providers) {
377
- const specRoot = resolveSpecRoot();
378
- const ajv = new Ajv20202({
651
+ const specRoot = resolveSpecRoot2();
652
+ const ajv = new Ajv20204({
379
653
  strict: false,
380
654
  allErrors: true,
381
655
  allowUnionTypes: true
382
656
  });
383
657
  applyAjvFormats(ajv);
384
- const baseFile = resolve2(specRoot, "schemas/frontmatter/base.schema.json");
385
- const baseSchema = JSON.parse(readFileSync2(baseFile, "utf8"));
658
+ const baseFile = resolve4(specRoot, "schemas/frontmatter/base.schema.json");
659
+ const baseSchema = JSON.parse(readFileSync4(baseFile, "utf8"));
386
660
  ajv.addSchema(baseSchema);
387
661
  registerProviderAuxiliarySchemas(ajv, providers);
388
662
  const compiled = /* @__PURE__ */ new Map();
@@ -419,17 +693,25 @@ function registerProviderAuxiliarySchemas(ajv, providers) {
419
693
  }
420
694
  }
421
695
  }
422
- function resolveSpecRoot() {
423
- const require2 = createRequire2(import.meta.url);
696
+ function resolveSpecRoot2() {
697
+ const require2 = createRequire4(import.meta.url);
424
698
  try {
425
699
  const indexPath = require2.resolve("@skill-map/spec/index.json");
426
- return dirname(indexPath);
700
+ return dirname3(indexPath);
427
701
  } catch {
428
702
  throw new Error(
429
703
  "@skill-map/spec not resolvable \u2014 ensure the workspace is linked or the package is installed."
430
704
  );
431
705
  }
432
706
  }
707
+ function existsSyncSafe(path) {
708
+ try {
709
+ readFileSync4(path, "utf8");
710
+ return true;
711
+ } catch {
712
+ return false;
713
+ }
714
+ }
433
715
 
434
716
  // kernel/i18n/orchestrator.texts.ts
435
717
  var ORCHESTRATOR_TEXTS = {
@@ -439,10 +721,201 @@ var ORCHESTRATOR_TEXTS = {
439
721
  frontmatterMalformedMissingClose: "Frontmatter in {{path}} opens with `---` but never closes \u2014 no matching `---` line at column 0 was found. The file was scanned as body-only and every metadata field was silently lost. Add a closing `---` line below the metadata block.",
440
722
  extensionErrorLinkKindNotDeclared: 'Extractor "{{extractorId}}" emitted a link of kind "{{linkKind}}" outside its declared `emitsLinkKinds` set [{{declaredKinds}}]. Link dropped.',
441
723
  extensionErrorIssueInvalidSeverity: `Rule "{{ruleId}}" emitted an issue with invalid severity {{severity}} (allowed: 'error' | 'warn' | 'info'). Issue dropped.`,
724
+ extensionErrorContributionUnknownId: 'Extractor "{{extractorId}}" emitted contribution "{{contributionId}}" on {{nodePath}} but did not declare it in its `viewContributions` map. Contribution dropped.',
725
+ extensionErrorContributionPayloadInvalid: 'Extractor "{{extractorId}}" emitted contribution "{{contributionId}}" on {{nodePath}}; payload failed the "{{contract}}" schema: {{errors}}. Contribution dropped.',
442
726
  runScanRootEmptyArray: "runScan: roots must contain at least one path (spec requires minItems: 1)",
443
727
  runScanRootMissing: "runScan: root path '{{root}}' does not exist or is not a directory"
444
728
  };
445
729
 
730
+ // kernel/util/format-error.ts
731
+ function formatErrorMessage(err) {
732
+ return err instanceof Error ? err.message : String(err);
733
+ }
734
+
735
+ // kernel/scan/walk-content.ts
736
+ import { readFile, readdir, stat } from "fs/promises";
737
+ import { join as join3, relative as relative3, sep as sep2 } from "path";
738
+
739
+ // kernel/scan/ignore.ts
740
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
741
+ import { dirname as dirname4, resolve as resolve5 } from "path";
742
+ import { fileURLToPath } from "url";
743
+ import ignoreFactory from "ignore";
744
+ function buildIgnoreFilter(opts = {}) {
745
+ const ig = ignoreFactory();
746
+ if (opts.includeDefaults !== false) {
747
+ ig.add(loadDefaultsText());
748
+ }
749
+ if (opts.configIgnore && opts.configIgnore.length > 0) {
750
+ ig.add(opts.configIgnore);
751
+ }
752
+ if (opts.ignoreFileText && opts.ignoreFileText.length > 0) {
753
+ ig.add(opts.ignoreFileText);
754
+ }
755
+ return {
756
+ ignores(relativePath) {
757
+ if (relativePath === "" || relativePath === "." || relativePath === "./") {
758
+ return false;
759
+ }
760
+ const normalised = relativePath.replace(/^\.\//, "").replace(/\\/g, "/").replace(/^\//, "");
761
+ if (normalised === "") return false;
762
+ return ig.ignores(normalised);
763
+ }
764
+ };
765
+ }
766
+ var cachedDefaults = null;
767
+ function loadDefaultsText() {
768
+ if (cachedDefaults !== null) return cachedDefaults;
769
+ cachedDefaults = readDefaultsFromDisk();
770
+ return cachedDefaults;
771
+ }
772
+ function readDefaultsFromDisk() {
773
+ const here = dirname4(fileURLToPath(import.meta.url));
774
+ const candidates = [
775
+ resolve5(here, "../../config/defaults/skillmapignore"),
776
+ // src/kernel/scan/ → src/config/defaults/
777
+ resolve5(here, "../config/defaults/skillmapignore"),
778
+ // dist/cli.js → dist/config/defaults/ (siblings)
779
+ resolve5(here, "config/defaults/skillmapignore")
780
+ ];
781
+ for (const candidate of candidates) {
782
+ if (existsSync5(candidate)) {
783
+ try {
784
+ return readFileSync5(candidate, "utf8");
785
+ } catch {
786
+ }
787
+ }
788
+ }
789
+ return "";
790
+ }
791
+
792
+ // kernel/scan/parsers/frontmatter-yaml.ts
793
+ import yaml3 from "js-yaml";
794
+ var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
795
+ var FORBIDDEN_FRONTMATTER_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
796
+ var frontmatterYamlParser = {
797
+ id: "frontmatter-yaml",
798
+ parse(raw, _path) {
799
+ const match = FRONTMATTER_RE.exec(raw);
800
+ if (!match) return { frontmatterRaw: "", frontmatter: {}, body: raw };
801
+ const frontmatterRaw = match[1];
802
+ const body = match[2];
803
+ const parsed = {};
804
+ try {
805
+ const doc = yaml3.load(frontmatterRaw, { schema: yaml3.JSON_SCHEMA });
806
+ if (doc && typeof doc === "object" && !Array.isArray(doc)) {
807
+ for (const [k, v] of Object.entries(doc)) {
808
+ if (FORBIDDEN_FRONTMATTER_KEYS.has(k)) continue;
809
+ parsed[k] = v;
810
+ }
811
+ }
812
+ } catch {
813
+ }
814
+ return { frontmatterRaw, frontmatter: parsed, body };
815
+ }
816
+ };
817
+
818
+ // kernel/scan/parsers/plain.ts
819
+ var plainParser = {
820
+ id: "plain",
821
+ parse(raw, _path) {
822
+ return { frontmatter: {}, frontmatterRaw: "", body: raw };
823
+ }
824
+ };
825
+
826
+ // kernel/scan/parsers/index.ts
827
+ var REGISTRY = /* @__PURE__ */ new Map([
828
+ [frontmatterYamlParser.id, frontmatterYamlParser],
829
+ [plainParser.id, plainParser]
830
+ ]);
831
+ var FROZEN_IDS = new Set(REGISTRY.keys());
832
+ function getParser(id) {
833
+ return REGISTRY.get(id);
834
+ }
835
+
836
+ // kernel/scan/walk-content.ts
837
+ var UnknownParserError = class extends Error {
838
+ constructor(parserId) {
839
+ super(`Unknown parser id '${parserId}'. Built-in parsers: 'frontmatter-yaml', 'plain'.`);
840
+ this.name = "UnknownParserError";
841
+ }
842
+ };
843
+ async function* walkContent(roots, options) {
844
+ const parser = getParser(options.parser);
845
+ if (!parser) throw new UnknownParserError(options.parser);
846
+ const filter = options.ignoreFilter ?? buildIgnoreFilter();
847
+ const extensions = options.extensions;
848
+ for (const root of roots) {
849
+ for await (const file of walkRoot(root, root, filter, extensions)) {
850
+ const relPath = relative3(root, file).split(sep2).join("/");
851
+ let raw;
852
+ try {
853
+ raw = await readFile(file, "utf8");
854
+ } catch {
855
+ continue;
856
+ }
857
+ const parsed = parser.parse(raw, relPath);
858
+ yield {
859
+ path: relPath,
860
+ body: parsed.body,
861
+ frontmatterRaw: parsed.frontmatterRaw,
862
+ frontmatter: parsed.frontmatter
863
+ };
864
+ }
865
+ }
866
+ }
867
+ async function* walkRoot(root, current, filter, extensions) {
868
+ let entries;
869
+ try {
870
+ entries = await readdir(current, { withFileTypes: true, encoding: "utf8" });
871
+ } catch {
872
+ return;
873
+ }
874
+ for (const entry of entries) {
875
+ const name = entry.name;
876
+ const full = join3(current, name);
877
+ const rel = relative3(root, full).split(sep2).join("/");
878
+ if (filter.ignores(rel)) continue;
879
+ if (entry.isSymbolicLink()) continue;
880
+ if (entry.isDirectory()) {
881
+ yield* walkRoot(root, full, filter, extensions);
882
+ } else if (entry.isFile() && hasMatchingExtension(name, extensions)) {
883
+ try {
884
+ const s = await stat(full);
885
+ if (s.isFile()) yield full;
886
+ } catch {
887
+ }
888
+ }
889
+ }
890
+ }
891
+ function hasMatchingExtension(name, extensions) {
892
+ for (const ext of extensions) {
893
+ if (name.endsWith(ext)) return true;
894
+ }
895
+ return false;
896
+ }
897
+
898
+ // kernel/extensions/provider.ts
899
+ var DEFAULT_READ_CONFIG = Object.freeze({
900
+ extensions: Object.freeze([".md"]),
901
+ parser: "frontmatter-yaml"
902
+ });
903
+ function resolveProviderWalk(provider) {
904
+ if (provider.walk) {
905
+ const walk2 = provider.walk.bind(provider);
906
+ return walk2;
907
+ }
908
+ const read = provider.read ?? DEFAULT_READ_CONFIG;
909
+ return (roots, options) => {
910
+ const walkOptions = {
911
+ extensions: read.extensions,
912
+ parser: read.parser
913
+ };
914
+ if (options?.ignoreFilter) walkOptions.ignoreFilter = options.ignoreFilter;
915
+ return walkContent(roots, walkOptions);
916
+ };
917
+ }
918
+
446
919
  // kernel/orchestrator.ts
447
920
  var SCANNED_BY = {
448
921
  name: "skill-map",
@@ -505,14 +978,26 @@ async function runScanInternal(_kernel, options) {
505
978
  emitter.emit(evt);
506
979
  await hookDispatcher.dispatch("extractor.completed", evt);
507
980
  }
508
- const issues = await runRules(exts.rules, walked.nodes, walked.internalLinks, emitter, hookDispatcher);
981
+ const ruleResult = await runRules(
982
+ exts.rules,
983
+ walked.nodes,
984
+ walked.internalLinks,
985
+ walked.orphanSidecars,
986
+ walked.sidecarRoots,
987
+ options.annotationContributions ?? [],
988
+ options.viewContributions ?? [],
989
+ emitter,
990
+ hookDispatcher
991
+ );
992
+ const issues = ruleResult.issues;
993
+ for (const c of ruleResult.contributions) walked.contributions.push(c);
509
994
  for (const issue of walked.frontmatterIssues) issues.push(issue);
510
995
  const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues) : [];
511
996
  const stats = {
512
997
  // `filesSkipped` is "files walked but not classified by any Provider".
513
998
  // Today every walked file IS classified by its Provider (the `claude`
514
999
  // Provider's `classify()` always returns a kind, falling back to
515
- // `'note'`), so this is always 0. Wired now so the field shape is
1000
+ // `'markdown'`), so this is always 0. Wired now so the field shape is
516
1001
  // spec-conformant; meaningful once multiple Providers compete.
517
1002
  filesWalked: walked.filesWalked,
518
1003
  filesSkipped: 0,
@@ -539,7 +1024,8 @@ async function runScanInternal(_kernel, options) {
539
1024
  },
540
1025
  renameOps,
541
1026
  extractorRuns: walked.extractorRuns,
542
- enrichments: walked.enrichments
1027
+ enrichments: walked.enrichments,
1028
+ contributions: walked.contributions
543
1029
  };
544
1030
  }
545
1031
  function validateRoots(roots) {
@@ -547,7 +1033,7 @@ function validateRoots(roots) {
547
1033
  throw new Error(ORCHESTRATOR_TEXTS.runScanRootEmptyArray);
548
1034
  }
549
1035
  for (const root of roots) {
550
- if (!existsSync2(root) || !statSync(root).isDirectory()) {
1036
+ if (!existsSync6(root) || !statSync2(root).isDirectory()) {
551
1037
  throw new Error(tx(ORCHESTRATOR_TEXTS.runScanRootMissing, { root }));
552
1038
  }
553
1039
  }
@@ -584,9 +1070,10 @@ async function runExtractorsForNode(opts) {
584
1070
  const internalLinks = [];
585
1071
  const externalLinks = [];
586
1072
  const enrichmentBuffer = /* @__PURE__ */ new Map();
1073
+ const contributions = [];
1074
+ const validators = loadSchemaValidators();
587
1075
  for (const extractor of opts.extractors) {
588
1076
  const qualifiedId = qualifiedExtensionId(extractor.pluginId, extractor.id);
589
- const isProb = extractor.mode === "probabilistic";
590
1077
  const emitLink = (link) => {
591
1078
  const validated = validateLink(extractor, link, opts.emitter);
592
1079
  if (!validated) return;
@@ -606,9 +1093,54 @@ async function runExtractorsForNode(opts) {
606
1093
  bodyHashAtEnrichment: opts.bodyHash,
607
1094
  value: { ...partial },
608
1095
  enrichedAt: Date.now(),
609
- isProbabilistic: isProb
1096
+ // Extractors are deterministic-only; `is_probabilistic` is
1097
+ // reserved on the row for future Action-issued enrichments.
1098
+ isProbabilistic: false
1099
+ });
1100
+ }
1101
+ };
1102
+ const declaredContributions = readDeclaredContributions(extractor);
1103
+ const emitContribution = (contributionId, payload) => {
1104
+ const declared = declaredContributions.get(contributionId);
1105
+ if (!declared) {
1106
+ emitExtensionError(opts.emitter, qualifiedId, opts.node.path, {
1107
+ phase: "emitContribution",
1108
+ contributionId,
1109
+ reason: "unknown-contribution-id",
1110
+ message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {
1111
+ extractorId: qualifiedId,
1112
+ contributionId,
1113
+ nodePath: opts.node.path
1114
+ })
1115
+ });
1116
+ return;
1117
+ }
1118
+ const result = validators.validateContributionPayload(declared.contract, payload);
1119
+ if (!result.ok) {
1120
+ emitExtensionError(opts.emitter, qualifiedId, opts.node.path, {
1121
+ phase: "emitContribution",
1122
+ contributionId,
1123
+ contract: declared.contract,
1124
+ reason: result.errors,
1125
+ message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
1126
+ extractorId: qualifiedId,
1127
+ contributionId,
1128
+ nodePath: opts.node.path,
1129
+ contract: declared.contract,
1130
+ errors: result.errors
1131
+ })
610
1132
  });
1133
+ return;
611
1134
  }
1135
+ contributions.push({
1136
+ pluginId: extractor.pluginId,
1137
+ extensionId: extractor.id,
1138
+ nodePath: opts.node.path,
1139
+ contributionId,
1140
+ contract: declared.contract,
1141
+ payload,
1142
+ emittedAt: Date.now()
1143
+ });
612
1144
  };
613
1145
  const store = opts.pluginStores?.get(extractor.pluginId);
614
1146
  const ctx = buildExtractorContext(
@@ -618,6 +1150,7 @@ async function runExtractorsForNode(opts) {
618
1150
  opts.frontmatter,
619
1151
  emitLink,
620
1152
  enrichNode,
1153
+ emitContribution,
621
1154
  store
622
1155
  );
623
1156
  await extractor.extract(ctx);
@@ -625,9 +1158,32 @@ async function runExtractorsForNode(opts) {
625
1158
  return {
626
1159
  internalLinks,
627
1160
  externalLinks,
628
- enrichments: Array.from(enrichmentBuffer.values())
1161
+ enrichments: Array.from(enrichmentBuffer.values()),
1162
+ contributions
629
1163
  };
630
1164
  }
1165
+ function readDeclaredContributions(extension) {
1166
+ const out = /* @__PURE__ */ new Map();
1167
+ const raw = extension.viewContributions;
1168
+ if (typeof raw !== "object" || raw === null) return out;
1169
+ for (const [id, value] of Object.entries(raw)) {
1170
+ if (typeof value !== "object" || value === null) continue;
1171
+ const contract = value.contract;
1172
+ if (typeof contract !== "string") continue;
1173
+ out.set(id, { contract });
1174
+ }
1175
+ return out;
1176
+ }
1177
+ function emitExtensionError(emitter, qualifiedId, nodePath, data) {
1178
+ emitter.emit(
1179
+ makeEvent("extension.error", {
1180
+ kind: "contribution-rejected",
1181
+ extensionId: qualifiedId,
1182
+ nodePath,
1183
+ ...data
1184
+ })
1185
+ );
1186
+ }
631
1187
  function computeCacheDecision(opts) {
632
1188
  const applicableExtractors = opts.extractors.filter(
633
1189
  (ex) => ex.applicableKinds === void 0 || ex.applicableKinds.includes(opts.kind)
@@ -750,7 +1306,9 @@ async function walkAndExtract(opts) {
750
1306
  const cachedPaths = /* @__PURE__ */ new Set();
751
1307
  const frontmatterIssues = [];
752
1308
  const enrichmentBuffer = /* @__PURE__ */ new Map();
1309
+ const contributionsBuffer = [];
753
1310
  const extractorRuns = [];
1311
+ const sidecarRoots = /* @__PURE__ */ new Map();
754
1312
  let filesWalked = 0;
755
1313
  let index = 0;
756
1314
  const walkOptions = ignoreFilter ? { ignoreFilter } : {};
@@ -761,14 +1319,20 @@ async function walkAndExtract(opts) {
761
1319
  if (list) list.push(qualified);
762
1320
  else shortIdToQualified.set(ex.id, [qualified]);
763
1321
  }
1322
+ const claimedPaths = /* @__PURE__ */ new Set();
764
1323
  for (const provider of providers) {
765
- for await (const raw of provider.walk(roots, walkOptions)) {
1324
+ for await (const raw of resolveProviderWalk(provider)(roots, walkOptions)) {
766
1325
  filesWalked += 1;
1326
+ if (claimedPaths.has(raw.path)) continue;
767
1327
  const bodyHash = sha256(raw.body);
768
1328
  const frontmatterHash = sha256(canonicalFrontmatter(raw.frontmatter, raw.frontmatterRaw));
769
1329
  const priorNode = priorNodesByPath.get(raw.path);
770
1330
  const nodeHashCacheEligible = enableCache && prior !== null && priorNode !== void 0 && priorNode.bodyHash === bodyHash && priorNode.frontmatterHash === frontmatterHash;
771
1331
  const kind = provider.classify(raw.path, raw.frontmatter);
1332
+ if (kind === null) {
1333
+ continue;
1334
+ }
1335
+ claimedPaths.add(raw.path);
772
1336
  index += 1;
773
1337
  const cacheDecision = computeCacheDecision({
774
1338
  extractors,
@@ -796,10 +1360,19 @@ async function walkAndExtract(opts) {
796
1360
  priorLinksByOriginating,
797
1361
  priorFrontmatterIssuesByNode
798
1362
  });
1363
+ const reusedSidecarIssues = resolveAndApplySidecar(
1364
+ reused.node,
1365
+ raw.path,
1366
+ roots,
1367
+ bodyHash,
1368
+ frontmatterHash,
1369
+ sidecarRoots
1370
+ );
799
1371
  nodes.push(reused.node);
800
1372
  cachedPaths.add(reused.node.path);
801
1373
  for (const link of reused.internalLinks) internalLinks.push(link);
802
1374
  for (const issue of reused.frontmatterIssues) frontmatterIssues.push(issue);
1375
+ for (const issue of reusedSidecarIssues) frontmatterIssues.push(issue);
803
1376
  for (const run of reused.extractorRuns) extractorRuns.push(run);
804
1377
  emitter.emit(makeEvent("scan.progress", { index, path: raw.path, kind, cached: true }));
805
1378
  continue;
@@ -835,6 +1408,15 @@ async function walkAndExtract(opts) {
835
1408
  nodes.push(node);
836
1409
  for (const issue of fresh.frontmatterIssues) frontmatterIssues.push(issue);
837
1410
  }
1411
+ const sidecarIssues = resolveAndApplySidecar(
1412
+ node,
1413
+ raw.path,
1414
+ roots,
1415
+ bodyHash,
1416
+ frontmatterHash,
1417
+ sidecarRoots
1418
+ );
1419
+ for (const issue of sidecarIssues) frontmatterIssues.push(issue);
838
1420
  emitter.emit(makeEvent("scan.progress", {
839
1421
  index,
840
1422
  path: raw.path,
@@ -857,6 +1439,7 @@ async function walkAndExtract(opts) {
857
1439
  for (const enr of extractResult.enrichments) {
858
1440
  enrichmentBuffer.set(`${enr.nodePath}\0${enr.extractorId}`, enr);
859
1441
  }
1442
+ for (const c of extractResult.contributions) contributionsBuffer.push(c);
860
1443
  const ranAt = Date.now();
861
1444
  for (const ex of applicableExtractors) {
862
1445
  const qualified = qualifiedExtensionId(ex.pluginId, ex.id);
@@ -869,6 +1452,7 @@ async function walkAndExtract(opts) {
869
1452
  }
870
1453
  }
871
1454
  }
1455
+ const orphanSidecars = discoverOrphanSidecars(roots);
872
1456
  return {
873
1457
  nodes,
874
1458
  internalLinks,
@@ -877,7 +1461,10 @@ async function walkAndExtract(opts) {
877
1461
  frontmatterIssues,
878
1462
  filesWalked,
879
1463
  enrichments: [...enrichmentBuffer.values()],
880
- extractorRuns
1464
+ extractorRuns,
1465
+ contributions: contributionsBuffer,
1466
+ orphanSidecars,
1467
+ sidecarRoots
881
1468
  };
882
1469
  }
883
1470
  function reuseCachedLink(link, shortIdToQualified, cachedQualifiedIds, applicableQualifiedIds) {
@@ -906,20 +1493,77 @@ function reuseCachedLink(link, shortIdToQualified, cachedQualifiedIds, applicabl
906
1493
  if (obsoleteSources.length === 0) return link;
907
1494
  return { ...link, sources: cachedSources };
908
1495
  }
909
- async function runRules(rules, nodes, internalLinks, emitter, hookDispatcher) {
1496
+ async function runRules(rules, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, emitter, hookDispatcher) {
910
1497
  const issues = [];
1498
+ const contributions = [];
1499
+ const validators = loadSchemaValidators();
1500
+ const ruleOrphans = orphanSidecars.map((o) => ({
1501
+ relativePath: o.relativePath,
1502
+ expectedMdPath: o.expectedMdPath
1503
+ }));
911
1504
  for (const rule of rules) {
912
- const emitted = await rule.evaluate({ nodes, links: internalLinks });
1505
+ const qualifiedId = qualifiedExtensionId(rule.pluginId, rule.id);
1506
+ const declaredContributions = readDeclaredContributions(rule);
1507
+ const emitContribution = (nodePath, contributionId, payload) => {
1508
+ const declared = declaredContributions.get(contributionId);
1509
+ if (!declared) {
1510
+ emitExtensionError(emitter, qualifiedId, nodePath, {
1511
+ phase: "emitContribution",
1512
+ contributionId,
1513
+ reason: "unknown-contribution-id",
1514
+ message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {
1515
+ extractorId: qualifiedId,
1516
+ contributionId,
1517
+ nodePath
1518
+ })
1519
+ });
1520
+ return;
1521
+ }
1522
+ const result = validators.validateContributionPayload(declared.contract, payload);
1523
+ if (!result.ok) {
1524
+ emitExtensionError(emitter, qualifiedId, nodePath, {
1525
+ phase: "emitContribution",
1526
+ contributionId,
1527
+ contract: declared.contract,
1528
+ reason: result.errors,
1529
+ message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
1530
+ extractorId: qualifiedId,
1531
+ contributionId,
1532
+ nodePath,
1533
+ contract: declared.contract,
1534
+ errors: result.errors
1535
+ })
1536
+ });
1537
+ return;
1538
+ }
1539
+ contributions.push({
1540
+ pluginId: rule.pluginId,
1541
+ extensionId: rule.id,
1542
+ nodePath,
1543
+ contributionId,
1544
+ contract: declared.contract,
1545
+ payload,
1546
+ emittedAt: Date.now()
1547
+ });
1548
+ };
1549
+ const emitted = await rule.evaluate({
1550
+ nodes,
1551
+ links: internalLinks,
1552
+ orphanSidecars: ruleOrphans,
1553
+ sidecarRoots,
1554
+ annotationContributions,
1555
+ viewContributions,
1556
+ emitContribution
1557
+ });
913
1558
  for (const issue of emitted) {
914
1559
  const validated = validateIssue(rule, issue, emitter);
915
1560
  if (validated) issues.push(validated);
916
1561
  }
917
- const ruleId = qualifiedExtensionId(rule.pluginId, rule.id);
918
- const evt = makeEvent("rule.completed", { ruleId });
1562
+ const evt = makeEvent("rule.completed", { ruleId: qualifiedId });
919
1563
  emitter.emit(evt);
920
1564
  await hookDispatcher.dispatch("rule.completed", evt);
921
1565
  }
922
- return issues;
1566
+ return { issues, contributions };
923
1567
  }
924
1568
  function originatingNodeOf(link, priorNodePaths) {
925
1569
  if (link.kind === "supersedes" && !priorNodePaths.has(link.source)) {
@@ -1090,7 +1734,7 @@ function makeHookDispatcher(hooks, emitter) {
1090
1734
  await hook.on(ctx);
1091
1735
  } catch (err) {
1092
1736
  const qualifiedId = qualifiedExtensionId(hook.pluginId, hook.id);
1093
- const message = err instanceof Error ? err.message : String(err);
1737
+ const message = formatErrorMessage(err);
1094
1738
  emitter.emit(
1095
1739
  makeEvent("extension.error", {
1096
1740
  kind: "hook-error",
@@ -1135,7 +1779,6 @@ function buildHookContext(_hook, trigger, event) {
1135
1779
  function buildNode(args) {
1136
1780
  const bytesFrontmatter = Buffer.byteLength(args.frontmatterRaw, "utf8");
1137
1781
  const bytesBody = Buffer.byteLength(args.body, "utf8");
1138
- const metadata = pickMetadata(args.frontmatter);
1139
1782
  const node = {
1140
1783
  path: args.path,
1141
1784
  kind: args.kind,
@@ -1150,12 +1793,7 @@ function buildNode(args) {
1150
1793
  linksOutCount: 0,
1151
1794
  linksInCount: 0,
1152
1795
  externalRefsCount: 0,
1153
- frontmatter: args.frontmatter,
1154
- title: pickString(args.frontmatter["name"]),
1155
- description: pickString(args.frontmatter["description"]),
1156
- stability: pickStability(metadata?.["stability"]),
1157
- version: pickString(metadata?.["version"]),
1158
- author: pickString(args.frontmatter["author"])
1796
+ frontmatter: args.frontmatter
1159
1797
  };
1160
1798
  if (args.encoder) {
1161
1799
  node.tokens = countTokens(args.encoder, args.frontmatterRaw, args.body);
@@ -1176,25 +1814,73 @@ function canonicalFrontmatter(parsed, raw) {
1176
1814
  if (!hasParsedKeys && hasRawText) {
1177
1815
  return raw;
1178
1816
  }
1179
- return yaml.dump(parsed, {
1817
+ return yaml4.dump(parsed, {
1180
1818
  sortKeys: true,
1181
1819
  lineWidth: -1,
1182
1820
  noRefs: true,
1183
1821
  noCompatMode: true
1184
1822
  });
1185
1823
  }
1186
- function pickMetadata(fm) {
1187
- const m = fm["metadata"];
1188
- return m && typeof m === "object" && !Array.isArray(m) ? m : null;
1189
- }
1190
- function pickString(value) {
1191
- return typeof value === "string" && value.length > 0 ? value : null;
1824
+ function resolveAndApplySidecar(node, relativePath, roots, liveBodyHash, liveFrontmatterHash, sidecarRoots) {
1825
+ const issues = [];
1826
+ const mdAbs = resolveAbsoluteMdPath(relativePath, roots);
1827
+ if (mdAbs === null) {
1828
+ node.sidecar = { present: false };
1829
+ return issues;
1830
+ }
1831
+ const result = readSidecarFor(mdAbs);
1832
+ if (!result.present) {
1833
+ node.sidecar = { present: false };
1834
+ return issues;
1835
+ }
1836
+ if (result.parsed === null) {
1837
+ node.sidecar = { present: true, status: null, annotations: null, root: null };
1838
+ for (const parseIssue of result.issues) {
1839
+ issues.push({
1840
+ ruleId: "invalid-sidecar",
1841
+ severity: "warn",
1842
+ nodeIds: [node.path],
1843
+ message: parseIssue.message,
1844
+ data: { sidecarPath: relativePathFromRoots(mdAbs, roots) }
1845
+ });
1846
+ }
1847
+ return issues;
1848
+ }
1849
+ const status = computeDriftStatus({
1850
+ storedBodyHash: result.parsed.identityBodyHash,
1851
+ storedFrontmatterHash: result.parsed.identityFrontmatterHash,
1852
+ liveBodyHash,
1853
+ liveFrontmatterHash
1854
+ });
1855
+ node.sidecar = {
1856
+ present: true,
1857
+ status,
1858
+ annotations: result.parsed.annotations,
1859
+ root: result.parsed.raw
1860
+ };
1861
+ sidecarRoots.set(node.path, result.parsed.raw);
1862
+ return issues;
1192
1863
  }
1193
- function pickStability(value) {
1194
- if (value === "experimental" || value === "stable" || value === "deprecated") return value;
1864
+ function resolveAbsoluteMdPath(relativePath, roots) {
1865
+ if (isAbsolute2(relativePath)) {
1866
+ return existsSync6(relativePath) ? relativePath : null;
1867
+ }
1868
+ for (const root of roots) {
1869
+ const candidate = resolvePath(root, relativePath);
1870
+ if (existsSync6(candidate)) return candidate;
1871
+ }
1195
1872
  return null;
1196
1873
  }
1197
- function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, store) {
1874
+ function relativePathFromRoots(absolutePath, roots) {
1875
+ for (const root of roots) {
1876
+ const abs = resolvePath(root);
1877
+ if (absolutePath.startsWith(`${abs}/`) || absolutePath.startsWith(`${abs}\\`)) {
1878
+ return absolutePath.slice(abs.length + 1).split(/[\\/]/).join("/");
1879
+ }
1880
+ }
1881
+ return absolutePath;
1882
+ }
1883
+ function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, emitContribution, store) {
1198
1884
  const scope = extractor.scope;
1199
1885
  return {
1200
1886
  node,
@@ -1202,6 +1888,7 @@ function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enr
1202
1888
  frontmatter: scope === "body" ? {} : frontmatter,
1203
1889
  emitLink,
1204
1890
  enrichNode,
1891
+ emitContribution,
1205
1892
  ...store !== void 0 ? { store } : {}
1206
1893
  };
1207
1894
  }
@@ -1340,16 +2027,16 @@ function assignSafe(target, source) {
1340
2027
  }
1341
2028
 
1342
2029
  // kernel/scan/watcher.ts
1343
- import { resolve as resolve3, relative as relative2, sep } from "path";
2030
+ import { resolve as resolve6, relative as relative4, sep as sep3 } from "path";
1344
2031
  import chokidar from "chokidar";
1345
2032
  function createChokidarWatcher(opts) {
1346
- const absRoots = opts.roots.map((r) => resolve3(opts.cwd, r));
2033
+ const absRoots = opts.roots.map((r) => resolve6(opts.cwd, r));
1347
2034
  const ignoreFilterOpt = opts.ignoreFilter;
1348
2035
  const getFilter = ignoreFilterOpt === void 0 ? void 0 : typeof ignoreFilterOpt === "function" ? ignoreFilterOpt : () => ignoreFilterOpt;
1349
2036
  const ignored = getFilter ? (path) => {
1350
2037
  const filter = getFilter();
1351
2038
  if (!filter) return false;
1352
- const rel = relativePathFromRoots(path, absRoots);
2039
+ const rel = relativePathFromRoots2(path, absRoots);
1353
2040
  if (rel === null) return false;
1354
2041
  return filter.ignores(rel);
1355
2042
  } : void 0;
@@ -1433,12 +2120,12 @@ function createChokidarWatcher(opts) {
1433
2120
  };
1434
2121
  return { ready, close };
1435
2122
  }
1436
- function relativePathFromRoots(absolute, absRoots) {
2123
+ function relativePathFromRoots2(absolute, absRoots) {
1437
2124
  for (const root of absRoots) {
1438
- const rel = relative2(root, absolute);
2125
+ const rel = relative4(root, absolute);
1439
2126
  if (rel === "" || rel === ".") return "";
1440
- if (!rel.startsWith("..") && !rel.startsWith(`..${sep}`)) {
1441
- return rel.split(sep).join("/");
2127
+ if (!rel.startsWith("..") && !rel.startsWith(`..${sep3}`)) {
2128
+ return rel.split(sep3).join("/");
1442
2129
  }
1443
2130
  }
1444
2131
  return null;
@@ -1689,7 +2376,23 @@ function parseLogLevel(value) {
1689
2376
 
1690
2377
  // kernel/index.ts
1691
2378
  function createKernel() {
1692
- return { registry: new Registry() };
2379
+ let annotationKeys = Object.freeze([]);
2380
+ let viewContributions = Object.freeze([]);
2381
+ return {
2382
+ registry: new Registry(),
2383
+ getRegisteredAnnotationKeys() {
2384
+ return annotationKeys;
2385
+ },
2386
+ setRegisteredAnnotationKeys(entries) {
2387
+ annotationKeys = Object.freeze([...entries]);
2388
+ },
2389
+ getRegisteredViewContributions() {
2390
+ return viewContributions;
2391
+ },
2392
+ setRegisteredViewContributions(entries) {
2393
+ viewContributions = Object.freeze([...entries]);
2394
+ }
2395
+ };
1693
2396
  }
1694
2397
  export {
1695
2398
  DuplicateExtensionError,