@skill-map/cli 0.21.0 → 0.22.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.
package/dist/index.js CHANGED
@@ -92,18 +92,15 @@ var Registry = class {
92
92
  }
93
93
  };
94
94
 
95
- // kernel/orchestrator.ts
96
- import { createHash } from "crypto";
97
- import { existsSync as existsSync8, statSync as statSync2 } from "fs";
98
- import { isAbsolute as isAbsolute3, resolve as resolvePath } from "path";
99
- import { Tiktoken } from "js-tiktoken/lite";
95
+ // kernel/orchestrator/index.ts
96
+ import { existsSync as existsSync9, statSync as statSync2 } from "fs";
97
+ import { Tiktoken as Tiktoken2 } from "js-tiktoken/lite";
100
98
  import cl100k_base from "js-tiktoken/ranks/cl100k_base";
101
- import yaml4 from "js-yaml";
102
99
 
103
100
  // package.json
104
101
  var package_default = {
105
102
  name: "@skill-map/cli",
106
- version: "0.21.0",
103
+ version: "0.22.0",
107
104
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
108
105
  license: "MIT",
109
106
  type: "module",
@@ -156,20 +153,21 @@ var package_default = {
156
153
  "lint:fix": "eslint . --fix",
157
154
  reference: "node scripts/build-reference.js",
158
155
  "reference:check": "node scripts/build-reference.js --check",
159
- validate: "npm run typecheck && npm run lint && npm run build && npm run test:ci && npm run reference:check",
156
+ validate: "npm run validate:compile && npm run validate:test",
157
+ "validate:compile": "npm run typecheck && npm run lint && npm run build && npm run reference:check",
158
+ "validate:test": "npm run test:ci",
160
159
  pretest: "tsup",
161
- "pretest:ci": "tsup",
162
160
  "pretest:coverage": "tsup",
163
161
  "pretest:coverage:html": "tsup",
164
162
  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'",
163
+ "test:ci": "node --import tsx --test 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts' 'server/**/*.test.ts'",
166
164
  "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
165
  "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'",
168
166
  clean: "rm -rf dist coverage"
169
167
  },
170
168
  dependencies: {
171
169
  "@hono/node-server": "2.0.1",
172
- "@skill-map/spec": "0.21.0",
170
+ "@skill-map/spec": "0.22.0",
173
171
  ajv: "8.18.0",
174
172
  "ajv-formats": "3.0.1",
175
173
  chokidar: "5.0.0",
@@ -206,12 +204,32 @@ var package_default = {
206
204
  }
207
205
  };
208
206
 
209
- // kernel/sidecar/parse.ts
210
- import { existsSync, readFileSync } from "fs";
211
- import { dirname, resolve } from "path";
207
+ // kernel/adapters/in-memory-progress.ts
208
+ var InMemoryProgressEmitter = class {
209
+ #listeners = /* @__PURE__ */ new Set();
210
+ emit(event) {
211
+ for (const listener of this.#listeners) listener(event);
212
+ }
213
+ subscribe(listener) {
214
+ this.#listeners.add(listener);
215
+ return () => {
216
+ this.#listeners.delete(listener);
217
+ };
218
+ }
219
+ };
220
+
221
+ // kernel/adapters/plugin-loader/index.ts
212
222
  import { createRequire } from "module";
223
+ import { existsSync, readFileSync as readFileSync2, readdirSync } from "fs";
224
+ import { join, resolve as resolve3 } from "path";
225
+ import { pathToFileURL } from "url";
226
+ import semver from "semver";
227
+
228
+ // kernel/adapters/plugin-loader/id-utils.ts
229
+ import { isAbsolute, relative, resolve } from "path";
230
+
231
+ // kernel/adapters/plugin-loader/validation.ts
213
232
  import { Ajv2020 } from "ajv/dist/2020.js";
214
- import yaml from "js-yaml";
215
233
 
216
234
  // kernel/util/ajv-interop.ts
217
235
  import addFormatsModule from "ajv-formats";
@@ -220,171 +238,128 @@ function applyAjvFormats(ajv) {
220
238
  addFormats(ajv);
221
239
  }
222
240
 
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;
241
+ // kernel/extensions/hook.ts
242
+ var HOOK_TRIGGERS = Object.freeze([
243
+ "boot",
244
+ "scan.started",
245
+ "scan.completed",
246
+ "extractor.completed",
247
+ "analyzer.completed",
248
+ "action.completed",
249
+ "job.spawning",
250
+ "job.completed",
251
+ "job.failed",
252
+ "shutdown"
253
+ ]);
254
+
255
+ // kernel/adapters/plugin-loader/validation.ts
256
+ var KNOWN_KINDS = /* @__PURE__ */ new Set([
257
+ "provider",
258
+ "extractor",
259
+ "analyzer",
260
+ "action",
261
+ "formatter",
262
+ "hook"
263
+ ]);
264
+ var KNOWN_KINDS_LIST = [...KNOWN_KINDS].join(" / ");
265
+ var HOOKABLE_TRIGGERS_LIST = HOOK_TRIGGERS.join(", ");
266
+
267
+ // kernel/adapters/plugin-loader/storage-schemas.ts
268
+ import { readFileSync } from "fs";
269
+ import { resolve as resolve2 } from "path";
270
+ import { Ajv2020 as Ajv20202 } from "ajv/dist/2020.js";
271
+
272
+ // kernel/i18n/plugin-store.texts.ts
273
+ var PLUGIN_STORE_TEXTS = {
274
+ kvValidationFailed: "plugin '{{pluginId}}' ctx.store.set('{{key}}', value): value violates declared schema ({{schemaPath}}) \u2014 {{errors}}",
275
+ dedicatedValidationFailed: "plugin '{{pluginId}}' ctx.store.write('{{table}}', row): row violates declared schema ({{schemaPath}}) \u2014 {{errors}}"
276
+ };
277
+
278
+ // kernel/adapters/plugin-store.ts
279
+ var KV_SCHEMA_KEY = "__kv__";
280
+ function makeKvStoreWrapper(opts) {
281
+ const { pluginId, schema, persist } = opts;
269
282
  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: []
283
+ async set(key, value) {
284
+ if (schema) {
285
+ if (!schema.validate(value)) {
286
+ throw new Error(
287
+ tx(PLUGIN_STORE_TEXTS.kvValidationFailed, {
288
+ pluginId,
289
+ schemaPath: schema.schemaPath,
290
+ key,
291
+ errors: formatAjvErrors(schema.validate.errors ?? null)
292
+ })
293
+ );
294
+ }
295
+ }
296
+ await persist(key, value);
297
+ }
280
298
  };
281
299
  }
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;
300
+ function makeDedicatedStoreWrapper(opts) {
301
+ const { pluginId, schemas, persist } = opts;
302
+ return {
303
+ async write(table, row) {
304
+ const schema = schemas?.[table];
305
+ if (schema) {
306
+ if (!schema.validate(row)) {
307
+ throw new Error(
308
+ tx(PLUGIN_STORE_TEXTS.dedicatedValidationFailed, {
309
+ pluginId,
310
+ table,
311
+ schemaPath: schema.schemaPath,
312
+ errors: formatAjvErrors(schema.validate.errors ?? null)
313
+ })
314
+ );
315
+ }
316
+ }
317
+ await persist(table, row);
318
+ }
319
+ };
338
320
  }
339
- function walk(root, current, shouldSkip, out) {
340
- let entries;
341
- try {
342
- entries = readdirSync(current, { withFileTypes: true, encoding: "utf8" });
343
- } catch {
344
- return;
321
+ function makePluginStore(opts) {
322
+ const manifest = opts.plugin.manifest;
323
+ if (!manifest?.storage) return void 0;
324
+ const storageSchemas = opts.plugin.storageSchemas;
325
+ if (manifest.storage.mode === "kv") {
326
+ if (!opts.persistKv) return void 0;
327
+ const schema = storageSchemas?.[KV_SCHEMA_KEY];
328
+ return makeKvStoreWrapper({
329
+ pluginId: manifest.id,
330
+ schema,
331
+ persist: opts.persistKv
332
+ });
345
333
  }
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 });
334
+ if (manifest.storage.mode === "dedicated") {
335
+ if (!opts.persistDedicated) return void 0;
336
+ return makeDedicatedStoreWrapper({
337
+ pluginId: manifest.id,
338
+ schemas: storageSchemas,
339
+ persist: opts.persistDedicated
340
+ });
360
341
  }
342
+ return void 0;
361
343
  }
362
- function safeIsFile(path) {
363
- try {
364
- return statSync(path).isFile();
365
- } catch {
366
- return false;
367
- }
344
+ function formatAjvErrors(errors) {
345
+ if (!errors || errors.length === 0) return "(no AJV details)";
346
+ return errors.map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
368
347
  }
369
348
 
370
- // kernel/sidecar/store.ts
371
- import { existsSync as existsSync5, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
372
- import { dirname as dirname4, resolve as resolve5 } from "path";
373
- import { createRequire as createRequire3 } from "module";
374
- import { Ajv2020 as Ajv20203 } from "ajv/dist/2020.js";
375
- import yaml2 from "js-yaml";
376
-
377
- // core/config/helper.ts
378
- import { isAbsolute, resolve as resolve4 } from "path";
379
-
380
- // kernel/config/loader.ts
381
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
349
+ // kernel/adapters/plugin-loader/index.ts
350
+ function installedSpecVersion() {
351
+ const require2 = createRequire(import.meta.url);
352
+ const indexPath = require2.resolve("@skill-map/spec/index.json");
353
+ const pkgPath = resolve3(indexPath, "..", "package.json");
354
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
355
+ return pkg.version;
356
+ }
382
357
 
383
358
  // kernel/adapters/schema-validators.ts
384
- import { readFileSync as readFileSync2 } from "fs";
385
- import { dirname as dirname2, resolve as resolve2 } from "path";
359
+ import { readFileSync as readFileSync3 } from "fs";
360
+ import { dirname, resolve as resolve4 } from "path";
386
361
  import { createRequire as createRequire2 } from "module";
387
- import { Ajv2020 as Ajv20202 } from "ajv/dist/2020.js";
362
+ import { Ajv2020 as Ajv20203 } from "ajv/dist/2020.js";
388
363
  var SCHEMA_FILES = {
389
364
  node: "schemas/node.schema.json",
390
365
  link: "schemas/link.schema.json",
@@ -419,23 +394,23 @@ function loadSchemaValidators() {
419
394
  return cachedValidators;
420
395
  }
421
396
  function buildSchemaValidators() {
422
- const specRoot = resolveSpecRoot2();
423
- const ajv = new Ajv20202({
397
+ const specRoot = resolveSpecRoot();
398
+ const ajv = new Ajv20203({
424
399
  strict: false,
425
400
  allErrors: true,
426
401
  allowUnionTypes: true
427
402
  });
428
403
  applyAjvFormats(ajv);
429
404
  for (const rel of SUPPORTING_SCHEMAS) {
430
- const file = resolve2(specRoot, rel);
405
+ const file = resolve4(specRoot, rel);
431
406
  if (!existsSyncSafe(file)) continue;
432
- const schema = JSON.parse(readFileSync2(file, "utf8"));
407
+ const schema = JSON.parse(readFileSync3(file, "utf8"));
433
408
  ajv.addSchema(schema);
434
409
  }
435
410
  const validators = /* @__PURE__ */ new Map();
436
411
  for (const [name, rel] of Object.entries(SCHEMA_FILES)) {
437
- const file = resolve2(specRoot, rel);
438
- const schema = JSON.parse(readFileSync2(file, "utf8"));
412
+ const file = resolve4(specRoot, rel);
413
+ const schema = JSON.parse(readFileSync3(file, "utf8"));
439
414
  const byId = typeof schema.$id === "string" ? ajv.getSchema(schema.$id) : void 0;
440
415
  validators.set(name, byId ?? ajv.compile(schema));
441
416
  }
@@ -515,15 +490,15 @@ function buildSchemaValidators() {
515
490
  };
516
491
  }
517
492
  function buildProviderFrontmatterValidator(providers) {
518
- const specRoot = resolveSpecRoot2();
519
- const ajv = new Ajv20202({
493
+ const specRoot = resolveSpecRoot();
494
+ const ajv = new Ajv20203({
520
495
  strict: false,
521
496
  allErrors: true,
522
497
  allowUnionTypes: true
523
498
  });
524
499
  applyAjvFormats(ajv);
525
- const baseFile = resolve2(specRoot, "schemas/frontmatter/base.schema.json");
526
- const baseSchema = JSON.parse(readFileSync2(baseFile, "utf8"));
500
+ const baseFile = resolve4(specRoot, "schemas/frontmatter/base.schema.json");
501
+ const baseSchema = JSON.parse(readFileSync3(baseFile, "utf8"));
527
502
  ajv.addSchema(baseSchema);
528
503
  registerProviderAuxiliarySchemas(ajv, providers);
529
504
  const compiled = /* @__PURE__ */ new Map();
@@ -560,11 +535,11 @@ function registerProviderAuxiliarySchemas(ajv, providers) {
560
535
  }
561
536
  }
562
537
  }
563
- function resolveSpecRoot2() {
538
+ function resolveSpecRoot() {
564
539
  const require2 = createRequire2(import.meta.url);
565
540
  try {
566
541
  const indexPath = require2.resolve("@skill-map/spec/index.json");
567
- return dirname2(indexPath);
542
+ return dirname(indexPath);
568
543
  } catch {
569
544
  throw new Error(
570
545
  "@skill-map/spec not resolvable \u2014 ensure the workspace is linked or the package is installed."
@@ -573,7 +548,7 @@ function resolveSpecRoot2() {
573
548
  }
574
549
  function existsSyncSafe(path) {
575
550
  try {
576
- readFileSync2(path, "utf8");
551
+ readFileSync3(path, "utf8");
577
552
  return true;
578
553
  } catch {
579
554
  return false;
@@ -585,45 +560,6 @@ function formatErrorMessage(err) {
585
560
  return err instanceof Error ? err.message : String(err);
586
561
  }
587
562
 
588
- // kernel/util/skill-map-paths.ts
589
- import { join as join3 } from "path";
590
-
591
- // core/paths/db-path.ts
592
- import { join as join2, resolve as resolve3 } from "path";
593
- var SKILL_MAP_DIR = ".skill-map";
594
- var DB_FILENAME = "skill-map.db";
595
- var LOCAL_SETTINGS_FILENAME = "settings.local.json";
596
- var DEFAULT_DB_REL = `${SKILL_MAP_DIR}/${DB_FILENAME}`;
597
- var GITIGNORE_ENTRIES = [
598
- `${SKILL_MAP_DIR}/${LOCAL_SETTINGS_FILENAME}`,
599
- `${SKILL_MAP_DIR}/${DB_FILENAME}`
600
- ];
601
-
602
- // core/config/atomic-write.ts
603
- import {
604
- existsSync as existsSync4,
605
- mkdirSync,
606
- readFileSync as readFileSync4,
607
- renameSync,
608
- unlinkSync,
609
- writeFileSync
610
- } from "fs";
611
- import { dirname as dirname3 } from "path";
612
-
613
- // kernel/adapters/in-memory-progress.ts
614
- var InMemoryProgressEmitter = class {
615
- #listeners = /* @__PURE__ */ new Set();
616
- emit(event) {
617
- for (const listener of this.#listeners) listener(event);
618
- }
619
- subscribe(listener) {
620
- this.#listeners.add(listener);
621
- return () => {
622
- this.#listeners.delete(listener);
623
- };
624
- }
625
- };
626
-
627
563
  // kernel/adapters/silent-logger.ts
628
564
  var SilentLogger = class {
629
565
  trace() {
@@ -657,115 +593,83 @@ function getActiveLogger() {
657
593
  return active;
658
594
  }
659
595
 
660
- // kernel/adapters/plugin-loader.ts
661
- import { createRequire as createRequire4 } from "module";
662
- import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync2 } from "fs";
663
- import { isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve6 } from "path";
664
- import { pathToFileURL } from "url";
665
- import { Ajv2020 as Ajv20204 } from "ajv/dist/2020.js";
666
- import semver from "semver";
667
-
668
- // kernel/i18n/plugin-store.texts.ts
669
- var PLUGIN_STORE_TEXTS = {
670
- kvValidationFailed: "plugin '{{pluginId}}' ctx.store.set('{{key}}', value): value violates declared schema ({{schemaPath}}) \u2014 {{errors}}",
671
- dedicatedValidationFailed: "plugin '{{pluginId}}' ctx.store.write('{{table}}', row): row violates declared schema ({{schemaPath}}) \u2014 {{errors}}"
672
- };
673
-
674
- // kernel/adapters/plugin-store.ts
675
- var KV_SCHEMA_KEY = "__kv__";
676
- function makeKvStoreWrapper(opts) {
677
- const { pluginId, schema, persist } = opts;
678
- return {
679
- async set(key, value) {
680
- if (schema) {
681
- if (!schema.validate(value)) {
682
- throw new Error(
683
- tx(PLUGIN_STORE_TEXTS.kvValidationFailed, {
684
- pluginId,
685
- schemaPath: schema.schemaPath,
686
- key,
687
- errors: formatAjvErrors(schema.validate.errors ?? null)
688
- })
689
- );
690
- }
691
- }
692
- await persist(key, value);
596
+ // kernel/extensions/hook-dispatcher.ts
597
+ function makeHookDispatcher(hooks, emitter) {
598
+ if (hooks.length === 0) {
599
+ return { dispatch: async () => {
600
+ } };
601
+ }
602
+ const byTrigger = /* @__PURE__ */ new Map();
603
+ for (const hook of hooks) {
604
+ if (hook.mode === "probabilistic") {
605
+ const qualifiedId = qualifiedExtensionId(hook.pluginId, hook.id);
606
+ log.warn(
607
+ `Probabilistic hook ${qualifiedId} deferred to job subsystem (future job subsystem). The hook is registered but will not dispatch in-scan.`,
608
+ { hookId: qualifiedId, mode: "probabilistic" }
609
+ );
610
+ continue;
693
611
  }
694
- };
695
- }
696
- function makeDedicatedStoreWrapper(opts) {
697
- const { pluginId, schemas, persist } = opts;
612
+ for (const trig of hook.triggers) {
613
+ const bucket = byTrigger.get(trig);
614
+ if (bucket) bucket.push(hook);
615
+ else byTrigger.set(trig, [hook]);
616
+ }
617
+ }
698
618
  return {
699
- async write(table, row) {
700
- const schema = schemas?.[table];
701
- if (schema) {
702
- if (!schema.validate(row)) {
703
- throw new Error(
704
- tx(PLUGIN_STORE_TEXTS.dedicatedValidationFailed, {
705
- pluginId,
706
- table,
707
- schemaPath: schema.schemaPath,
708
- errors: formatAjvErrors(schema.validate.errors ?? null)
619
+ async dispatch(trigger, event) {
620
+ const subs = byTrigger.get(trigger);
621
+ if (!subs || subs.length === 0) return;
622
+ for (const hook of subs) {
623
+ if (!matchesFilter(hook, event)) continue;
624
+ const ctx = buildHookContext(hook, trigger, event);
625
+ try {
626
+ await hook.on(ctx);
627
+ } catch (err) {
628
+ const qualifiedId = qualifiedExtensionId(hook.pluginId, hook.id);
629
+ const message = formatErrorMessage(err);
630
+ emitter.emit(
631
+ makeEvent("extension.error", {
632
+ kind: "hook-error",
633
+ extensionId: qualifiedId,
634
+ trigger,
635
+ message
709
636
  })
710
637
  );
711
638
  }
712
639
  }
713
- await persist(table, row);
714
640
  }
715
641
  };
716
642
  }
717
- function makePluginStore(opts) {
718
- const manifest = opts.plugin.manifest;
719
- if (!manifest?.storage) return void 0;
720
- const storageSchemas = opts.plugin.storageSchemas;
721
- if (manifest.storage.mode === "kv") {
722
- if (!opts.persistKv) return void 0;
723
- const schema = storageSchemas?.[KV_SCHEMA_KEY];
724
- return makeKvStoreWrapper({
725
- pluginId: manifest.id,
726
- schema,
727
- persist: opts.persistKv
728
- });
729
- }
730
- if (manifest.storage.mode === "dedicated") {
731
- if (!opts.persistDedicated) return void 0;
732
- return makeDedicatedStoreWrapper({
733
- pluginId: manifest.id,
734
- schemas: storageSchemas,
735
- persist: opts.persistDedicated
736
- });
737
- }
738
- return void 0;
643
+ function makeEvent(type, data) {
644
+ return { type, timestamp: (/* @__PURE__ */ new Date()).toISOString(), data };
739
645
  }
740
- function formatAjvErrors(errors) {
741
- if (!errors || errors.length === 0) return "(no AJV details)";
742
- return errors.map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
646
+ function matchesFilter(hook, event) {
647
+ if (!hook.filter) return true;
648
+ const data = event.data ?? {};
649
+ for (const [key, expected] of Object.entries(hook.filter)) {
650
+ if (data[key] !== expected) return false;
651
+ }
652
+ return true;
743
653
  }
744
-
745
- // kernel/extensions/hook.ts
746
- var HOOK_TRIGGERS = Object.freeze([
747
- "boot",
748
- "scan.started",
749
- "scan.completed",
750
- "extractor.completed",
751
- "analyzer.completed",
752
- "action.completed",
753
- "job.spawning",
754
- "job.completed",
755
- "job.failed",
756
- "shutdown"
757
- ]);
758
-
759
- // kernel/adapters/plugin-loader.ts
760
- var KNOWN_KINDS = /* @__PURE__ */ new Set(["provider", "extractor", "analyzer", "action", "formatter", "hook"]);
761
- var KNOWN_KINDS_LIST = [...KNOWN_KINDS].join(" / ");
762
- var HOOKABLE_TRIGGERS_LIST = HOOK_TRIGGERS.join(", ");
763
- function installedSpecVersion() {
764
- const require2 = createRequire4(import.meta.url);
765
- const indexPath = require2.resolve("@skill-map/spec/index.json");
766
- const pkgPath = resolve6(indexPath, "..", "package.json");
767
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
768
- return pkg.version;
654
+ function buildHookContext(_hook, trigger, event) {
655
+ const data = event.data ?? {};
656
+ const ctx = {
657
+ event: {
658
+ type: trigger,
659
+ timestamp: event.timestamp,
660
+ ...event.runId !== void 0 ? { runId: event.runId } : {},
661
+ ...event.jobId !== void 0 ? { jobId: event.jobId } : {},
662
+ data: event.data
663
+ }
664
+ };
665
+ if (typeof data["extractorId"] === "string") ctx.extractorId = data["extractorId"];
666
+ if (typeof data["analyzerId"] === "string") ctx.analyzerId = data["analyzerId"];
667
+ if (typeof data["actionId"] === "string") ctx.actionId = data["actionId"];
668
+ if (data["node"] && typeof data["node"] === "object") {
669
+ ctx.node = data["node"];
670
+ }
671
+ if (data["jobResult"] !== void 0) ctx.jobResult = data["jobResult"];
672
+ return ctx;
769
673
  }
770
674
 
771
675
  // kernel/i18n/orchestrator.texts.ts
@@ -782,1081 +686,1004 @@ var ORCHESTRATOR_TEXTS = {
782
686
  runScanRootMissing: "runScan: root path '{{root}}' does not exist or is not a directory"
783
687
  };
784
688
 
785
- // kernel/scan/walk-content.ts
786
- import { readFile, readdir, stat } from "fs/promises";
787
- import { join as join5, relative as relative3, sep as sep2 } from "path";
788
-
789
- // kernel/scan/ignore.ts
790
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
791
- import { dirname as dirname5, resolve as resolve7 } from "path";
792
- import { fileURLToPath } from "url";
793
- import ignoreFactory from "ignore";
794
- function buildIgnoreFilter(opts = {}) {
795
- const ig = ignoreFactory();
796
- if (opts.includeDefaults !== false) {
797
- ig.add(loadDefaultsText());
798
- }
799
- if (opts.configIgnore && opts.configIgnore.length > 0) {
800
- ig.add(opts.configIgnore);
801
- }
802
- if (opts.ignoreFileText && opts.ignoreFileText.length > 0) {
803
- ig.add(opts.ignoreFileText);
804
- }
805
- return {
806
- ignores(relativePath) {
807
- if (relativePath === "" || relativePath === "." || relativePath === "./") {
808
- return false;
689
+ // kernel/orchestrator/extractors.ts
690
+ async function runExtractorsForNode(opts) {
691
+ const internalLinks = [];
692
+ const externalLinks = [];
693
+ const enrichmentBuffer = /* @__PURE__ */ new Map();
694
+ const contributions = [];
695
+ const validators = loadSchemaValidators();
696
+ for (const extractor of opts.extractors) {
697
+ const qualifiedId = qualifiedExtensionId(extractor.pluginId, extractor.id);
698
+ const emitLink = (link) => {
699
+ const validated = validateLink(extractor, link, opts.emitter);
700
+ if (!validated) return;
701
+ if (isExternalUrlLink(validated)) externalLinks.push(validated);
702
+ else internalLinks.push(validated);
703
+ };
704
+ const enrichNode = (partial) => {
705
+ const key = `${opts.node.path}\0${qualifiedId}`;
706
+ const existing = enrichmentBuffer.get(key);
707
+ if (existing) {
708
+ existing.value = { ...existing.value, ...partial };
709
+ existing.enrichedAt = Date.now();
710
+ } else {
711
+ enrichmentBuffer.set(key, {
712
+ nodePath: opts.node.path,
713
+ extractorId: qualifiedId,
714
+ bodyHashAtEnrichment: opts.bodyHash,
715
+ value: { ...partial },
716
+ enrichedAt: Date.now(),
717
+ // Extractors are deterministic-only; `is_probabilistic` is
718
+ // reserved on the row for future Action-issued enrichments.
719
+ isProbabilistic: false
720
+ });
809
721
  }
810
- const normalised = relativePath.replace(/^\.\//, "").replace(/\\/g, "/").replace(/^\//, "");
811
- if (normalised === "") return false;
812
- return ig.ignores(normalised);
813
- }
814
- };
815
- }
816
- var cachedDefaults = null;
817
- function loadDefaultsText() {
818
- if (cachedDefaults !== null) return cachedDefaults;
819
- cachedDefaults = readDefaultsFromDisk();
820
- return cachedDefaults;
821
- }
822
- function readDefaultsFromDisk() {
823
- const here = dirname5(fileURLToPath(import.meta.url));
824
- const candidates = [
825
- resolve7(here, "../../config/defaults/skillmapignore"),
826
- // src/kernel/scan/ → src/config/defaults/
827
- resolve7(here, "../config/defaults/skillmapignore"),
828
- // dist/cli.js → dist/config/defaults/ (siblings)
829
- resolve7(here, "config/defaults/skillmapignore")
830
- ];
831
- for (const candidate of candidates) {
832
- if (existsSync7(candidate)) {
833
- try {
834
- return readFileSync7(candidate, "utf8");
835
- } catch {
722
+ };
723
+ const declaredContributions = readDeclaredContributions(extractor);
724
+ const emitContribution = (contributionId, payload) => {
725
+ const declared = declaredContributions.get(contributionId);
726
+ if (!declared) {
727
+ emitExtensionError(opts.emitter, qualifiedId, opts.node.path, {
728
+ phase: "emitContribution",
729
+ contributionId,
730
+ reason: "unknown-contribution-id",
731
+ message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {
732
+ extractorId: qualifiedId,
733
+ contributionId,
734
+ nodePath: opts.node.path
735
+ })
736
+ });
737
+ return;
836
738
  }
837
- }
739
+ const result = validators.validateContributionPayload(declared.slot, payload);
740
+ if (!result.ok) {
741
+ emitExtensionError(opts.emitter, qualifiedId, opts.node.path, {
742
+ phase: "emitContribution",
743
+ contributionId,
744
+ slot: declared.slot,
745
+ reason: result.errors,
746
+ message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
747
+ extractorId: qualifiedId,
748
+ contributionId,
749
+ nodePath: opts.node.path,
750
+ slot: declared.slot,
751
+ errors: result.errors
752
+ })
753
+ });
754
+ return;
755
+ }
756
+ contributions.push({
757
+ pluginId: extractor.pluginId,
758
+ extensionId: extractor.id,
759
+ nodePath: opts.node.path,
760
+ contributionId,
761
+ slot: declared.slot,
762
+ payload,
763
+ emittedAt: Date.now()
764
+ });
765
+ };
766
+ const store = opts.pluginStores?.get(extractor.pluginId);
767
+ const ctx = buildExtractorContext(
768
+ extractor,
769
+ opts.node,
770
+ opts.body,
771
+ opts.frontmatter,
772
+ emitLink,
773
+ enrichNode,
774
+ emitContribution,
775
+ store
776
+ );
777
+ await extractor.extract(ctx);
838
778
  }
839
- return "";
779
+ return {
780
+ internalLinks,
781
+ externalLinks,
782
+ enrichments: Array.from(enrichmentBuffer.values()),
783
+ contributions
784
+ };
840
785
  }
841
-
842
- // built-in-plugins/parsers/frontmatter-yaml/index.ts
843
- import yaml3 from "js-yaml";
844
- var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
845
- var FORBIDDEN_FRONTMATTER_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
846
- var frontmatterYamlParser = {
847
- id: "frontmatter-yaml",
848
- parse(raw, _path) {
849
- const match = FRONTMATTER_RE.exec(raw);
850
- if (!match) return { frontmatterRaw: "", frontmatter: {}, body: raw };
851
- const frontmatterRaw = match[1];
852
- const body = match[2];
853
- const parsed = {};
854
- try {
855
- const doc = yaml3.load(frontmatterRaw, { schema: yaml3.JSON_SCHEMA });
856
- if (doc && typeof doc === "object" && !Array.isArray(doc)) {
857
- for (const [k, v] of Object.entries(doc)) {
858
- if (FORBIDDEN_FRONTMATTER_KEYS.has(k)) continue;
859
- parsed[k] = v;
860
- }
861
- }
862
- } catch {
863
- }
864
- return { frontmatterRaw, frontmatter: parsed, body };
786
+ function readDeclaredContributions(extension) {
787
+ const out = /* @__PURE__ */ new Map();
788
+ const raw = extension.viewContributions;
789
+ if (typeof raw !== "object" || raw === null) return out;
790
+ for (const [id, value] of Object.entries(raw)) {
791
+ if (typeof value !== "object" || value === null) continue;
792
+ const slot = value.slot;
793
+ if (typeof slot !== "string") continue;
794
+ out.set(id, { slot });
865
795
  }
866
- };
867
-
868
- // built-in-plugins/parsers/plain/index.ts
869
- var plainParser = {
870
- id: "plain",
871
- parse(raw, _path) {
872
- return { frontmatter: {}, frontmatterRaw: "", body: raw };
796
+ return out;
797
+ }
798
+ function emitExtensionError(emitter, qualifiedId, nodePath, data) {
799
+ emitter.emit(
800
+ makeEvent("extension.error", {
801
+ kind: "contribution-rejected",
802
+ extensionId: qualifiedId,
803
+ nodePath,
804
+ ...data
805
+ })
806
+ );
807
+ }
808
+ function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, emitContribution, store) {
809
+ const scope = extractor.scope;
810
+ return {
811
+ node,
812
+ body: scope === "frontmatter" ? "" : body,
813
+ frontmatter: scope === "body" ? {} : frontmatter,
814
+ emitLink,
815
+ enrichNode,
816
+ emitContribution,
817
+ ...store !== void 0 ? { store } : {}
818
+ };
819
+ }
820
+ function validateLink(extractor, link, emitter) {
821
+ if (!extractor.emitsLinkKinds.includes(link.kind)) {
822
+ const qualifiedId = `${extractor.pluginId}/${extractor.id}`;
823
+ emitter.emit(
824
+ makeEvent("extension.error", {
825
+ kind: "link-kind-not-declared",
826
+ extensionId: qualifiedId,
827
+ linkKind: link.kind,
828
+ declaredKinds: extractor.emitsLinkKinds,
829
+ link: { source: link.source, target: link.target, kind: link.kind },
830
+ message: tx(ORCHESTRATOR_TEXTS.extensionErrorLinkKindNotDeclared, {
831
+ extractorId: qualifiedId,
832
+ linkKind: link.kind,
833
+ declaredKinds: extractor.emitsLinkKinds.join(", ")
834
+ })
835
+ })
836
+ );
837
+ return null;
873
838
  }
874
- };
875
-
876
- // kernel/scan/parsers/index.ts
877
- var REGISTRY = /* @__PURE__ */ new Map([
878
- [frontmatterYamlParser.id, frontmatterYamlParser],
879
- [plainParser.id, plainParser]
880
- ]);
881
- var FROZEN_IDS = new Set(REGISTRY.keys());
882
- function getParser(id) {
883
- return REGISTRY.get(id);
839
+ const confidence = link.confidence ?? extractor.defaultConfidence;
840
+ return { ...link, confidence };
884
841
  }
885
-
886
- // kernel/scan/walk-content.ts
887
- var UnknownParserError = class extends Error {
888
- constructor(parserId) {
889
- super(`Unknown parser id '${parserId}'. Built-in parsers: 'frontmatter-yaml', 'plain'.`);
890
- this.name = "UnknownParserError";
842
+ function recomputeLinkCounts(nodes, links) {
843
+ const byPath2 = /* @__PURE__ */ new Map();
844
+ for (const node of nodes) {
845
+ node.linksOutCount = 0;
846
+ node.linksInCount = 0;
847
+ byPath2.set(node.path, node);
891
848
  }
892
- };
893
- async function* walkContent(roots, options) {
894
- const parser = getParser(options.parser);
895
- if (!parser) throw new UnknownParserError(options.parser);
896
- const filter = options.ignoreFilter ?? buildIgnoreFilter();
897
- const extensions = options.extensions;
898
- for (const root of roots) {
899
- for await (const file of walkRoot(root, root, filter, extensions)) {
900
- const relPath = relative3(root, file).split(sep2).join("/");
901
- let raw;
902
- try {
903
- raw = await readFile(file, "utf8");
904
- } catch {
905
- continue;
906
- }
907
- const parsed = parser.parse(raw, relPath);
908
- yield {
909
- path: relPath,
910
- body: parsed.body,
911
- frontmatterRaw: parsed.frontmatterRaw,
912
- frontmatter: parsed.frontmatter
913
- };
914
- }
849
+ for (const link of links) {
850
+ const source = byPath2.get(link.source);
851
+ if (source) source.linksOutCount += 1;
852
+ const target = byPath2.get(link.target);
853
+ if (target) target.linksInCount += 1;
915
854
  }
916
855
  }
917
- async function* walkRoot(root, current, filter, extensions) {
918
- let entries;
919
- try {
920
- entries = await readdir(current, { withFileTypes: true, encoding: "utf8" });
921
- } catch {
922
- return;
856
+ function recomputeExternalRefsCount(nodes, externalLinks, cachedPaths) {
857
+ const byPath2 = /* @__PURE__ */ new Map();
858
+ for (const node of nodes) {
859
+ if (!cachedPaths.has(node.path)) node.externalRefsCount = 0;
860
+ byPath2.set(node.path, node);
923
861
  }
924
- for (const entry of entries) {
925
- const name = entry.name;
926
- const full = join5(current, name);
927
- const rel = relative3(root, full).split(sep2).join("/");
928
- if (filter.ignores(rel)) continue;
929
- if (entry.isSymbolicLink()) continue;
930
- if (entry.isDirectory()) {
931
- yield* walkRoot(root, full, filter, extensions);
932
- } else if (entry.isFile() && hasMatchingExtension(name, extensions)) {
933
- try {
934
- const s = await stat(full);
935
- if (s.isFile()) yield full;
936
- } catch {
937
- }
938
- }
862
+ for (const link of externalLinks) {
863
+ const source = byPath2.get(link.source);
864
+ if (source && !cachedPaths.has(source.path)) source.externalRefsCount += 1;
939
865
  }
940
866
  }
941
- function hasMatchingExtension(name, extensions) {
942
- for (const ext of extensions) {
943
- if (name.endsWith(ext)) return true;
944
- }
945
- return false;
867
+ var EXTERNAL_URL_SCHEME_RE = /^[a-z][a-z0-9+\-.]+:/i;
868
+ function isExternalUrlLink(link) {
869
+ return EXTERNAL_URL_SCHEME_RE.test(link.target);
946
870
  }
947
871
 
948
- // kernel/extensions/provider.ts
949
- var DEFAULT_READ_CONFIG = Object.freeze({
950
- extensions: Object.freeze([".md"]),
951
- parser: "frontmatter-yaml"
952
- });
953
- function resolveProviderWalk(provider) {
954
- if (provider.walk) {
955
- const walk2 = provider.walk.bind(provider);
956
- return walk2;
957
- }
958
- const read = provider.read ?? DEFAULT_READ_CONFIG;
959
- return (roots, options) => {
960
- const walkOptions = {
961
- extensions: read.extensions,
962
- parser: read.parser
872
+ // kernel/orchestrator/analyzers.ts
873
+ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, emitter, hookDispatcher) {
874
+ const issues = [];
875
+ const contributions = [];
876
+ const validators = loadSchemaValidators();
877
+ const analyzerOrphans = orphanSidecars.map((o) => ({
878
+ relativePath: o.relativePath,
879
+ expectedMdPath: o.expectedMdPath
880
+ }));
881
+ for (const analyzer of analyzers) {
882
+ const qualifiedId = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
883
+ const declaredContributions = readDeclaredContributions(analyzer);
884
+ const emitContribution = (nodePath, contributionId, payload) => {
885
+ const declared = declaredContributions.get(contributionId);
886
+ if (!declared) {
887
+ emitExtensionError(emitter, qualifiedId, nodePath, {
888
+ phase: "emitContribution",
889
+ contributionId,
890
+ reason: "unknown-contribution-id",
891
+ message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {
892
+ extractorId: qualifiedId,
893
+ contributionId,
894
+ nodePath
895
+ })
896
+ });
897
+ return;
898
+ }
899
+ const result = validators.validateContributionPayload(declared.slot, payload);
900
+ if (!result.ok) {
901
+ emitExtensionError(emitter, qualifiedId, nodePath, {
902
+ phase: "emitContribution",
903
+ contributionId,
904
+ slot: declared.slot,
905
+ reason: result.errors,
906
+ message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
907
+ extractorId: qualifiedId,
908
+ contributionId,
909
+ nodePath,
910
+ slot: declared.slot,
911
+ errors: result.errors
912
+ })
913
+ });
914
+ return;
915
+ }
916
+ contributions.push({
917
+ pluginId: analyzer.pluginId,
918
+ extensionId: analyzer.id,
919
+ nodePath,
920
+ contributionId,
921
+ slot: declared.slot,
922
+ payload,
923
+ emittedAt: Date.now()
924
+ });
963
925
  };
964
- if (options?.ignoreFilter) walkOptions.ignoreFilter = options.ignoreFilter;
965
- return walkContent(roots, walkOptions);
966
- };
926
+ const emitted = await analyzer.evaluate({
927
+ nodes,
928
+ links: internalLinks,
929
+ orphanSidecars: analyzerOrphans,
930
+ sidecarRoots,
931
+ annotationContributions,
932
+ viewContributions,
933
+ orphanJobFiles,
934
+ ...referenceablePaths ? { referenceablePaths } : {},
935
+ ...cwd ? { cwd } : {},
936
+ emitContribution
937
+ });
938
+ for (const issue of emitted) {
939
+ const validated = validateIssue(analyzer, issue, emitter);
940
+ if (validated) issues.push(validated);
941
+ }
942
+ const evt = makeEvent("analyzer.completed", { analyzerId: qualifiedId });
943
+ emitter.emit(evt);
944
+ await hookDispatcher.dispatch("analyzer.completed", evt);
945
+ }
946
+ return { issues, contributions };
947
+ }
948
+ function validateIssue(analyzer, issue, emitter) {
949
+ const severity = issue.severity;
950
+ if (severity !== "error" && severity !== "warn" && severity !== "info") {
951
+ const qualifiedId = `${analyzer.pluginId}/${analyzer.id}`;
952
+ emitter.emit(
953
+ makeEvent("extension.error", {
954
+ kind: "issue-invalid-severity",
955
+ extensionId: qualifiedId,
956
+ severity,
957
+ issue: { analyzerId: issue.analyzerId || analyzer.id, message: issue.message, nodeIds: issue.nodeIds },
958
+ message: tx(ORCHESTRATOR_TEXTS.extensionErrorIssueInvalidSeverity, {
959
+ analyzerId: qualifiedId,
960
+ severity: JSON.stringify(severity)
961
+ })
962
+ })
963
+ );
964
+ return null;
965
+ }
966
+ return { ...issue, analyzerId: issue.analyzerId || analyzer.id };
967
967
  }
968
968
 
969
- // kernel/extensions/hook-dispatcher.ts
970
- function makeHookDispatcher(hooks, emitter) {
971
- if (hooks.length === 0) {
972
- return { dispatch: async () => {
973
- } };
969
+ // kernel/orchestrator/cache.ts
970
+ function indexPriorSnapshot(prior) {
971
+ const priorNodesByPath = /* @__PURE__ */ new Map();
972
+ const priorNodePaths = /* @__PURE__ */ new Set();
973
+ const priorLinksByOriginating = /* @__PURE__ */ new Map();
974
+ const priorFrontmatterIssuesByNode = /* @__PURE__ */ new Map();
975
+ if (!prior) {
976
+ return { priorNodesByPath, priorNodePaths, priorLinksByOriginating, priorFrontmatterIssuesByNode };
974
977
  }
975
- const byTrigger = /* @__PURE__ */ new Map();
976
- for (const hook of hooks) {
977
- if (hook.mode === "probabilistic") {
978
- const qualifiedId = qualifiedExtensionId(hook.pluginId, hook.id);
979
- log.warn(
980
- `Probabilistic hook ${qualifiedId} deferred to job subsystem (future job subsystem). The hook is registered but will not dispatch in-scan.`,
981
- { hookId: qualifiedId, mode: "probabilistic" }
982
- );
983
- continue;
984
- }
985
- for (const trig of hook.triggers) {
986
- const bucket = byTrigger.get(trig);
987
- if (bucket) bucket.push(hook);
988
- else byTrigger.set(trig, [hook]);
989
- }
978
+ indexPriorNodes(prior.nodes, priorNodesByPath, priorNodePaths);
979
+ indexPriorLinks(prior.links, priorNodePaths, priorLinksByOriginating);
980
+ indexPriorFrontmatterIssues(prior.issues, priorFrontmatterIssuesByNode);
981
+ return { priorNodesByPath, priorNodePaths, priorLinksByOriginating, priorFrontmatterIssuesByNode };
982
+ }
983
+ function indexPriorNodes(nodes, byPath2, paths) {
984
+ for (const node of nodes) {
985
+ byPath2.set(node.path, node);
986
+ paths.add(node.path);
990
987
  }
991
- return {
992
- async dispatch(trigger, event) {
993
- const subs = byTrigger.get(trigger);
994
- if (!subs || subs.length === 0) return;
995
- for (const hook of subs) {
996
- if (!matchesFilter(hook, event)) continue;
997
- const ctx = buildHookContext(hook, trigger, event);
998
- try {
999
- await hook.on(ctx);
1000
- } catch (err) {
1001
- const qualifiedId = qualifiedExtensionId(hook.pluginId, hook.id);
1002
- const message = formatErrorMessage(err);
1003
- emitter.emit(
1004
- makeEvent("extension.error", {
1005
- kind: "hook-error",
1006
- extensionId: qualifiedId,
1007
- trigger,
1008
- message
1009
- })
1010
- );
1011
- }
1012
- }
1013
- }
1014
- };
1015
988
  }
1016
- function makeEvent(type, data) {
1017
- return { type, timestamp: (/* @__PURE__ */ new Date()).toISOString(), data };
989
+ function indexPriorLinks(links, priorNodePaths, byOriginating) {
990
+ for (const link of links) {
991
+ const key = originatingNodeOf(link, priorNodePaths);
992
+ const list = byOriginating.get(key);
993
+ if (list) list.push(link);
994
+ else byOriginating.set(key, [link]);
995
+ }
1018
996
  }
1019
- function matchesFilter(hook, event) {
1020
- if (!hook.filter) return true;
1021
- const data = event.data ?? {};
1022
- for (const [key, expected] of Object.entries(hook.filter)) {
1023
- if (data[key] !== expected) return false;
997
+ function indexPriorFrontmatterIssues(issues, byNode) {
998
+ for (const issue of issues) {
999
+ if (issue.analyzerId !== "frontmatter-invalid" && issue.analyzerId !== "frontmatter-malformed") continue;
1000
+ if (issue.nodeIds.length !== 1) continue;
1001
+ const path = issue.nodeIds[0];
1002
+ const list = byNode.get(path);
1003
+ if (list) list.push(issue);
1004
+ else byNode.set(path, [issue]);
1024
1005
  }
1025
- return true;
1026
1006
  }
1027
- function buildHookContext(_hook, trigger, event) {
1028
- const data = event.data ?? {};
1029
- const ctx = {
1030
- event: {
1031
- type: trigger,
1032
- timestamp: event.timestamp,
1033
- ...event.runId !== void 0 ? { runId: event.runId } : {},
1034
- ...event.jobId !== void 0 ? { jobId: event.jobId } : {},
1035
- data: event.data
1036
- }
1037
- };
1038
- if (typeof data["extractorId"] === "string") ctx.extractorId = data["extractorId"];
1039
- if (typeof data["analyzerId"] === "string") ctx.analyzerId = data["analyzerId"];
1040
- if (typeof data["actionId"] === "string") ctx.actionId = data["actionId"];
1041
- if (data["node"] && typeof data["node"] === "object") {
1042
- ctx.node = data["node"];
1007
+ function originatingNodeOf(link, priorNodePaths) {
1008
+ if (link.kind === "supersedes" && !priorNodePaths.has(link.source)) {
1009
+ return link.target;
1043
1010
  }
1044
- if (data["jobResult"] !== void 0) ctx.jobResult = data["jobResult"];
1045
- return ctx;
1011
+ return link.source;
1046
1012
  }
1047
-
1048
- // kernel/orchestrator.ts
1049
- var SCANNED_BY = {
1050
- name: "skill-map",
1051
- version: package_default.version,
1052
- specVersion: resolveSpecVersionSafe()
1053
- };
1054
- function resolveSpecVersionSafe() {
1055
- try {
1056
- return installedSpecVersion();
1057
- } catch {
1058
- return "unknown";
1013
+ function computeCacheDecision(opts) {
1014
+ const applicableExtractors = opts.extractors.filter(
1015
+ (ex) => ex.applicableKinds === void 0 || ex.applicableKinds.includes(opts.kind)
1016
+ );
1017
+ const applicableQualifiedIds = new Set(
1018
+ applicableExtractors.map((ex) => qualifiedExtensionId(ex.pluginId, ex.id))
1019
+ );
1020
+ const split = opts.priorExtractorRuns === void 0 ? splitLegacy(applicableExtractors, applicableQualifiedIds, opts.nodeHashCacheEligible) : splitFineGrained(applicableExtractors, opts);
1021
+ return {
1022
+ applicableExtractors,
1023
+ applicableQualifiedIds,
1024
+ cachedQualifiedIds: split.cachedQualifiedIds,
1025
+ missingExtractors: split.missingExtractors,
1026
+ fullCacheHit: opts.nodeHashCacheEligible && split.missingExtractors.length === 0
1027
+ };
1028
+ }
1029
+ function splitLegacy(applicableExtractors, applicableQualifiedIds, nodeHashCacheEligible) {
1030
+ const cachedQualifiedIds = /* @__PURE__ */ new Set();
1031
+ const missingExtractors = [];
1032
+ if (nodeHashCacheEligible) {
1033
+ for (const id of applicableQualifiedIds) cachedQualifiedIds.add(id);
1034
+ } else {
1035
+ for (const ex of applicableExtractors) missingExtractors.push(ex);
1059
1036
  }
1037
+ return { cachedQualifiedIds, missingExtractors };
1060
1038
  }
1061
- async function runScanWithRenames(_kernel, options) {
1062
- return runScanInternal(_kernel, options);
1039
+ function splitFineGrained(applicableExtractors, opts) {
1040
+ const cachedQualifiedIds = /* @__PURE__ */ new Set();
1041
+ const missingExtractors = [];
1042
+ const priorRunsForNode = opts.priorExtractorRuns.get(opts.nodePath) ?? /* @__PURE__ */ new Map();
1043
+ for (const ex of applicableExtractors) {
1044
+ const qualified = qualifiedExtensionId(ex.pluginId, ex.id);
1045
+ const prior = priorRunsForNode.get(qualified);
1046
+ if (opts.nodeHashCacheEligible && prior !== void 0 && prior.bodyHash === opts.bodyHash && prior.sidecarAnnotationsHash === opts.sidecarAnnotationsHash) {
1047
+ cachedQualifiedIds.add(qualified);
1048
+ } else {
1049
+ missingExtractors.push(ex);
1050
+ }
1051
+ }
1052
+ return { cachedQualifiedIds, missingExtractors };
1063
1053
  }
1064
- async function runScan(_kernel, options) {
1065
- const { result } = await runScanInternal(_kernel, options);
1066
- return result;
1054
+ function cloneNodeAndReshapeLinks(opts) {
1055
+ const node = { ...opts.priorNode, bytes: { ...opts.priorNode.bytes } };
1056
+ if (opts.priorNode.tokens) node.tokens = { ...opts.priorNode.tokens };
1057
+ const internalLinks = [];
1058
+ const reusedLinks = opts.priorLinksByOriginating.get(opts.priorNode.path) ?? [];
1059
+ for (const link of reusedLinks) {
1060
+ const reshaped = reuseCachedLink(
1061
+ link,
1062
+ opts.shortIdToQualified,
1063
+ opts.cachedQualifiedIds,
1064
+ opts.applicableQualifiedIds
1065
+ );
1066
+ if (reshaped) internalLinks.push(reshaped);
1067
+ }
1068
+ const frontmatterIssues = [];
1069
+ const reusedFm = opts.priorFrontmatterIssuesByNode.get(opts.priorNode.path) ?? [];
1070
+ for (const issue of reusedFm) {
1071
+ frontmatterIssues.push({ ...issue, severity: opts.strict ? "error" : "warn" });
1072
+ }
1073
+ return { node, internalLinks, frontmatterIssues };
1067
1074
  }
1068
- async function runScanInternal(_kernel, options) {
1069
- validateRoots(options.roots);
1070
- const start = Date.now();
1071
- const scannedAt = start;
1072
- const emitter = options.emitter ?? new InMemoryProgressEmitter();
1073
- const exts = options.extensions ?? { providers: [], extractors: [], analyzers: [] };
1074
- const hookDispatcher = makeHookDispatcher(exts.hooks ?? [], emitter);
1075
- const tokenize = options.tokenize !== false;
1076
- const scope = options.scope ?? "project";
1077
- const strict = options.strict === true;
1078
- const encoder = tokenize ? new Tiktoken(cl100k_base) : null;
1079
- const prior = options.priorSnapshot ?? null;
1080
- const enableCache = options.enableCache === true;
1081
- const priorExtractorRuns = options.priorExtractorRuns;
1082
- const priorIndex = indexPriorSnapshot(prior);
1083
- const providerFrontmatter = buildProviderFrontmatterValidator(exts.providers);
1084
- const scanStartedEvent = makeEvent("scan.started", { roots: options.roots });
1085
- emitter.emit(scanStartedEvent);
1086
- await hookDispatcher.dispatch("scan.started", scanStartedEvent);
1087
- const walked = await walkAndExtract({
1088
- providers: exts.providers,
1089
- extractors: exts.extractors,
1090
- roots: options.roots,
1091
- ...options.ignoreFilter ? { ignoreFilter: options.ignoreFilter } : {},
1092
- emitter,
1093
- encoder,
1094
- strict,
1095
- enableCache,
1096
- prior,
1097
- priorIndex,
1098
- priorExtractorRuns,
1099
- providerFrontmatter,
1100
- pluginStores: options.pluginStores
1101
- });
1102
- recomputeLinkCounts(walked.nodes, walked.internalLinks);
1103
- recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);
1104
- for (const extractor of exts.extractors) {
1105
- const extractorId = qualifiedExtensionId(extractor.pluginId, extractor.id);
1106
- const evt = makeEvent("extractor.completed", { extractorId });
1107
- emitter.emit(evt);
1108
- await hookDispatcher.dispatch("extractor.completed", evt);
1075
+ function reusePriorNode(opts) {
1076
+ const base = cloneNodeAndReshapeLinks(opts);
1077
+ const ranAt = Date.now();
1078
+ const extractorRuns = [];
1079
+ for (const qualified of opts.cachedQualifiedIds) {
1080
+ extractorRuns.push({
1081
+ nodePath: opts.priorNode.path,
1082
+ extractorId: qualified,
1083
+ bodyHashAtRun: opts.bodyHash,
1084
+ ranAt,
1085
+ sidecarAnnotationsHashAtRun: opts.sidecarAnnotationsHash
1086
+ });
1109
1087
  }
1110
- const analyzerResult = await runAnalyzers(
1111
- exts.analyzers,
1112
- walked.nodes,
1113
- walked.internalLinks,
1114
- walked.orphanSidecars,
1115
- walked.sidecarRoots,
1116
- options.annotationContributions ?? [],
1117
- options.viewContributions ?? [],
1118
- options.orphanJobFiles ?? [],
1119
- options.referenceablePaths,
1120
- options.cwd,
1121
- emitter,
1122
- hookDispatcher
1088
+ return { ...base, extractorRuns };
1089
+ }
1090
+ function reuseCachedLink(link, shortIdToQualified, cachedQualifiedIds, applicableQualifiedIds) {
1091
+ if (!Array.isArray(link.sources) || link.sources.length === 0) return null;
1092
+ const partition = partitionLinkSources(
1093
+ link.sources,
1094
+ shortIdToQualified,
1095
+ cachedQualifiedIds,
1096
+ applicableQualifiedIds
1123
1097
  );
1124
- const issues = analyzerResult.issues;
1125
- for (const c of analyzerResult.contributions) walked.contributions.push(c);
1126
- for (const analyzer of exts.analyzers ?? []) {
1127
- if (analyzer.viewContributions === void 0) continue;
1128
- for (const node of walked.nodes) {
1129
- walked.freshlyRunTuples.add(`${analyzer.pluginId}\0${analyzer.id}\0${node.path}`);
1098
+ if (partition.hasMissing) return null;
1099
+ if (partition.cached.length === 0) return null;
1100
+ if (partition.obsolete.length === 0) return link;
1101
+ return { ...link, sources: partition.cached };
1102
+ }
1103
+ function partitionLinkSources(sources, shortIdToQualified, cachedQualifiedIds, applicableQualifiedIds) {
1104
+ const cached = [];
1105
+ const obsolete = [];
1106
+ let hasMissing = false;
1107
+ for (const source of sources) {
1108
+ const category = classifyLinkSource(
1109
+ source,
1110
+ shortIdToQualified,
1111
+ cachedQualifiedIds,
1112
+ applicableQualifiedIds
1113
+ );
1114
+ if (category === "cached") cached.push(source);
1115
+ else if (category === "missing") hasMissing = true;
1116
+ else obsolete.push(source);
1117
+ }
1118
+ return { cached, obsolete, hasMissing };
1119
+ }
1120
+ function classifyLinkSource(source, shortIdToQualified, cachedQualifiedIds, applicableQualifiedIds) {
1121
+ const candidates = shortIdToQualified.get(source);
1122
+ if (!candidates || candidates.length === 0) return "obsolete";
1123
+ if (candidates.some((q) => cachedQualifiedIds.has(q))) return "cached";
1124
+ if (candidates.some((q) => applicableQualifiedIds.has(q))) return "missing";
1125
+ return "obsolete";
1126
+ }
1127
+
1128
+ // kernel/orchestrator/renames.ts
1129
+ function findHighConfidenceRenames(opts) {
1130
+ const ops = [];
1131
+ for (const fromPath of opts.deletedPaths) {
1132
+ if (opts.claimedDeleted.has(fromPath)) continue;
1133
+ const fromNode = opts.priorByPath.get(fromPath);
1134
+ for (const toPath of opts.newPaths) {
1135
+ if (opts.claimedNew.has(toPath)) continue;
1136
+ const toNode = opts.currentByPath.get(toPath);
1137
+ if (toNode.bodyHash === fromNode.bodyHash) {
1138
+ ops.push({ from: fromPath, to: toPath, confidence: "high" });
1139
+ opts.claimedDeleted.add(fromPath);
1140
+ opts.claimedNew.add(toPath);
1141
+ break;
1142
+ }
1130
1143
  }
1131
1144
  }
1132
- for (const issue of walked.frontmatterIssues) issues.push(issue);
1133
- const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues) : [];
1134
- const stats = {
1135
- // `filesSkipped` is "files walked but not classified by any Provider".
1136
- // Today every walked file IS classified by its Provider (the `claude`
1137
- // Provider's `classify()` always returns a kind, falling back to
1138
- // `'markdown'`), so this is always 0. Wired now so the field shape is
1139
- // spec-conformant; meaningful once multiple Providers compete.
1140
- filesWalked: walked.filesWalked,
1141
- filesSkipped: 0,
1142
- nodesCount: walked.nodes.length,
1143
- linksCount: walked.internalLinks.length,
1144
- issuesCount: issues.length,
1145
- durationMs: Date.now() - start
1146
- };
1147
- const scanCompletedEvent = makeEvent("scan.completed", { stats });
1148
- emitter.emit(scanCompletedEvent);
1149
- await hookDispatcher.dispatch("scan.completed", scanCompletedEvent);
1150
- return {
1151
- result: {
1152
- schemaVersion: 1,
1153
- scannedAt,
1154
- scope,
1155
- roots: options.roots,
1156
- providers: exts.providers.map((a) => a.id),
1157
- scannedBy: SCANNED_BY,
1158
- nodes: walked.nodes,
1159
- links: walked.internalLinks,
1160
- issues,
1161
- stats
1162
- },
1163
- renameOps,
1164
- extractorRuns: walked.extractorRuns,
1165
- enrichments: walked.enrichments,
1166
- contributions: walked.contributions,
1167
- freshlyRunTuples: walked.freshlyRunTuples
1168
- };
1145
+ return ops;
1169
1146
  }
1170
- function validateRoots(roots) {
1171
- if (roots.length === 0) {
1172
- throw new Error(ORCHESTRATOR_TEXTS.runScanRootEmptyArray);
1147
+ function buildFrontmatterRenameCandidates(opts) {
1148
+ const candidatesByNew = /* @__PURE__ */ new Map();
1149
+ for (const toPath of opts.newPaths) {
1150
+ if (opts.claimedNew.has(toPath)) continue;
1151
+ const toNode = opts.currentByPath.get(toPath);
1152
+ const matches = [];
1153
+ for (const fromPath of opts.deletedPaths) {
1154
+ if (opts.claimedDeleted.has(fromPath)) continue;
1155
+ const fromNode = opts.priorByPath.get(fromPath);
1156
+ if (toNode.frontmatterHash === fromNode.frontmatterHash) {
1157
+ matches.push(fromPath);
1158
+ }
1159
+ }
1160
+ if (matches.length > 0) candidatesByNew.set(toPath, matches);
1173
1161
  }
1174
- for (const root of roots) {
1175
- if (!existsSync8(root) || !statSync2(root).isDirectory()) {
1176
- throw new Error(tx(ORCHESTRATOR_TEXTS.runScanRootMissing, { root }));
1162
+ return candidatesByNew;
1163
+ }
1164
+ function claimSingletonRenames(opts) {
1165
+ const ops = [];
1166
+ for (const toPath of opts.newPaths) {
1167
+ if (opts.claimedNew.has(toPath)) continue;
1168
+ const candidates = opts.candidatesByNew.get(toPath);
1169
+ if (!candidates) continue;
1170
+ const remaining = candidates.filter((p) => !opts.claimedDeleted.has(p));
1171
+ if (remaining.length === 1) {
1172
+ const fromPath = remaining[0];
1173
+ ops.push({ from: fromPath, to: toPath, confidence: "medium" });
1174
+ opts.issues.push({
1175
+ analyzerId: "auto-rename-medium",
1176
+ severity: "warn",
1177
+ nodeIds: [toPath],
1178
+ message: `Auto-rename (medium confidence): ${fromPath} \u2192 ${toPath}`,
1179
+ data: { from: fromPath, to: toPath, confidence: "medium" }
1180
+ });
1181
+ opts.claimedDeleted.add(fromPath);
1182
+ opts.claimedNew.add(toPath);
1177
1183
  }
1178
1184
  }
1185
+ return ops;
1179
1186
  }
1180
- function indexPriorSnapshot(prior) {
1181
- const priorNodesByPath = /* @__PURE__ */ new Map();
1182
- const priorNodePaths = /* @__PURE__ */ new Set();
1183
- const priorLinksByOriginating = /* @__PURE__ */ new Map();
1184
- const priorFrontmatterIssuesByNode = /* @__PURE__ */ new Map();
1185
- if (!prior) {
1186
- return { priorNodesByPath, priorNodePaths, priorLinksByOriginating, priorFrontmatterIssuesByNode };
1187
+ function flagAmbiguousRenames(opts) {
1188
+ for (const toPath of opts.newPaths) {
1189
+ if (opts.claimedNew.has(toPath)) continue;
1190
+ const candidates = opts.candidatesByNew.get(toPath);
1191
+ if (!candidates) continue;
1192
+ const remaining = candidates.filter((p) => !opts.claimedDeleted.has(p));
1193
+ if (remaining.length > 1) {
1194
+ opts.issues.push({
1195
+ analyzerId: "auto-rename-ambiguous",
1196
+ severity: "warn",
1197
+ nodeIds: [toPath],
1198
+ message: `Auto-rename ambiguous: ${toPath} matches ${remaining.length} prior frontmatters \u2014 pick one with \`sm orphans undo-rename ${toPath} --from <old.path>\`.`,
1199
+ data: { to: toPath, candidates: remaining }
1200
+ });
1201
+ }
1187
1202
  }
1188
- for (const node of prior.nodes) {
1189
- priorNodesByPath.set(node.path, node);
1190
- priorNodePaths.add(node.path);
1203
+ }
1204
+ function flagOrphans(opts) {
1205
+ for (const fromPath of opts.deletedPaths) {
1206
+ if (opts.claimedDeleted.has(fromPath)) continue;
1207
+ opts.issues.push({
1208
+ analyzerId: "orphan",
1209
+ severity: "info",
1210
+ nodeIds: [fromPath],
1211
+ message: `Orphan history: ${fromPath} was deleted; no rename match found.`,
1212
+ data: { path: fromPath }
1213
+ });
1191
1214
  }
1192
- for (const link of prior.links) {
1193
- const key = originatingNodeOf(link, priorNodePaths);
1194
- const list = priorLinksByOriginating.get(key);
1195
- if (list) list.push(link);
1196
- else priorLinksByOriginating.set(key, [link]);
1215
+ }
1216
+ function detectRenamesAndOrphans(prior, current, issues) {
1217
+ const priorByPath = /* @__PURE__ */ new Map();
1218
+ for (const n of prior.nodes) priorByPath.set(n.path, n);
1219
+ const currentByPath = /* @__PURE__ */ new Map();
1220
+ for (const n of current) currentByPath.set(n.path, n);
1221
+ const deletedPaths = [...priorByPath.keys()].filter((p) => !currentByPath.has(p)).sort();
1222
+ const newPaths = [...currentByPath.keys()].filter((p) => !priorByPath.has(p)).sort();
1223
+ const claimedDeleted = /* @__PURE__ */ new Set();
1224
+ const claimedNew = /* @__PURE__ */ new Set();
1225
+ const ops = [];
1226
+ ops.push(...findHighConfidenceRenames({
1227
+ deletedPaths,
1228
+ newPaths,
1229
+ priorByPath,
1230
+ currentByPath,
1231
+ claimedDeleted,
1232
+ claimedNew
1233
+ }));
1234
+ const candidatesByNew = buildFrontmatterRenameCandidates({
1235
+ deletedPaths,
1236
+ newPaths,
1237
+ priorByPath,
1238
+ currentByPath,
1239
+ claimedDeleted,
1240
+ claimedNew
1241
+ });
1242
+ ops.push(...claimSingletonRenames({
1243
+ newPaths,
1244
+ candidatesByNew,
1245
+ claimedDeleted,
1246
+ claimedNew,
1247
+ issues
1248
+ }));
1249
+ flagAmbiguousRenames({ newPaths, candidatesByNew, claimedDeleted, claimedNew, issues });
1250
+ flagOrphans({ deletedPaths, claimedDeleted, issues });
1251
+ return ops;
1252
+ }
1253
+
1254
+ // kernel/scan/walk-content.ts
1255
+ import { readFile, readdir, stat } from "fs/promises";
1256
+ import { join as join2, relative as relative2, sep } from "path";
1257
+
1258
+ // kernel/scan/ignore.ts
1259
+ import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
1260
+ import { dirname as dirname2, resolve as resolve5 } from "path";
1261
+ import { fileURLToPath } from "url";
1262
+ import ignoreFactory from "ignore";
1263
+ function buildIgnoreFilter(opts = {}) {
1264
+ const ig = ignoreFactory();
1265
+ if (opts.includeDefaults !== false) {
1266
+ ig.add(loadDefaultsText());
1197
1267
  }
1198
- for (const issue of prior.issues) {
1199
- if (issue.analyzerId !== "frontmatter-invalid" && issue.analyzerId !== "frontmatter-malformed") continue;
1200
- if (issue.nodeIds.length !== 1) continue;
1201
- const path = issue.nodeIds[0];
1202
- const list = priorFrontmatterIssuesByNode.get(path);
1203
- if (list) list.push(issue);
1204
- else priorFrontmatterIssuesByNode.set(path, [issue]);
1268
+ if (opts.configIgnore && opts.configIgnore.length > 0) {
1269
+ ig.add(opts.configIgnore);
1205
1270
  }
1206
- return { priorNodesByPath, priorNodePaths, priorLinksByOriginating, priorFrontmatterIssuesByNode };
1207
- }
1208
- async function runExtractorsForNode(opts) {
1209
- const internalLinks = [];
1210
- const externalLinks = [];
1211
- const enrichmentBuffer = /* @__PURE__ */ new Map();
1212
- const contributions = [];
1213
- const validators = loadSchemaValidators();
1214
- for (const extractor of opts.extractors) {
1215
- const qualifiedId = qualifiedExtensionId(extractor.pluginId, extractor.id);
1216
- const emitLink = (link) => {
1217
- const validated = validateLink(extractor, link, opts.emitter);
1218
- if (!validated) return;
1219
- if (isExternalUrlLink(validated)) externalLinks.push(validated);
1220
- else internalLinks.push(validated);
1221
- };
1222
- const enrichNode = (partial) => {
1223
- const key = `${opts.node.path}\0${qualifiedId}`;
1224
- const existing = enrichmentBuffer.get(key);
1225
- if (existing) {
1226
- existing.value = { ...existing.value, ...partial };
1227
- existing.enrichedAt = Date.now();
1228
- } else {
1229
- enrichmentBuffer.set(key, {
1230
- nodePath: opts.node.path,
1231
- extractorId: qualifiedId,
1232
- bodyHashAtEnrichment: opts.bodyHash,
1233
- value: { ...partial },
1234
- enrichedAt: Date.now(),
1235
- // Extractors are deterministic-only; `is_probabilistic` is
1236
- // reserved on the row for future Action-issued enrichments.
1237
- isProbabilistic: false
1238
- });
1271
+ if (opts.ignoreFileText && opts.ignoreFileText.length > 0) {
1272
+ ig.add(opts.ignoreFileText);
1273
+ }
1274
+ return {
1275
+ ignores(relativePath) {
1276
+ if (relativePath === "" || relativePath === "." || relativePath === "./") {
1277
+ return false;
1239
1278
  }
1240
- };
1241
- const declaredContributions = readDeclaredContributions(extractor);
1242
- const emitContribution = (contributionId, payload) => {
1243
- const declared = declaredContributions.get(contributionId);
1244
- if (!declared) {
1245
- emitExtensionError(opts.emitter, qualifiedId, opts.node.path, {
1246
- phase: "emitContribution",
1247
- contributionId,
1248
- reason: "unknown-contribution-id",
1249
- message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {
1250
- extractorId: qualifiedId,
1251
- contributionId,
1252
- nodePath: opts.node.path
1253
- })
1254
- });
1255
- return;
1279
+ const normalised = relativePath.replace(/^\.\//, "").replace(/\\/g, "/").replace(/^\//, "");
1280
+ if (normalised === "") return false;
1281
+ return ig.ignores(normalised);
1282
+ }
1283
+ };
1284
+ }
1285
+ var cachedDefaults = null;
1286
+ function loadDefaultsText() {
1287
+ if (cachedDefaults !== null) return cachedDefaults;
1288
+ cachedDefaults = readDefaultsFromDisk();
1289
+ return cachedDefaults;
1290
+ }
1291
+ function readDefaultsFromDisk() {
1292
+ const here = dirname2(fileURLToPath(import.meta.url));
1293
+ const candidates = [
1294
+ resolve5(here, "../../config/defaults/skillmapignore"),
1295
+ // src/kernel/scan/ → src/config/defaults/
1296
+ resolve5(here, "../config/defaults/skillmapignore"),
1297
+ // dist/cli.js → dist/config/defaults/ (siblings)
1298
+ resolve5(here, "config/defaults/skillmapignore")
1299
+ ];
1300
+ for (const candidate of candidates) {
1301
+ if (existsSync2(candidate)) {
1302
+ try {
1303
+ return readFileSync4(candidate, "utf8");
1304
+ } catch {
1256
1305
  }
1257
- const result = validators.validateContributionPayload(declared.slot, payload);
1258
- if (!result.ok) {
1259
- emitExtensionError(opts.emitter, qualifiedId, opts.node.path, {
1260
- phase: "emitContribution",
1261
- contributionId,
1262
- slot: declared.slot,
1263
- reason: result.errors,
1264
- message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
1265
- extractorId: qualifiedId,
1266
- contributionId,
1267
- nodePath: opts.node.path,
1268
- slot: declared.slot,
1269
- errors: result.errors
1270
- })
1271
- });
1272
- return;
1306
+ }
1307
+ }
1308
+ return "";
1309
+ }
1310
+
1311
+ // built-in-plugins/parsers/frontmatter-yaml/index.ts
1312
+ import yaml from "js-yaml";
1313
+ var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
1314
+ var FORBIDDEN_FRONTMATTER_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1315
+ var frontmatterYamlParser = {
1316
+ id: "frontmatter-yaml",
1317
+ parse(raw, _path) {
1318
+ const match = FRONTMATTER_RE.exec(raw);
1319
+ if (!match) return { frontmatterRaw: "", frontmatter: {}, body: raw };
1320
+ const frontmatterRaw = match[1];
1321
+ const body = match[2];
1322
+ const parsed = {};
1323
+ try {
1324
+ const doc = yaml.load(frontmatterRaw, { schema: yaml.JSON_SCHEMA });
1325
+ if (doc && typeof doc === "object" && !Array.isArray(doc)) {
1326
+ for (const [k, v] of Object.entries(doc)) {
1327
+ if (FORBIDDEN_FRONTMATTER_KEYS.has(k)) continue;
1328
+ parsed[k] = v;
1329
+ }
1273
1330
  }
1274
- contributions.push({
1275
- pluginId: extractor.pluginId,
1276
- extensionId: extractor.id,
1277
- nodePath: opts.node.path,
1278
- contributionId,
1279
- slot: declared.slot,
1280
- payload,
1281
- emittedAt: Date.now()
1282
- });
1283
- };
1284
- const store = opts.pluginStores?.get(extractor.pluginId);
1285
- const ctx = buildExtractorContext(
1286
- extractor,
1287
- opts.node,
1288
- opts.body,
1289
- opts.frontmatter,
1290
- emitLink,
1291
- enrichNode,
1292
- emitContribution,
1293
- store
1294
- );
1295
- await extractor.extract(ctx);
1331
+ } catch {
1332
+ }
1333
+ return { frontmatterRaw, frontmatter: parsed, body };
1334
+ }
1335
+ };
1336
+
1337
+ // built-in-plugins/parsers/plain/index.ts
1338
+ var plainParser = {
1339
+ id: "plain",
1340
+ parse(raw, _path) {
1341
+ return { frontmatter: {}, frontmatterRaw: "", body: raw };
1296
1342
  }
1297
- return {
1298
- internalLinks,
1299
- externalLinks,
1300
- enrichments: Array.from(enrichmentBuffer.values()),
1301
- contributions
1302
- };
1343
+ };
1344
+
1345
+ // kernel/scan/parsers/index.ts
1346
+ var REGISTRY = /* @__PURE__ */ new Map([
1347
+ [frontmatterYamlParser.id, frontmatterYamlParser],
1348
+ [plainParser.id, plainParser]
1349
+ ]);
1350
+ var FROZEN_IDS = new Set(REGISTRY.keys());
1351
+ function getParser(id) {
1352
+ return REGISTRY.get(id);
1303
1353
  }
1304
- function readDeclaredContributions(extension) {
1305
- const out = /* @__PURE__ */ new Map();
1306
- const raw = extension.viewContributions;
1307
- if (typeof raw !== "object" || raw === null) return out;
1308
- for (const [id, value] of Object.entries(raw)) {
1309
- if (typeof value !== "object" || value === null) continue;
1310
- const slot = value.slot;
1311
- if (typeof slot !== "string") continue;
1312
- out.set(id, { slot });
1354
+
1355
+ // kernel/scan/walk-content.ts
1356
+ var UnknownParserError = class extends Error {
1357
+ constructor(parserId) {
1358
+ super(`Unknown parser id '${parserId}'. Built-in parsers: 'frontmatter-yaml', 'plain'.`);
1359
+ this.name = "UnknownParserError";
1313
1360
  }
1314
- return out;
1315
- }
1316
- function emitExtensionError(emitter, qualifiedId, nodePath, data) {
1317
- emitter.emit(
1318
- makeEvent("extension.error", {
1319
- kind: "contribution-rejected",
1320
- extensionId: qualifiedId,
1321
- nodePath,
1322
- ...data
1323
- })
1324
- );
1325
- }
1326
- function computeCacheDecision(opts) {
1327
- const applicableExtractors = opts.extractors.filter(
1328
- (ex) => ex.applicableKinds === void 0 || ex.applicableKinds.includes(opts.kind)
1329
- );
1330
- const applicableQualifiedIds = new Set(
1331
- applicableExtractors.map((ex) => qualifiedExtensionId(ex.pluginId, ex.id))
1332
- );
1333
- const cachedQualifiedIds = /* @__PURE__ */ new Set();
1334
- const missingExtractors = [];
1335
- if (opts.priorExtractorRuns === void 0) {
1336
- if (opts.nodeHashCacheEligible) {
1337
- for (const id of applicableQualifiedIds) cachedQualifiedIds.add(id);
1338
- } else {
1339
- for (const ex of applicableExtractors) missingExtractors.push(ex);
1340
- }
1341
- } else {
1342
- const priorRunsForNode = opts.priorExtractorRuns.get(opts.nodePath) ?? /* @__PURE__ */ new Map();
1343
- for (const ex of applicableExtractors) {
1344
- const qualified = qualifiedExtensionId(ex.pluginId, ex.id);
1345
- const prior = priorRunsForNode.get(qualified);
1346
- const bodyMatch = prior !== void 0 && prior.bodyHash === opts.bodyHash;
1347
- const sidecarOk = prior !== void 0 && prior.sidecarAnnotationsHash === opts.sidecarAnnotationsHash;
1348
- if (opts.nodeHashCacheEligible && bodyMatch && sidecarOk) {
1349
- cachedQualifiedIds.add(qualified);
1350
- } else {
1351
- missingExtractors.push(ex);
1361
+ };
1362
+ async function* walkContent(roots, options) {
1363
+ const parser = getParser(options.parser);
1364
+ if (!parser) throw new UnknownParserError(options.parser);
1365
+ const filter = options.ignoreFilter ?? buildIgnoreFilter();
1366
+ const extensions = options.extensions;
1367
+ for (const root of roots) {
1368
+ for await (const file of walkRoot(root, root, filter, extensions)) {
1369
+ const relPath = relative2(root, file).split(sep).join("/");
1370
+ let raw;
1371
+ try {
1372
+ raw = await readFile(file, "utf8");
1373
+ } catch {
1374
+ continue;
1352
1375
  }
1376
+ const parsed = parser.parse(raw, relPath);
1377
+ yield {
1378
+ path: relPath,
1379
+ body: parsed.body,
1380
+ frontmatterRaw: parsed.frontmatterRaw,
1381
+ frontmatter: parsed.frontmatter
1382
+ };
1353
1383
  }
1354
1384
  }
1355
- return {
1356
- applicableExtractors,
1357
- applicableQualifiedIds,
1358
- cachedQualifiedIds,
1359
- missingExtractors,
1360
- fullCacheHit: opts.nodeHashCacheEligible && missingExtractors.length === 0
1361
- };
1362
1385
  }
1363
- function cloneNodeAndReshapeLinks(opts) {
1364
- const node = { ...opts.priorNode, bytes: { ...opts.priorNode.bytes } };
1365
- if (opts.priorNode.tokens) node.tokens = { ...opts.priorNode.tokens };
1366
- const internalLinks = [];
1367
- const reusedLinks = opts.priorLinksByOriginating.get(opts.priorNode.path) ?? [];
1368
- for (const link of reusedLinks) {
1369
- const reshaped = reuseCachedLink(
1370
- link,
1371
- opts.shortIdToQualified,
1372
- opts.cachedQualifiedIds,
1373
- opts.applicableQualifiedIds
1374
- );
1375
- if (reshaped) internalLinks.push(reshaped);
1386
+ async function* walkRoot(root, current, filter, extensions) {
1387
+ let entries;
1388
+ try {
1389
+ entries = await readdir(current, { withFileTypes: true, encoding: "utf8" });
1390
+ } catch {
1391
+ return;
1376
1392
  }
1377
- const frontmatterIssues = [];
1378
- const reusedFm = opts.priorFrontmatterIssuesByNode.get(opts.priorNode.path) ?? [];
1379
- for (const issue of reusedFm) {
1380
- frontmatterIssues.push({ ...issue, severity: opts.strict ? "error" : "warn" });
1393
+ for (const entry of entries) {
1394
+ const name = entry.name;
1395
+ const full = join2(current, name);
1396
+ const rel = relative2(root, full).split(sep).join("/");
1397
+ if (filter.ignores(rel)) continue;
1398
+ if (entry.isSymbolicLink()) continue;
1399
+ if (entry.isDirectory()) {
1400
+ yield* walkRoot(root, full, filter, extensions);
1401
+ } else if (entry.isFile() && hasMatchingExtension(name, extensions)) {
1402
+ try {
1403
+ const s = await stat(full);
1404
+ if (s.isFile()) yield full;
1405
+ } catch {
1406
+ }
1407
+ }
1381
1408
  }
1382
- return { node, internalLinks, frontmatterIssues };
1383
1409
  }
1384
- function reusePriorNode(opts) {
1385
- const base = cloneNodeAndReshapeLinks(opts);
1386
- const ranAt = Date.now();
1387
- const extractorRuns = [];
1388
- for (const qualified of opts.cachedQualifiedIds) {
1389
- extractorRuns.push({
1390
- nodePath: opts.priorNode.path,
1391
- extractorId: qualified,
1392
- bodyHashAtRun: opts.bodyHash,
1393
- ranAt,
1394
- sidecarAnnotationsHashAtRun: opts.sidecarAnnotationsHash
1395
- });
1410
+ function hasMatchingExtension(name, extensions) {
1411
+ for (const ext of extensions) {
1412
+ if (name.endsWith(ext)) return true;
1396
1413
  }
1397
- return { ...base, extractorRuns };
1414
+ return false;
1398
1415
  }
1399
- function buildFreshNodeAndValidateFrontmatter(opts) {
1400
- const node = buildNode({
1401
- path: opts.raw.path,
1402
- kind: opts.kind,
1403
- providerId: opts.provider.id,
1404
- frontmatterRaw: opts.raw.frontmatterRaw,
1405
- body: opts.raw.body,
1406
- frontmatter: opts.raw.frontmatter,
1407
- bodyHash: opts.bodyHash,
1408
- frontmatterHash: opts.frontmatterHash,
1409
- encoder: opts.encoder
1410
- });
1411
- const frontmatterIssues = [];
1412
- if (opts.raw.frontmatterRaw.length > 0) {
1413
- const fmIssue = validateFrontmatter(
1414
- opts.providerFrontmatter,
1415
- opts.provider,
1416
- opts.kind,
1417
- opts.raw.frontmatter,
1418
- opts.raw.path,
1419
- opts.strict
1420
- );
1421
- if (fmIssue) frontmatterIssues.push(fmIssue);
1422
- } else {
1423
- const malformed = detectMalformedFrontmatter(opts.raw.body, opts.raw.path, opts.strict);
1424
- if (malformed) frontmatterIssues.push(malformed);
1416
+
1417
+ // kernel/extensions/provider.ts
1418
+ var DEFAULT_READ_CONFIG = Object.freeze({
1419
+ extensions: Object.freeze([".md"]),
1420
+ parser: "frontmatter-yaml"
1421
+ });
1422
+ function resolveProviderWalk(provider) {
1423
+ if (provider.walk) {
1424
+ const walk2 = provider.walk.bind(provider);
1425
+ return walk2;
1425
1426
  }
1426
- return { node, frontmatterIssues };
1427
+ const read = provider.read ?? DEFAULT_READ_CONFIG;
1428
+ return (roots, options) => {
1429
+ const walkOptions = {
1430
+ extensions: read.extensions,
1431
+ parser: read.parser
1432
+ };
1433
+ if (options?.ignoreFilter) walkOptions.ignoreFilter = options.ignoreFilter;
1434
+ return walkContent(roots, walkOptions);
1435
+ };
1427
1436
  }
1428
- async function walkAndExtract(opts) {
1429
- const {
1430
- providers,
1431
- extractors,
1432
- roots,
1433
- ignoreFilter,
1434
- emitter,
1435
- encoder,
1436
- strict,
1437
- enableCache,
1438
- prior,
1439
- priorIndex,
1440
- priorExtractorRuns,
1441
- providerFrontmatter,
1442
- pluginStores
1443
- } = opts;
1444
- const { priorNodesByPath, priorLinksByOriginating, priorFrontmatterIssuesByNode } = priorIndex;
1445
- const nodes = [];
1446
- const internalLinks = [];
1447
- const externalLinks = [];
1448
- const cachedPaths = /* @__PURE__ */ new Set();
1449
- const frontmatterIssues = [];
1450
- const enrichmentBuffer = /* @__PURE__ */ new Map();
1451
- const contributionsBuffer = [];
1452
- const freshlyRunTuples = /* @__PURE__ */ new Set();
1453
- const extractorRuns = [];
1454
- const sidecarRoots = /* @__PURE__ */ new Map();
1455
- let filesWalked = 0;
1456
- let index = 0;
1457
- const walkOptions = ignoreFilter ? { ignoreFilter } : {};
1458
- const shortIdToQualified = /* @__PURE__ */ new Map();
1459
- for (const ex of extractors) {
1460
- const qualified = qualifiedExtensionId(ex.pluginId, ex.id);
1461
- const list = shortIdToQualified.get(ex.id);
1462
- if (list) list.push(qualified);
1463
- else shortIdToQualified.set(ex.id, [qualified]);
1437
+
1438
+ // kernel/sidecar/parse.ts
1439
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
1440
+ import { dirname as dirname3, resolve as resolve6 } from "path";
1441
+ import { createRequire as createRequire3 } from "module";
1442
+ import { Ajv2020 as Ajv20204 } from "ajv/dist/2020.js";
1443
+ import yaml2 from "js-yaml";
1444
+ function readSidecarFor(mdAbsolutePath) {
1445
+ const sidecarPath = sidecarPathFor(mdAbsolutePath);
1446
+ if (!existsSync3(sidecarPath)) {
1447
+ return { parsed: null, present: false, issues: [] };
1464
1448
  }
1465
- const claimedPaths = /* @__PURE__ */ new Set();
1466
- for (const provider of providers) {
1467
- for await (const raw of resolveProviderWalk(provider)(roots, walkOptions)) {
1468
- filesWalked += 1;
1469
- if (claimedPaths.has(raw.path)) continue;
1470
- const bodyHash = sha256(raw.body);
1471
- const frontmatterHash = sha256(canonicalFrontmatter(raw.frontmatter, raw.frontmatterRaw));
1472
- const priorNode = priorNodesByPath.get(raw.path);
1473
- const nodeHashCacheEligible = enableCache && prior !== null && priorNode !== void 0 && priorNode.bodyHash === bodyHash && priorNode.frontmatterHash === frontmatterHash;
1474
- const kind = provider.classify(raw.path, raw.frontmatter);
1475
- if (kind === null) {
1476
- continue;
1477
- }
1478
- claimedPaths.add(raw.path);
1479
- index += 1;
1480
- const sidecarResolution = resolveSidecarOverlay(
1481
- raw.path,
1482
- raw.path,
1483
- roots,
1484
- bodyHash,
1485
- frontmatterHash
1486
- );
1487
- const sidecarAnnotationsHash = sha256(
1488
- canonicalSidecarAnnotations(sidecarResolution.overlay.annotations)
1489
- );
1490
- const cacheDecision = computeCacheDecision({
1491
- extractors,
1492
- kind,
1493
- nodePath: raw.path,
1494
- bodyHash,
1495
- sidecarAnnotationsHash,
1496
- nodeHashCacheEligible,
1497
- priorExtractorRuns
1498
- });
1499
- const {
1500
- applicableExtractors,
1501
- applicableQualifiedIds,
1502
- cachedQualifiedIds,
1503
- missingExtractors,
1504
- fullCacheHit
1505
- } = cacheDecision;
1506
- const attachSidecar = (node2) => {
1507
- node2.sidecar = sidecarResolution.overlay;
1508
- if (sidecarResolution.parsedRoot !== null) {
1509
- sidecarRoots.set(node2.path, sidecarResolution.parsedRoot);
1510
- }
1511
- return sidecarResolution.issues.map(
1512
- (i) => i.nodeIds.length > 0 ? i : { ...i, nodeIds: [node2.path] }
1513
- );
1514
- };
1515
- if (fullCacheHit && priorNode) {
1516
- const reused = reusePriorNode({
1517
- priorNode,
1518
- bodyHash,
1519
- sidecarAnnotationsHash,
1520
- strict,
1521
- cachedQualifiedIds,
1522
- applicableQualifiedIds,
1523
- shortIdToQualified,
1524
- priorLinksByOriginating,
1525
- priorFrontmatterIssuesByNode
1526
- });
1527
- const reusedSidecarIssues = attachSidecar(reused.node);
1528
- nodes.push(reused.node);
1529
- cachedPaths.add(reused.node.path);
1530
- for (const link of reused.internalLinks) internalLinks.push(link);
1531
- for (const issue of reused.frontmatterIssues) frontmatterIssues.push(issue);
1532
- for (const issue of reusedSidecarIssues) frontmatterIssues.push(issue);
1533
- for (const run of reused.extractorRuns) extractorRuns.push(run);
1534
- emitter.emit(makeEvent("scan.progress", { index, path: raw.path, kind, cached: true }));
1535
- continue;
1536
- }
1537
- let node;
1538
- const partialCacheHit = nodeHashCacheEligible && cachedQualifiedIds.size > 0 && priorNode !== void 0;
1539
- if (partialCacheHit && priorNode) {
1540
- const partial = cloneNodeAndReshapeLinks({
1541
- priorNode,
1542
- strict,
1543
- cachedQualifiedIds,
1544
- applicableQualifiedIds,
1545
- shortIdToQualified,
1546
- priorLinksByOriginating,
1547
- priorFrontmatterIssuesByNode
1548
- });
1549
- node = partial.node;
1550
- for (const link of partial.internalLinks) internalLinks.push(link);
1551
- for (const issue of partial.frontmatterIssues) frontmatterIssues.push(issue);
1552
- nodes.push(node);
1553
- } else {
1554
- const fresh = buildFreshNodeAndValidateFrontmatter({
1555
- raw,
1556
- kind,
1557
- provider,
1558
- bodyHash,
1559
- frontmatterHash,
1560
- encoder,
1561
- providerFrontmatter,
1562
- strict
1563
- });
1564
- node = fresh.node;
1565
- nodes.push(node);
1566
- for (const issue of fresh.frontmatterIssues) frontmatterIssues.push(issue);
1567
- }
1568
- const sidecarIssues = attachSidecar(node);
1569
- for (const issue of sidecarIssues) frontmatterIssues.push(issue);
1570
- emitter.emit(makeEvent("scan.progress", {
1571
- index,
1572
- path: raw.path,
1573
- kind,
1574
- cached: false,
1575
- ...partialCacheHit ? { partialCache: true } : {}
1576
- }));
1577
- const extractorsToRun = partialCacheHit ? missingExtractors : applicableExtractors;
1578
- for (const ex of extractorsToRun) {
1579
- freshlyRunTuples.add(`${ex.pluginId}\0${ex.id}\0${node.path}`);
1580
- }
1581
- const extractResult = await runExtractorsForNode({
1582
- extractors: extractorsToRun,
1583
- node,
1584
- body: raw.body,
1585
- frontmatter: raw.frontmatter,
1586
- bodyHash,
1587
- emitter,
1588
- ...pluginStores ? { pluginStores } : {}
1589
- });
1590
- for (const link of extractResult.internalLinks) internalLinks.push(link);
1591
- for (const link of extractResult.externalLinks) externalLinks.push(link);
1592
- for (const enr of extractResult.enrichments) {
1593
- enrichmentBuffer.set(`${enr.nodePath}\0${enr.extractorId}`, enr);
1594
- }
1595
- for (const c of extractResult.contributions) contributionsBuffer.push(c);
1596
- const ranAt = Date.now();
1597
- for (const ex of applicableExtractors) {
1598
- const qualified = qualifiedExtensionId(ex.pluginId, ex.id);
1599
- extractorRuns.push({
1600
- nodePath: node.path,
1601
- extractorId: qualified,
1602
- bodyHashAtRun: bodyHash,
1603
- ranAt,
1604
- sidecarAnnotationsHashAtRun: sidecarAnnotationsHash
1605
- });
1606
- }
1607
- }
1449
+ let raw;
1450
+ try {
1451
+ raw = readFileSync5(sidecarPath, "utf8");
1452
+ } catch (err) {
1453
+ return {
1454
+ parsed: null,
1455
+ present: true,
1456
+ issues: [{ message: `cannot read ${sidecarPath}: ${err.message}` }]
1457
+ };
1458
+ }
1459
+ let parsedYaml;
1460
+ try {
1461
+ parsedYaml = yaml2.load(raw);
1462
+ } catch (err) {
1463
+ return {
1464
+ parsed: null,
1465
+ present: true,
1466
+ issues: [{ message: `malformed YAML in ${sidecarPath}: ${err.message}` }]
1467
+ };
1468
+ }
1469
+ if (!isPlainObject(parsedYaml)) {
1470
+ return {
1471
+ parsed: null,
1472
+ present: true,
1473
+ issues: [{ message: `sidecar root must be a YAML mapping at ${sidecarPath}` }]
1474
+ };
1475
+ }
1476
+ const sidecarValidator = getSidecarValidator();
1477
+ if (!sidecarValidator(parsedYaml)) {
1478
+ const errors = (sidecarValidator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
1479
+ return {
1480
+ parsed: null,
1481
+ present: true,
1482
+ issues: [{ message: `sidecar schema validation failed at ${sidecarPath}: ${errors}` }]
1483
+ };
1608
1484
  }
1609
- const orphanSidecars = discoverOrphanSidecars(roots);
1485
+ const root = parsedYaml;
1486
+ const identityBlock = root["identity"];
1487
+ const annotationsRaw = root["annotations"];
1488
+ const annotations = isPlainObject(annotationsRaw) ? Object.keys(annotationsRaw).length === 0 ? null : annotationsRaw : null;
1610
1489
  return {
1611
- nodes,
1612
- internalLinks,
1613
- externalLinks,
1614
- cachedPaths,
1615
- frontmatterIssues,
1616
- filesWalked,
1617
- enrichments: [...enrichmentBuffer.values()],
1618
- extractorRuns,
1619
- contributions: contributionsBuffer,
1620
- freshlyRunTuples,
1621
- orphanSidecars,
1622
- sidecarRoots
1490
+ parsed: {
1491
+ filePath: sidecarPath,
1492
+ identityBodyHash: String(identityBlock["bodyHash"]),
1493
+ identityFrontmatterHash: String(identityBlock["frontmatterHash"]),
1494
+ identityPath: String(identityBlock["path"]),
1495
+ annotations,
1496
+ raw: root
1497
+ },
1498
+ present: true,
1499
+ issues: []
1623
1500
  };
1624
1501
  }
1625
- function reuseCachedLink(link, shortIdToQualified, cachedQualifiedIds, applicableQualifiedIds) {
1626
- if (!Array.isArray(link.sources) || link.sources.length === 0) return null;
1627
- const cachedSources = [];
1628
- const obsoleteSources = [];
1629
- let hasMissing = false;
1630
- for (const source of link.sources) {
1631
- const candidates = shortIdToQualified.get(source);
1632
- if (!candidates || candidates.length === 0) {
1633
- obsoleteSources.push(source);
1634
- continue;
1635
- }
1636
- if (candidates.some((q) => cachedQualifiedIds.has(q))) {
1637
- cachedSources.push(source);
1638
- continue;
1639
- }
1640
- if (candidates.some((q) => applicableQualifiedIds.has(q))) {
1641
- hasMissing = true;
1642
- continue;
1643
- }
1644
- obsoleteSources.push(source);
1502
+ function sidecarPathFor(mdAbsolutePath) {
1503
+ if (mdAbsolutePath.endsWith(".md")) {
1504
+ return `${mdAbsolutePath.slice(0, -".md".length)}.sm`;
1645
1505
  }
1646
- if (hasMissing) return null;
1647
- if (cachedSources.length === 0) return null;
1648
- if (obsoleteSources.length === 0) return link;
1649
- return { ...link, sources: cachedSources };
1506
+ return `${mdAbsolutePath}.sm`;
1650
1507
  }
1651
- async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, emitter, hookDispatcher) {
1652
- const issues = [];
1653
- const contributions = [];
1654
- const validators = loadSchemaValidators();
1655
- const analyzerOrphans = orphanSidecars.map((o) => ({
1656
- relativePath: o.relativePath,
1657
- expectedMdPath: o.expectedMdPath
1658
- }));
1659
- for (const analyzer of analyzers) {
1660
- const qualifiedId = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
1661
- const declaredContributions = readDeclaredContributions(analyzer);
1662
- const emitContribution = (nodePath, contributionId, payload) => {
1663
- const declared = declaredContributions.get(contributionId);
1664
- if (!declared) {
1665
- emitExtensionError(emitter, qualifiedId, nodePath, {
1666
- phase: "emitContribution",
1667
- contributionId,
1668
- reason: "unknown-contribution-id",
1669
- message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {
1670
- extractorId: qualifiedId,
1671
- contributionId,
1672
- nodePath
1673
- })
1674
- });
1675
- return;
1676
- }
1677
- const result = validators.validateContributionPayload(declared.slot, payload);
1678
- if (!result.ok) {
1679
- emitExtensionError(emitter, qualifiedId, nodePath, {
1680
- phase: "emitContribution",
1681
- contributionId,
1682
- slot: declared.slot,
1683
- reason: result.errors,
1684
- message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
1685
- extractorId: qualifiedId,
1686
- contributionId,
1687
- nodePath,
1688
- slot: declared.slot,
1689
- errors: result.errors
1690
- })
1691
- });
1692
- return;
1693
- }
1694
- contributions.push({
1695
- pluginId: analyzer.pluginId,
1696
- extensionId: analyzer.id,
1697
- nodePath,
1698
- contributionId,
1699
- slot: declared.slot,
1700
- payload,
1701
- emittedAt: Date.now()
1702
- });
1703
- };
1704
- const emitted = await analyzer.evaluate({
1705
- nodes,
1706
- links: internalLinks,
1707
- orphanSidecars: analyzerOrphans,
1708
- sidecarRoots,
1709
- annotationContributions,
1710
- viewContributions,
1711
- orphanJobFiles,
1712
- ...referenceablePaths ? { referenceablePaths } : {},
1713
- ...cwd ? { cwd } : {},
1714
- emitContribution
1715
- });
1716
- for (const issue of emitted) {
1717
- const validated = validateIssue(analyzer, issue, emitter);
1718
- if (validated) issues.push(validated);
1719
- }
1720
- const evt = makeEvent("analyzer.completed", { analyzerId: qualifiedId });
1721
- emitter.emit(evt);
1722
- await hookDispatcher.dispatch("analyzer.completed", evt);
1508
+ function isPlainObject(value) {
1509
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1510
+ }
1511
+ var cachedSidecarValidator = null;
1512
+ function getSidecarValidator() {
1513
+ if (cachedSidecarValidator) return cachedSidecarValidator;
1514
+ const ajv = new Ajv20204({ strict: false, allErrors: true, allowUnionTypes: true });
1515
+ applyAjvFormats(ajv);
1516
+ const specRoot = resolveSpecRoot2();
1517
+ const annotationsSchema = JSON.parse(
1518
+ readFileSync5(resolve6(specRoot, "schemas/annotations.schema.json"), "utf8")
1519
+ );
1520
+ const sidecarSchema = JSON.parse(
1521
+ readFileSync5(resolve6(specRoot, "schemas/sidecar.schema.json"), "utf8")
1522
+ );
1523
+ ajv.addSchema(annotationsSchema);
1524
+ cachedSidecarValidator = ajv.compile(sidecarSchema);
1525
+ return cachedSidecarValidator;
1526
+ }
1527
+ function resolveSpecRoot2() {
1528
+ const require2 = createRequire3(import.meta.url);
1529
+ try {
1530
+ const indexPath = require2.resolve("@skill-map/spec/index.json");
1531
+ return dirname3(indexPath);
1532
+ } catch {
1533
+ throw new Error(
1534
+ "@skill-map/spec not resolvable \u2014 sidecar reader cannot load schemas."
1535
+ );
1723
1536
  }
1724
- return { issues, contributions };
1725
1537
  }
1726
- function originatingNodeOf(link, priorNodePaths) {
1727
- if (link.kind === "supersedes" && !priorNodePaths.has(link.source)) {
1728
- return link.target;
1538
+
1539
+ // kernel/sidecar/drift.ts
1540
+ function computeDriftStatus(args) {
1541
+ const bodyDrift = args.storedBodyHash !== args.liveBodyHash;
1542
+ const fmDrift = args.storedFrontmatterHash !== args.liveFrontmatterHash;
1543
+ if (bodyDrift && fmDrift) return "stale-both";
1544
+ if (bodyDrift) return "stale-body";
1545
+ if (fmDrift) return "stale-frontmatter";
1546
+ return "fresh";
1547
+ }
1548
+
1549
+ // kernel/sidecar/discover-orphans.ts
1550
+ import { existsSync as existsSync4, readdirSync as readdirSync2, statSync } from "fs";
1551
+ import { join as join3, relative as relative3, sep as sep2 } from "path";
1552
+ function discoverOrphanSidecars(roots, shouldSkip) {
1553
+ const out = [];
1554
+ for (const root of roots) {
1555
+ walk(root, root, shouldSkip ?? (() => false), out);
1729
1556
  }
1730
- return link.source;
1557
+ return out;
1731
1558
  }
1732
- function findHighConfidenceRenames(opts) {
1733
- const ops = [];
1734
- for (const fromPath of opts.deletedPaths) {
1735
- if (opts.claimedDeleted.has(fromPath)) continue;
1736
- const fromNode = opts.priorByPath.get(fromPath);
1737
- for (const toPath of opts.newPaths) {
1738
- if (opts.claimedNew.has(toPath)) continue;
1739
- const toNode = opts.currentByPath.get(toPath);
1740
- if (toNode.bodyHash === fromNode.bodyHash) {
1741
- ops.push({ from: fromPath, to: toPath, confidence: "high" });
1742
- opts.claimedDeleted.add(fromPath);
1743
- opts.claimedNew.add(toPath);
1744
- break;
1745
- }
1559
+ function walk(root, current, shouldSkip, out) {
1560
+ let entries;
1561
+ try {
1562
+ entries = readdirSync2(current, { withFileTypes: true, encoding: "utf8" });
1563
+ } catch {
1564
+ return;
1565
+ }
1566
+ for (const entry of entries) {
1567
+ const full = join3(current, entry.name);
1568
+ const rel = relative3(root, full).split(sep2).join("/");
1569
+ if (shouldSkip(rel)) continue;
1570
+ if (entry.isSymbolicLink()) continue;
1571
+ if (entry.isDirectory()) {
1572
+ walk(root, full, shouldSkip, out);
1573
+ continue;
1746
1574
  }
1575
+ if (!entry.isFile()) continue;
1576
+ if (!entry.name.endsWith(".sm")) continue;
1577
+ const expectedMd = `${full.slice(0, -".sm".length)}.md`;
1578
+ if (existsSync4(expectedMd) && safeIsFile(expectedMd)) continue;
1579
+ out.push({ sidecarPath: full, relativePath: rel, expectedMdPath: expectedMd });
1747
1580
  }
1748
- return ops;
1749
1581
  }
1750
- function buildFrontmatterRenameCandidates(opts) {
1751
- const candidatesByNew = /* @__PURE__ */ new Map();
1752
- for (const toPath of opts.newPaths) {
1753
- if (opts.claimedNew.has(toPath)) continue;
1754
- const toNode = opts.currentByPath.get(toPath);
1755
- const matches = [];
1756
- for (const fromPath of opts.deletedPaths) {
1757
- if (opts.claimedDeleted.has(fromPath)) continue;
1758
- const fromNode = opts.priorByPath.get(fromPath);
1759
- if (toNode.frontmatterHash === fromNode.frontmatterHash) {
1760
- matches.push(fromPath);
1761
- }
1762
- }
1763
- if (matches.length > 0) candidatesByNew.set(toPath, matches);
1582
+ function safeIsFile(path) {
1583
+ try {
1584
+ return statSync(path).isFile();
1585
+ } catch {
1586
+ return false;
1764
1587
  }
1765
- return candidatesByNew;
1766
1588
  }
1767
- function claimSingletonRenames(opts) {
1768
- const ops = [];
1769
- for (const toPath of opts.newPaths) {
1770
- if (opts.claimedNew.has(toPath)) continue;
1771
- const candidates = opts.candidatesByNew.get(toPath);
1772
- if (!candidates) continue;
1773
- const remaining = candidates.filter((p) => !opts.claimedDeleted.has(p));
1774
- if (remaining.length === 1) {
1775
- const fromPath = remaining[0];
1776
- ops.push({ from: fromPath, to: toPath, confidence: "medium" });
1777
- opts.issues.push({
1778
- analyzerId: "auto-rename-medium",
1779
- severity: "warn",
1780
- nodeIds: [toPath],
1781
- message: `Auto-rename (medium confidence): ${fromPath} \u2192 ${toPath}`,
1782
- data: { from: fromPath, to: toPath, confidence: "medium" }
1783
- });
1784
- opts.claimedDeleted.add(fromPath);
1785
- opts.claimedNew.add(toPath);
1589
+
1590
+ // kernel/sidecar/store.ts
1591
+ import { existsSync as existsSync7, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
1592
+ import { dirname as dirname5, resolve as resolve9 } from "path";
1593
+ import { createRequire as createRequire4 } from "module";
1594
+ import { Ajv2020 as Ajv20205 } from "ajv/dist/2020.js";
1595
+ import yaml3 from "js-yaml";
1596
+
1597
+ // core/config/helper.ts
1598
+ import { isAbsolute as isAbsolute2, resolve as resolve8 } from "path";
1599
+
1600
+ // kernel/config/loader.ts
1601
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
1602
+
1603
+ // kernel/util/skill-map-paths.ts
1604
+ import { join as join5 } from "path";
1605
+
1606
+ // core/paths/db-path.ts
1607
+ import { join as join4, resolve as resolve7 } from "path";
1608
+ var SKILL_MAP_DIR = ".skill-map";
1609
+ var DB_FILENAME = "skill-map.db";
1610
+ var LOCAL_SETTINGS_FILENAME = "settings.local.json";
1611
+ var DEFAULT_DB_REL = `${SKILL_MAP_DIR}/${DB_FILENAME}`;
1612
+ var GITIGNORE_ENTRIES = [
1613
+ `${SKILL_MAP_DIR}/${LOCAL_SETTINGS_FILENAME}`,
1614
+ `${SKILL_MAP_DIR}/${DB_FILENAME}`
1615
+ ];
1616
+
1617
+ // core/config/atomic-write.ts
1618
+ import {
1619
+ existsSync as existsSync6,
1620
+ mkdirSync,
1621
+ readFileSync as readFileSync7,
1622
+ renameSync,
1623
+ unlinkSync,
1624
+ writeFileSync
1625
+ } from "fs";
1626
+ import { dirname as dirname4 } from "path";
1627
+
1628
+ // kernel/orchestrator/node-build.ts
1629
+ import { createHash } from "crypto";
1630
+ import { existsSync as existsSync8 } from "fs";
1631
+ import { isAbsolute as isAbsolute3, resolve as resolvePath } from "path";
1632
+ import "js-tiktoken/lite";
1633
+ import yaml4 from "js-yaml";
1634
+
1635
+ // kernel/orchestrator/frontmatter.ts
1636
+ function validateFrontmatter(providerFrontmatter, provider, kind, frontmatter, path, strict) {
1637
+ const result = providerFrontmatter.validate(provider, kind, frontmatter);
1638
+ if (result.ok) return null;
1639
+ return {
1640
+ analyzerId: "frontmatter-invalid",
1641
+ severity: strict ? "error" : "warn",
1642
+ nodeIds: [path],
1643
+ message: tx(ORCHESTRATOR_TEXTS.frontmatterInvalid, { path, kind, errors: result.errors }),
1644
+ data: { kind, errors: result.errors }
1645
+ };
1646
+ }
1647
+ function detectMalformedFrontmatter(body, path, strict) {
1648
+ const hint = classifyMalformedFrontmatter(body);
1649
+ if (!hint) return null;
1650
+ return {
1651
+ analyzerId: "frontmatter-malformed",
1652
+ severity: strict ? "error" : "warn",
1653
+ nodeIds: [path],
1654
+ message: malformedMessage(hint, path),
1655
+ data: { hint }
1656
+ };
1657
+ }
1658
+ function classifyMalformedFrontmatter(body) {
1659
+ if (body.startsWith("\uFEFF")) {
1660
+ if (/^---\r?\n[\s\S]*?[A-Za-z0-9_-]+\s*:/.test(body)) {
1661
+ return "byte-order-mark";
1786
1662
  }
1787
1663
  }
1788
- return ops;
1789
- }
1790
- function flagAmbiguousRenames(opts) {
1791
- for (const toPath of opts.newPaths) {
1792
- if (opts.claimedNew.has(toPath)) continue;
1793
- const candidates = opts.candidatesByNew.get(toPath);
1794
- if (!candidates) continue;
1795
- const remaining = candidates.filter((p) => !opts.claimedDeleted.has(p));
1796
- if (remaining.length > 1) {
1797
- opts.issues.push({
1798
- analyzerId: "auto-rename-ambiguous",
1799
- severity: "warn",
1800
- nodeIds: [toPath],
1801
- message: `Auto-rename ambiguous: ${toPath} matches ${remaining.length} prior frontmatters \u2014 pick one with \`sm orphans undo-rename ${toPath} --from <old.path>\`.`,
1802
- data: { to: toPath, candidates: remaining }
1803
- });
1664
+ if (/^[ \t]+---\r?\n[ \t]*[A-Za-z0-9_-]+\s*:/.test(body)) {
1665
+ return "paste-with-indent";
1666
+ }
1667
+ if (/^---\r?\n[ \t]*[A-Za-z0-9_-]+\s*:/.test(body)) {
1668
+ const hasCloseFence = /\r?\n---(?:\r?\n|$)/.test(body);
1669
+ if (!hasCloseFence) {
1670
+ return "missing-close";
1804
1671
  }
1805
1672
  }
1673
+ return null;
1806
1674
  }
1807
- function flagOrphans(opts) {
1808
- for (const fromPath of opts.deletedPaths) {
1809
- if (opts.claimedDeleted.has(fromPath)) continue;
1810
- opts.issues.push({
1811
- analyzerId: "orphan",
1812
- severity: "info",
1813
- nodeIds: [fromPath],
1814
- message: `Orphan history: ${fromPath} was deleted; no rename match found.`,
1815
- data: { path: fromPath }
1816
- });
1675
+ function malformedMessage(hint, path) {
1676
+ switch (hint) {
1677
+ case "paste-with-indent":
1678
+ return tx(ORCHESTRATOR_TEXTS.frontmatterMalformedPasteWithIndent, { path });
1679
+ case "byte-order-mark":
1680
+ return tx(ORCHESTRATOR_TEXTS.frontmatterMalformedByteOrderMark, { path });
1681
+ case "missing-close":
1682
+ return tx(ORCHESTRATOR_TEXTS.frontmatterMalformedMissingClose, { path });
1817
1683
  }
1818
1684
  }
1819
- function detectRenamesAndOrphans(prior, current, issues) {
1820
- const priorByPath = /* @__PURE__ */ new Map();
1821
- for (const n of prior.nodes) priorByPath.set(n.path, n);
1822
- const currentByPath = /* @__PURE__ */ new Map();
1823
- for (const n of current) currentByPath.set(n.path, n);
1824
- const deletedPaths = [...priorByPath.keys()].filter((p) => !currentByPath.has(p)).sort();
1825
- const newPaths = [...currentByPath.keys()].filter((p) => !priorByPath.has(p)).sort();
1826
- const claimedDeleted = /* @__PURE__ */ new Set();
1827
- const claimedNew = /* @__PURE__ */ new Set();
1828
- const ops = [];
1829
- ops.push(...findHighConfidenceRenames({
1830
- deletedPaths,
1831
- newPaths,
1832
- priorByPath,
1833
- currentByPath,
1834
- claimedDeleted,
1835
- claimedNew
1836
- }));
1837
- const candidatesByNew = buildFrontmatterRenameCandidates({
1838
- deletedPaths,
1839
- newPaths,
1840
- priorByPath,
1841
- currentByPath,
1842
- claimedDeleted,
1843
- claimedNew
1844
- });
1845
- ops.push(...claimSingletonRenames({
1846
- newPaths,
1847
- candidatesByNew,
1848
- claimedDeleted,
1849
- claimedNew,
1850
- issues
1851
- }));
1852
- flagAmbiguousRenames({ newPaths, candidatesByNew, claimedDeleted, claimedNew, issues });
1853
- flagOrphans({ deletedPaths, claimedDeleted, issues });
1854
- return ops;
1855
- }
1856
- var EXTERNAL_URL_SCHEME_RE = /^[a-z][a-z0-9+\-.]+:/i;
1857
- function isExternalUrlLink(link) {
1858
- return EXTERNAL_URL_SCHEME_RE.test(link.target);
1859
- }
1685
+
1686
+ // kernel/orchestrator/node-build.ts
1860
1687
  function buildNode(args) {
1861
1688
  const bytesFrontmatter = Buffer.byteLength(args.frontmatterRaw, "utf8");
1862
1689
  const bytesBody = Buffer.byteLength(args.body, "utf8");
@@ -1943,171 +1770,72 @@ function resolveSidecarOverlay(relativePath, nodePathForIssue, roots, liveBodyHa
1943
1770
  storedBodyHash: result.parsed.identityBodyHash,
1944
1771
  storedFrontmatterHash: result.parsed.identityFrontmatterHash,
1945
1772
  liveBodyHash,
1946
- liveFrontmatterHash
1947
- });
1948
- return {
1949
- // R15 closure (2026-05-07) — surface the full parsed root on the
1950
- // overlay so BFF consumers (UI inspector audit / plugin-contributions
1951
- // / debug panels) can read `for.*`, `audit.*`, `settings.*`, and
1952
- // plugin-namespaced sub-keys without re-reading the file. The
1953
- // `annotations` field above stays — it duplicates `root.annotations`
1954
- // by design so existing consumers keep working unchanged.
1955
- overlay: {
1956
- present: true,
1957
- status,
1958
- annotations: result.parsed.annotations,
1959
- root: result.parsed.raw
1960
- },
1961
- issues,
1962
- parsedRoot: result.parsed.raw
1963
- };
1964
- }
1965
- function resolveAbsoluteMdPath(relativePath, roots) {
1966
- if (isAbsolute3(relativePath)) {
1967
- return existsSync8(relativePath) ? relativePath : null;
1968
- }
1969
- for (const root of roots) {
1970
- const candidate = resolvePath(root, relativePath);
1971
- if (existsSync8(candidate)) return candidate;
1972
- }
1973
- return null;
1974
- }
1975
- function relativePathFromRoots(absolutePath, roots) {
1976
- for (const root of roots) {
1977
- const abs = resolvePath(root);
1978
- if (absolutePath.startsWith(`${abs}/`) || absolutePath.startsWith(`${abs}\\`)) {
1979
- return absolutePath.slice(abs.length + 1).split(/[\\/]/).join("/");
1980
- }
1981
- }
1982
- return absolutePath;
1983
- }
1984
- function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, emitContribution, store) {
1985
- const scope = extractor.scope;
1986
- return {
1987
- node,
1988
- body: scope === "frontmatter" ? "" : body,
1989
- frontmatter: scope === "body" ? {} : frontmatter,
1990
- emitLink,
1991
- enrichNode,
1992
- emitContribution,
1993
- ...store !== void 0 ? { store } : {}
1994
- };
1995
- }
1996
- function validateLink(extractor, link, emitter) {
1997
- if (!extractor.emitsLinkKinds.includes(link.kind)) {
1998
- const qualifiedId = `${extractor.pluginId}/${extractor.id}`;
1999
- emitter.emit(
2000
- makeEvent("extension.error", {
2001
- kind: "link-kind-not-declared",
2002
- extensionId: qualifiedId,
2003
- linkKind: link.kind,
2004
- declaredKinds: extractor.emitsLinkKinds,
2005
- link: { source: link.source, target: link.target, kind: link.kind },
2006
- message: tx(ORCHESTRATOR_TEXTS.extensionErrorLinkKindNotDeclared, {
2007
- extractorId: qualifiedId,
2008
- linkKind: link.kind,
2009
- declaredKinds: extractor.emitsLinkKinds.join(", ")
2010
- })
2011
- })
2012
- );
2013
- return null;
2014
- }
2015
- const confidence = link.confidence ?? extractor.defaultConfidence;
2016
- return { ...link, confidence };
2017
- }
2018
- function validateFrontmatter(providerFrontmatter, provider, kind, frontmatter, path, strict) {
2019
- const result = providerFrontmatter.validate(provider, kind, frontmatter);
2020
- if (result.ok) return null;
2021
- return {
2022
- analyzerId: "frontmatter-invalid",
2023
- severity: strict ? "error" : "warn",
2024
- nodeIds: [path],
2025
- message: tx(ORCHESTRATOR_TEXTS.frontmatterInvalid, { path, kind, errors: result.errors }),
2026
- data: { kind, errors: result.errors }
2027
- };
2028
- }
2029
- function detectMalformedFrontmatter(body, path, strict) {
2030
- const hint = classifyMalformedFrontmatter(body);
2031
- if (!hint) return null;
2032
- return {
2033
- analyzerId: "frontmatter-malformed",
2034
- severity: strict ? "error" : "warn",
2035
- nodeIds: [path],
2036
- message: malformedMessage(hint, path),
2037
- data: { hint }
2038
- };
2039
- }
2040
- function classifyMalformedFrontmatter(body) {
2041
- if (body.startsWith("\uFEFF")) {
2042
- if (/^---\r?\n[\s\S]*?[A-Za-z0-9_-]+\s*:/.test(body)) {
2043
- return "byte-order-mark";
2044
- }
2045
- }
2046
- if (/^[ \t]+---\r?\n[ \t]*[A-Za-z0-9_-]+\s*:/.test(body)) {
2047
- return "paste-with-indent";
2048
- }
2049
- if (/^---\r?\n[ \t]*[A-Za-z0-9_-]+\s*:/.test(body)) {
2050
- const hasCloseFence = /\r?\n---(?:\r?\n|$)/.test(body);
2051
- if (!hasCloseFence) {
2052
- return "missing-close";
2053
- }
2054
- }
2055
- return null;
2056
- }
2057
- function malformedMessage(hint, path) {
2058
- switch (hint) {
2059
- case "paste-with-indent":
2060
- return tx(ORCHESTRATOR_TEXTS.frontmatterMalformedPasteWithIndent, { path });
2061
- case "byte-order-mark":
2062
- return tx(ORCHESTRATOR_TEXTS.frontmatterMalformedByteOrderMark, { path });
2063
- case "missing-close":
2064
- return tx(ORCHESTRATOR_TEXTS.frontmatterMalformedMissingClose, { path });
2065
- }
2066
- }
2067
- function validateIssue(analyzer, issue, emitter) {
2068
- const severity = issue.severity;
2069
- if (severity !== "error" && severity !== "warn" && severity !== "info") {
2070
- const qualifiedId = `${analyzer.pluginId}/${analyzer.id}`;
2071
- emitter.emit(
2072
- makeEvent("extension.error", {
2073
- kind: "issue-invalid-severity",
2074
- extensionId: qualifiedId,
2075
- severity,
2076
- issue: { analyzerId: issue.analyzerId || analyzer.id, message: issue.message, nodeIds: issue.nodeIds },
2077
- message: tx(ORCHESTRATOR_TEXTS.extensionErrorIssueInvalidSeverity, {
2078
- analyzerId: qualifiedId,
2079
- severity: JSON.stringify(severity)
2080
- })
2081
- })
2082
- );
2083
- return null;
2084
- }
2085
- return { ...issue, analyzerId: issue.analyzerId || analyzer.id };
1773
+ liveFrontmatterHash
1774
+ });
1775
+ return {
1776
+ // R15 closure (2026-05-07) — surface the full parsed root on the
1777
+ // overlay so BFF consumers (UI inspector audit / plugin-contributions
1778
+ // / debug panels) can read `for.*`, `audit.*`, `settings.*`, and
1779
+ // plugin-namespaced sub-keys without re-reading the file. The
1780
+ // `annotations` field above stays — it duplicates `root.annotations`
1781
+ // by design so existing consumers keep working unchanged.
1782
+ overlay: {
1783
+ present: true,
1784
+ status,
1785
+ annotations: result.parsed.annotations,
1786
+ root: result.parsed.raw
1787
+ },
1788
+ issues,
1789
+ parsedRoot: result.parsed.raw
1790
+ };
2086
1791
  }
2087
- function recomputeLinkCounts(nodes, links) {
2088
- const byPath2 = /* @__PURE__ */ new Map();
2089
- for (const node of nodes) {
2090
- node.linksOutCount = 0;
2091
- node.linksInCount = 0;
2092
- byPath2.set(node.path, node);
1792
+ function resolveAbsoluteMdPath(relativePath, roots) {
1793
+ if (isAbsolute3(relativePath)) {
1794
+ return existsSync8(relativePath) ? relativePath : null;
2093
1795
  }
2094
- for (const link of links) {
2095
- const source = byPath2.get(link.source);
2096
- if (source) source.linksOutCount += 1;
2097
- const target = byPath2.get(link.target);
2098
- if (target) target.linksInCount += 1;
1796
+ for (const root of roots) {
1797
+ const candidate = resolvePath(root, relativePath);
1798
+ if (existsSync8(candidate)) return candidate;
2099
1799
  }
1800
+ return null;
2100
1801
  }
2101
- function recomputeExternalRefsCount(nodes, externalLinks, cachedPaths) {
2102
- const byPath2 = /* @__PURE__ */ new Map();
2103
- for (const node of nodes) {
2104
- if (!cachedPaths.has(node.path)) node.externalRefsCount = 0;
2105
- byPath2.set(node.path, node);
1802
+ function relativePathFromRoots(absolutePath, roots) {
1803
+ for (const root of roots) {
1804
+ const abs = resolvePath(root);
1805
+ if (absolutePath.startsWith(`${abs}/`) || absolutePath.startsWith(`${abs}\\`)) {
1806
+ return absolutePath.slice(abs.length + 1).split(/[\\/]/).join("/");
1807
+ }
2106
1808
  }
2107
- for (const link of externalLinks) {
2108
- const source = byPath2.get(link.source);
2109
- if (source && !cachedPaths.has(source.path)) source.externalRefsCount += 1;
1809
+ return absolutePath;
1810
+ }
1811
+ function buildFreshNodeAndValidateFrontmatter(opts) {
1812
+ const node = buildNode({
1813
+ path: opts.raw.path,
1814
+ kind: opts.kind,
1815
+ providerId: opts.provider.id,
1816
+ frontmatterRaw: opts.raw.frontmatterRaw,
1817
+ body: opts.raw.body,
1818
+ frontmatter: opts.raw.frontmatter,
1819
+ bodyHash: opts.bodyHash,
1820
+ frontmatterHash: opts.frontmatterHash,
1821
+ encoder: opts.encoder
1822
+ });
1823
+ const frontmatterIssues = [];
1824
+ if (opts.raw.frontmatterRaw.length > 0) {
1825
+ const fmIssue = validateFrontmatter(
1826
+ opts.providerFrontmatter,
1827
+ opts.provider,
1828
+ opts.kind,
1829
+ opts.raw.frontmatter,
1830
+ opts.raw.path,
1831
+ opts.strict
1832
+ );
1833
+ if (fmIssue) frontmatterIssues.push(fmIssue);
1834
+ } else {
1835
+ const malformed = detectMalformedFrontmatter(opts.raw.body, opts.raw.path, opts.strict);
1836
+ if (malformed) frontmatterIssues.push(malformed);
2110
1837
  }
1838
+ return { node, frontmatterIssues };
2111
1839
  }
2112
1840
  function mergeNodeWithEnrichments(node, enrichments, opts = {}) {
2113
1841
  const includeStale = opts.includeStale === true;
@@ -2127,11 +1855,400 @@ function assignSafe(target, source) {
2127
1855
  }
2128
1856
  }
2129
1857
 
1858
+ // kernel/orchestrator/walk.ts
1859
+ async function walkAndExtract(opts) {
1860
+ const accum = createWalkAccumulators();
1861
+ const wctx = buildWalkContext(opts);
1862
+ const claimedPaths = /* @__PURE__ */ new Set();
1863
+ const walkOptions = opts.ignoreFilter ? { ignoreFilter: opts.ignoreFilter } : {};
1864
+ let filesWalked = 0;
1865
+ let index = 0;
1866
+ for (const provider of opts.providers) {
1867
+ for await (const raw of resolveProviderWalk(provider)(opts.roots, walkOptions)) {
1868
+ filesWalked += 1;
1869
+ if (claimedPaths.has(raw.path)) continue;
1870
+ const advanced = await processRawNode(raw, provider, wctx, accum, claimedPaths, index + 1);
1871
+ if (advanced) index += 1;
1872
+ }
1873
+ }
1874
+ const orphanSidecars = discoverOrphanSidecars(opts.roots);
1875
+ return {
1876
+ nodes: accum.nodes,
1877
+ internalLinks: accum.internalLinks,
1878
+ externalLinks: accum.externalLinks,
1879
+ cachedPaths: accum.cachedPaths,
1880
+ frontmatterIssues: accum.frontmatterIssues,
1881
+ filesWalked,
1882
+ enrichments: [...accum.enrichmentBuffer.values()],
1883
+ extractorRuns: accum.extractorRuns,
1884
+ contributions: accum.contributionsBuffer,
1885
+ freshlyRunTuples: accum.freshlyRunTuples,
1886
+ orphanSidecars,
1887
+ sidecarRoots: accum.sidecarRoots
1888
+ };
1889
+ }
1890
+ function createWalkAccumulators() {
1891
+ return {
1892
+ nodes: [],
1893
+ internalLinks: [],
1894
+ externalLinks: [],
1895
+ cachedPaths: /* @__PURE__ */ new Set(),
1896
+ frontmatterIssues: [],
1897
+ enrichmentBuffer: /* @__PURE__ */ new Map(),
1898
+ contributionsBuffer: [],
1899
+ freshlyRunTuples: /* @__PURE__ */ new Set(),
1900
+ extractorRuns: [],
1901
+ sidecarRoots: /* @__PURE__ */ new Map()
1902
+ };
1903
+ }
1904
+ function buildWalkContext(opts) {
1905
+ const { priorNodesByPath, priorLinksByOriginating, priorFrontmatterIssuesByNode } = opts.priorIndex;
1906
+ const shortIdToQualified = /* @__PURE__ */ new Map();
1907
+ for (const ex of opts.extractors) {
1908
+ const qualified = qualifiedExtensionId(ex.pluginId, ex.id);
1909
+ const list = shortIdToQualified.get(ex.id);
1910
+ if (list) list.push(qualified);
1911
+ else shortIdToQualified.set(ex.id, [qualified]);
1912
+ }
1913
+ return { opts, priorNodesByPath, priorLinksByOriginating, priorFrontmatterIssuesByNode, shortIdToQualified };
1914
+ }
1915
+ async function processRawNode(raw, provider, wctx, accum, claimedPaths, nextIndex) {
1916
+ const bodyHash = sha256(raw.body);
1917
+ const frontmatterHash = sha256(canonicalFrontmatter(raw.frontmatter, raw.frontmatterRaw));
1918
+ const kind = provider.classify(raw.path, raw.frontmatter);
1919
+ if (kind === null) {
1920
+ return false;
1921
+ }
1922
+ claimedPaths.add(raw.path);
1923
+ const priorNode = wctx.priorNodesByPath.get(raw.path);
1924
+ const nodeHashCacheEligible = wctx.opts.enableCache && wctx.opts.prior !== null && priorNode !== void 0 && priorNode.bodyHash === bodyHash && priorNode.frontmatterHash === frontmatterHash;
1925
+ const sidecarResolution = resolveSidecarOverlay(
1926
+ raw.path,
1927
+ raw.path,
1928
+ wctx.opts.roots,
1929
+ bodyHash,
1930
+ frontmatterHash
1931
+ );
1932
+ const sidecarAnnotationsHash = sha256(
1933
+ canonicalSidecarAnnotations(sidecarResolution.overlay.annotations)
1934
+ );
1935
+ const cacheDecision = computeCacheDecision({
1936
+ extractors: wctx.opts.extractors,
1937
+ kind,
1938
+ nodePath: raw.path,
1939
+ bodyHash,
1940
+ sidecarAnnotationsHash,
1941
+ nodeHashCacheEligible,
1942
+ priorExtractorRuns: wctx.opts.priorExtractorRuns
1943
+ });
1944
+ const ctx = {
1945
+ raw,
1946
+ provider,
1947
+ kind,
1948
+ bodyHash,
1949
+ frontmatterHash,
1950
+ sidecarResolution,
1951
+ sidecarAnnotationsHash,
1952
+ nodeHashCacheEligible,
1953
+ cacheDecision,
1954
+ priorNode,
1955
+ index: nextIndex
1956
+ };
1957
+ if (cacheDecision.fullCacheHit && priorNode) {
1958
+ applyFullCacheHit(ctx, wctx, accum);
1959
+ } else {
1960
+ await applyExtractPath(ctx, wctx, accum);
1961
+ }
1962
+ return true;
1963
+ }
1964
+ function attachSidecar(node, resolution, sidecarRoots) {
1965
+ node.sidecar = resolution.overlay;
1966
+ if (resolution.parsedRoot !== null) {
1967
+ sidecarRoots.set(node.path, resolution.parsedRoot);
1968
+ }
1969
+ return resolution.issues.map(
1970
+ (i) => i.nodeIds.length > 0 ? i : { ...i, nodeIds: [node.path] }
1971
+ );
1972
+ }
1973
+ function applyFullCacheHit(ctx, wctx, accum) {
1974
+ const reused = reusePriorNode({
1975
+ priorNode: ctx.priorNode,
1976
+ bodyHash: ctx.bodyHash,
1977
+ sidecarAnnotationsHash: ctx.sidecarAnnotationsHash,
1978
+ strict: wctx.opts.strict,
1979
+ cachedQualifiedIds: ctx.cacheDecision.cachedQualifiedIds,
1980
+ applicableQualifiedIds: ctx.cacheDecision.applicableQualifiedIds,
1981
+ shortIdToQualified: wctx.shortIdToQualified,
1982
+ priorLinksByOriginating: wctx.priorLinksByOriginating,
1983
+ priorFrontmatterIssuesByNode: wctx.priorFrontmatterIssuesByNode
1984
+ });
1985
+ const reusedSidecarIssues = attachSidecar(reused.node, ctx.sidecarResolution, accum.sidecarRoots);
1986
+ accum.nodes.push(reused.node);
1987
+ accum.cachedPaths.add(reused.node.path);
1988
+ for (const link of reused.internalLinks) accum.internalLinks.push(link);
1989
+ for (const issue of reused.frontmatterIssues) accum.frontmatterIssues.push(issue);
1990
+ for (const issue of reusedSidecarIssues) accum.frontmatterIssues.push(issue);
1991
+ for (const run of reused.extractorRuns) accum.extractorRuns.push(run);
1992
+ wctx.opts.emitter.emit(makeEvent("scan.progress", {
1993
+ index: ctx.index,
1994
+ path: ctx.raw.path,
1995
+ kind: ctx.kind,
1996
+ cached: true
1997
+ }));
1998
+ }
1999
+ async function applyExtractPath(ctx, wctx, accum) {
2000
+ const node = buildOrReuseNode(ctx, wctx, accum);
2001
+ const sidecarIssues = attachSidecar(node, ctx.sidecarResolution, accum.sidecarRoots);
2002
+ for (const issue of sidecarIssues) accum.frontmatterIssues.push(issue);
2003
+ const partialCacheHit = isPartialCacheHit(ctx);
2004
+ emitExtractProgress(ctx, wctx, partialCacheHit);
2005
+ const extractorsToRun = partialCacheHit ? ctx.cacheDecision.missingExtractors : ctx.cacheDecision.applicableExtractors;
2006
+ recordFreshlyRunTuples(extractorsToRun, node.path, accum);
2007
+ const extractResult = await runExtractorsForNode({
2008
+ extractors: extractorsToRun,
2009
+ node,
2010
+ body: ctx.raw.body,
2011
+ frontmatter: ctx.raw.frontmatter,
2012
+ bodyHash: ctx.bodyHash,
2013
+ emitter: wctx.opts.emitter,
2014
+ ...wctx.opts.pluginStores ? { pluginStores: wctx.opts.pluginStores } : {}
2015
+ });
2016
+ mergeExtractResult(extractResult, accum);
2017
+ recordExtractorRuns(node.path, ctx, accum);
2018
+ }
2019
+ function emitExtractProgress(ctx, wctx, partialCacheHit) {
2020
+ wctx.opts.emitter.emit(makeEvent("scan.progress", {
2021
+ index: ctx.index,
2022
+ path: ctx.raw.path,
2023
+ kind: ctx.kind,
2024
+ cached: false,
2025
+ ...partialCacheHit ? { partialCache: true } : {}
2026
+ }));
2027
+ }
2028
+ function recordFreshlyRunTuples(extractors, nodePath, accum) {
2029
+ for (const ex of extractors) {
2030
+ accum.freshlyRunTuples.add(`${ex.pluginId}\0${ex.id}\0${nodePath}`);
2031
+ }
2032
+ }
2033
+ function mergeExtractResult(extractResult, accum) {
2034
+ for (const link of extractResult.internalLinks) accum.internalLinks.push(link);
2035
+ for (const link of extractResult.externalLinks) accum.externalLinks.push(link);
2036
+ for (const enr of extractResult.enrichments) {
2037
+ accum.enrichmentBuffer.set(`${enr.nodePath}\0${enr.extractorId}`, enr);
2038
+ }
2039
+ for (const c of extractResult.contributions) accum.contributionsBuffer.push(c);
2040
+ }
2041
+ function isPartialCacheHit(ctx) {
2042
+ return ctx.nodeHashCacheEligible && ctx.cacheDecision.cachedQualifiedIds.size > 0 && ctx.priorNode !== void 0;
2043
+ }
2044
+ function buildOrReuseNode(ctx, wctx, accum) {
2045
+ if (isPartialCacheHit(ctx) && ctx.priorNode) {
2046
+ const partial = cloneNodeAndReshapeLinks({
2047
+ priorNode: ctx.priorNode,
2048
+ strict: wctx.opts.strict,
2049
+ cachedQualifiedIds: ctx.cacheDecision.cachedQualifiedIds,
2050
+ applicableQualifiedIds: ctx.cacheDecision.applicableQualifiedIds,
2051
+ shortIdToQualified: wctx.shortIdToQualified,
2052
+ priorLinksByOriginating: wctx.priorLinksByOriginating,
2053
+ priorFrontmatterIssuesByNode: wctx.priorFrontmatterIssuesByNode
2054
+ });
2055
+ for (const link of partial.internalLinks) accum.internalLinks.push(link);
2056
+ for (const issue of partial.frontmatterIssues) accum.frontmatterIssues.push(issue);
2057
+ accum.nodes.push(partial.node);
2058
+ return partial.node;
2059
+ }
2060
+ const fresh = buildFreshNodeAndValidateFrontmatter({
2061
+ raw: ctx.raw,
2062
+ kind: ctx.kind,
2063
+ provider: ctx.provider,
2064
+ bodyHash: ctx.bodyHash,
2065
+ frontmatterHash: ctx.frontmatterHash,
2066
+ encoder: wctx.opts.encoder,
2067
+ providerFrontmatter: wctx.opts.providerFrontmatter,
2068
+ strict: wctx.opts.strict
2069
+ });
2070
+ accum.nodes.push(fresh.node);
2071
+ for (const issue of fresh.frontmatterIssues) accum.frontmatterIssues.push(issue);
2072
+ return fresh.node;
2073
+ }
2074
+ function recordExtractorRuns(nodePath, ctx, accum) {
2075
+ const ranAt = Date.now();
2076
+ for (const ex of ctx.cacheDecision.applicableExtractors) {
2077
+ accum.extractorRuns.push({
2078
+ nodePath,
2079
+ extractorId: qualifiedExtensionId(ex.pluginId, ex.id),
2080
+ bodyHashAtRun: ctx.bodyHash,
2081
+ ranAt,
2082
+ sidecarAnnotationsHashAtRun: ctx.sidecarAnnotationsHash
2083
+ });
2084
+ }
2085
+ }
2086
+
2087
+ // kernel/orchestrator/index.ts
2088
+ var SCANNED_BY = {
2089
+ name: "skill-map",
2090
+ version: package_default.version,
2091
+ specVersion: resolveSpecVersionSafe()
2092
+ };
2093
+ function resolveSpecVersionSafe() {
2094
+ try {
2095
+ return installedSpecVersion();
2096
+ } catch {
2097
+ return "unknown";
2098
+ }
2099
+ }
2100
+ async function runScanWithRenames(_kernel, options) {
2101
+ return runScanInternal(_kernel, options);
2102
+ }
2103
+ async function runScan(_kernel, options) {
2104
+ const { result } = await runScanInternal(_kernel, options);
2105
+ return result;
2106
+ }
2107
+ async function runScanInternal(_kernel, options) {
2108
+ validateRoots(options.roots);
2109
+ const setup = buildScanSetup(options);
2110
+ const { emitter, exts, hookDispatcher, encoder, prior, start } = setup;
2111
+ const scanStartedEvent = makeEvent("scan.started", { roots: options.roots });
2112
+ emitter.emit(scanStartedEvent);
2113
+ await hookDispatcher.dispatch("scan.started", scanStartedEvent);
2114
+ const walked = await walkAndExtract({
2115
+ providers: exts.providers,
2116
+ extractors: exts.extractors,
2117
+ roots: options.roots,
2118
+ ...options.ignoreFilter ? { ignoreFilter: options.ignoreFilter } : {},
2119
+ emitter,
2120
+ encoder,
2121
+ strict: setup.strict,
2122
+ enableCache: setup.enableCache,
2123
+ prior,
2124
+ priorIndex: setup.priorIndex,
2125
+ priorExtractorRuns: setup.priorExtractorRuns,
2126
+ providerFrontmatter: setup.providerFrontmatter,
2127
+ pluginStores: options.pluginStores
2128
+ });
2129
+ recomputeLinkCounts(walked.nodes, walked.internalLinks);
2130
+ recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);
2131
+ await dispatchExtractorCompleted(exts.extractors, emitter, hookDispatcher);
2132
+ const analyzerResult = await runAnalyzers(
2133
+ exts.analyzers,
2134
+ walked.nodes,
2135
+ walked.internalLinks,
2136
+ walked.orphanSidecars,
2137
+ walked.sidecarRoots,
2138
+ options.annotationContributions ?? [],
2139
+ options.viewContributions ?? [],
2140
+ options.orphanJobFiles ?? [],
2141
+ options.referenceablePaths,
2142
+ options.cwd,
2143
+ emitter,
2144
+ hookDispatcher
2145
+ );
2146
+ mergeAnalyzerEmissions(walked, analyzerResult, exts.analyzers);
2147
+ const issues = analyzerResult.issues;
2148
+ for (const issue of walked.frontmatterIssues) issues.push(issue);
2149
+ const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues) : [];
2150
+ const stats = buildScanStats(walked, issues, start);
2151
+ const scanCompletedEvent = makeEvent("scan.completed", { stats });
2152
+ emitter.emit(scanCompletedEvent);
2153
+ await hookDispatcher.dispatch("scan.completed", scanCompletedEvent);
2154
+ return buildScanReturn(walked, issues, renameOps, stats, options, setup);
2155
+ }
2156
+ function buildScanSetup(options) {
2157
+ const start = Date.now();
2158
+ const emitter = options.emitter ?? new InMemoryProgressEmitter();
2159
+ const exts = options.extensions ?? { providers: [], extractors: [], analyzers: [] };
2160
+ const hookDispatcher = makeHookDispatcher(exts.hooks ?? [], emitter);
2161
+ const tokenize = options.tokenize !== false;
2162
+ const encoder = tokenize ? new Tiktoken2(cl100k_base) : null;
2163
+ const prior = options.priorSnapshot ?? null;
2164
+ const priorIndex = indexPriorSnapshot(prior);
2165
+ const providerFrontmatter = buildProviderFrontmatterValidator(exts.providers);
2166
+ return {
2167
+ start,
2168
+ scannedAt: start,
2169
+ emitter,
2170
+ exts,
2171
+ hookDispatcher,
2172
+ encoder,
2173
+ prior,
2174
+ priorIndex,
2175
+ priorExtractorRuns: options.priorExtractorRuns,
2176
+ providerFrontmatter,
2177
+ scope: options.scope ?? "project",
2178
+ strict: options.strict === true,
2179
+ enableCache: options.enableCache === true
2180
+ };
2181
+ }
2182
+ async function dispatchExtractorCompleted(extractors, emitter, hookDispatcher) {
2183
+ for (const extractor of extractors) {
2184
+ const extractorId = qualifiedExtensionId(extractor.pluginId, extractor.id);
2185
+ const evt = makeEvent("extractor.completed", { extractorId });
2186
+ emitter.emit(evt);
2187
+ await hookDispatcher.dispatch("extractor.completed", evt);
2188
+ }
2189
+ }
2190
+ function mergeAnalyzerEmissions(walked, analyzerResult, analyzers) {
2191
+ for (const c of analyzerResult.contributions) walked.contributions.push(c);
2192
+ for (const analyzer of analyzers ?? []) {
2193
+ if (analyzer.viewContributions === void 0) continue;
2194
+ for (const node of walked.nodes) {
2195
+ walked.freshlyRunTuples.add(`${analyzer.pluginId}\0${analyzer.id}\0${node.path}`);
2196
+ }
2197
+ }
2198
+ }
2199
+ function buildScanStats(walked, issues, start) {
2200
+ return {
2201
+ // `filesSkipped` is "files walked but not classified by any
2202
+ // Provider". Today every walked file IS classified by its Provider
2203
+ // (the `claude` Provider's `classify()` always returns a kind,
2204
+ // falling back to `'markdown'`), so this is always 0. Wired now
2205
+ // so the field shape is spec-conformant; meaningful once multiple
2206
+ // Providers compete.
2207
+ filesWalked: walked.filesWalked,
2208
+ filesSkipped: 0,
2209
+ nodesCount: walked.nodes.length,
2210
+ linksCount: walked.internalLinks.length,
2211
+ issuesCount: issues.length,
2212
+ durationMs: Date.now() - start
2213
+ };
2214
+ }
2215
+ function buildScanReturn(walked, issues, renameOps, stats, options, setup) {
2216
+ return {
2217
+ result: {
2218
+ schemaVersion: 1,
2219
+ scannedAt: setup.scannedAt,
2220
+ scope: setup.scope,
2221
+ roots: options.roots,
2222
+ providers: setup.exts.providers.map((a) => a.id),
2223
+ scannedBy: SCANNED_BY,
2224
+ nodes: walked.nodes,
2225
+ links: walked.internalLinks,
2226
+ issues,
2227
+ stats
2228
+ },
2229
+ renameOps,
2230
+ extractorRuns: walked.extractorRuns,
2231
+ enrichments: walked.enrichments,
2232
+ contributions: walked.contributions,
2233
+ freshlyRunTuples: walked.freshlyRunTuples
2234
+ };
2235
+ }
2236
+ function validateRoots(roots) {
2237
+ if (roots.length === 0) {
2238
+ throw new Error(ORCHESTRATOR_TEXTS.runScanRootEmptyArray);
2239
+ }
2240
+ for (const root of roots) {
2241
+ if (!existsSync9(root) || !statSync2(root).isDirectory()) {
2242
+ throw new Error(tx(ORCHESTRATOR_TEXTS.runScanRootMissing, { root }));
2243
+ }
2244
+ }
2245
+ }
2246
+
2130
2247
  // kernel/scan/watcher.ts
2131
- import { resolve as resolve8, relative as relative4, sep as sep3 } from "path";
2248
+ import { resolve as resolve10, relative as relative4, sep as sep3 } from "path";
2132
2249
  import chokidar from "chokidar";
2133
2250
  function createChokidarWatcher(opts) {
2134
- const absRoots = opts.roots.map((r) => resolve8(opts.cwd, r));
2251
+ const absRoots = opts.roots.map((r) => resolve10(opts.cwd, r));
2135
2252
  const ignoreFilterOpt = opts.ignoreFilter;
2136
2253
  const getFilter = ignoreFilterOpt === void 0 ? void 0 : typeof ignoreFilterOpt === "function" ? ignoreFilterOpt : () => ignoreFilterOpt;
2137
2254
  const ignored = getFilter ? (path) => {