@skill-map/cli 0.21.0 → 0.23.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.23.0",
107
104
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
108
105
  license: "MIT",
109
106
  type: "module",
@@ -156,29 +153,30 @@ 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.23.0",
173
171
  ajv: "8.18.0",
174
172
  "ajv-formats": "3.0.1",
175
173
  chokidar: "5.0.0",
176
174
  clipanion: "4.0.0-rc.4",
177
- hono: "4.12.16",
175
+ hono: "4.12.18",
178
176
  ignore: "7.0.5",
179
177
  "js-tiktoken": "1.0.21",
180
178
  "js-yaml": "4.1.1",
181
- kysely: "0.28.16",
179
+ kysely: "0.28.17",
182
180
  semver: "7.7.4",
183
181
  typanion: "3.14.0",
184
182
  ws: "8.20.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,149 @@ 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}}): {{errors}}",
275
+ dedicatedValidationFailed: "plugin '{{pluginId}}' ctx.store.write('{{table}}', row): row violates declared schema ({{schemaPath}}): {{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;
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
+ };
306
320
  }
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
- );
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
+ });
316
333
  }
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);
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
+ });
336
341
  }
337
- return out;
342
+ return void 0;
338
343
  }
339
- function walk(root, current, shouldSkip, out) {
340
- let entries;
341
- try {
342
- entries = readdirSync(current, { withFileTypes: true, encoding: "utf8" });
343
- } catch {
344
- return;
345
- }
346
- for (const entry of entries) {
347
- const full = join(current, entry.name);
348
- const rel = relative(root, full).split(sep).join("/");
349
- if (shouldSkip(rel)) continue;
350
- if (entry.isSymbolicLink()) continue;
351
- if (entry.isDirectory()) {
352
- walk(root, full, shouldSkip, out);
353
- continue;
354
- }
355
- if (!entry.isFile()) continue;
356
- if (!entry.name.endsWith(".sm")) continue;
357
- const expectedMd = `${full.slice(0, -".sm".length)}.md`;
358
- if (existsSync2(expectedMd) && safeIsFile(expectedMd)) continue;
359
- out.push({ sidecarPath: full, relativePath: rel, expectedMdPath: expectedMd });
360
- }
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("; ");
361
347
  }
362
- function safeIsFile(path) {
363
- try {
364
- return statSync(path).isFile();
365
- } catch {
366
- return false;
367
- }
348
+
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;
368
356
  }
369
357
 
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";
358
+ // kernel/adapters/schema-validators.ts
359
+ import { readFileSync as readFileSync3 } from "fs";
360
+ import { dirname, resolve as resolve4 } from "path";
361
+ import { createRequire as createRequire2 } from "module";
374
362
  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
363
 
380
- // kernel/config/loader.ts
381
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
364
+ // kernel/types/view-catalog.ts
365
+ var ALL_SLOT_NAMES = [
366
+ "card.title.right",
367
+ "card.subtitle.left",
368
+ "card.footer.left",
369
+ "card.footer.right",
370
+ "graph.node.alert",
371
+ "inspector.header.badge.counter",
372
+ "inspector.header.badge.tag",
373
+ "inspector.body.panel.breakdown",
374
+ "inspector.body.panel.records",
375
+ "inspector.body.panel.tree",
376
+ "inspector.body.panel.key-values",
377
+ "inspector.body.panel.link-list",
378
+ "inspector.body.panel.markdown",
379
+ "topbar.nav.start"
380
+ ];
381
+ var KNOWN_SLOT_NAMES = new Set(ALL_SLOT_NAMES);
382
382
 
383
383
  // kernel/adapters/schema-validators.ts
384
- import { readFileSync as readFileSync2 } from "fs";
385
- import { dirname as dirname2, resolve as resolve2 } from "path";
386
- import { createRequire as createRequire2 } from "module";
387
- import { Ajv2020 as Ajv20202 } from "ajv/dist/2020.js";
388
384
  var SCHEMA_FILES = {
389
385
  node: "schemas/node.schema.json",
390
386
  link: "schemas/link.schema.json",
@@ -419,23 +415,23 @@ function loadSchemaValidators() {
419
415
  return cachedValidators;
420
416
  }
421
417
  function buildSchemaValidators() {
422
- const specRoot = resolveSpecRoot2();
423
- const ajv = new Ajv20202({
418
+ const specRoot = resolveSpecRoot();
419
+ const ajv = new Ajv20203({
424
420
  strict: false,
425
421
  allErrors: true,
426
422
  allowUnionTypes: true
427
423
  });
428
424
  applyAjvFormats(ajv);
429
425
  for (const rel of SUPPORTING_SCHEMAS) {
430
- const file = resolve2(specRoot, rel);
426
+ const file = resolve4(specRoot, rel);
431
427
  if (!existsSyncSafe(file)) continue;
432
- const schema = JSON.parse(readFileSync2(file, "utf8"));
428
+ const schema = JSON.parse(readFileSync3(file, "utf8"));
433
429
  ajv.addSchema(schema);
434
430
  }
435
431
  const validators = /* @__PURE__ */ new Map();
436
432
  for (const [name, rel] of Object.entries(SCHEMA_FILES)) {
437
- const file = resolve2(specRoot, rel);
438
- const schema = JSON.parse(readFileSync2(file, "utf8"));
433
+ const file = resolve4(specRoot, rel);
434
+ const schema = JSON.parse(readFileSync3(file, "utf8"));
439
435
  const byId = typeof schema.$id === "string" ? ajv.getSchema(schema.$id) : void 0;
440
436
  validators.set(name, byId ?? ajv.compile(schema));
441
437
  }
@@ -452,24 +448,8 @@ function buildSchemaValidators() {
452
448
  });
453
449
  const contributionValidators = /* @__PURE__ */ new Map();
454
450
  const VIEW_SLOTS_ID = "https://skill-map.dev/spec/v0/view-slots.schema.json";
455
- const KNOWN_SLOTS = /* @__PURE__ */ new Set([
456
- "card.title.right",
457
- "card.subtitle.left",
458
- "card.footer.left",
459
- "card.footer.right",
460
- "graph.node.alert",
461
- "inspector.header.badge.counter",
462
- "inspector.header.badge.tag",
463
- "inspector.body.panel.breakdown",
464
- "inspector.body.panel.records",
465
- "inspector.body.panel.tree",
466
- "inspector.body.panel.key-values",
467
- "inspector.body.panel.link-list",
468
- "inspector.body.panel.markdown",
469
- "topbar.nav.start"
470
- ]);
471
451
  function getContributionValidator(slot) {
472
- if (!KNOWN_SLOTS.has(slot)) return null;
452
+ if (!KNOWN_SLOT_NAMES.has(slot)) return null;
473
453
  const existing = contributionValidators.get(slot);
474
454
  if (existing) return existing;
475
455
  const ref = `${VIEW_SLOTS_ID}#/$defs/payloads/${slot}`;
@@ -515,15 +495,15 @@ function buildSchemaValidators() {
515
495
  };
516
496
  }
517
497
  function buildProviderFrontmatterValidator(providers) {
518
- const specRoot = resolveSpecRoot2();
519
- const ajv = new Ajv20202({
498
+ const specRoot = resolveSpecRoot();
499
+ const ajv = new Ajv20203({
520
500
  strict: false,
521
501
  allErrors: true,
522
502
  allowUnionTypes: true
523
503
  });
524
504
  applyAjvFormats(ajv);
525
- const baseFile = resolve2(specRoot, "schemas/frontmatter/base.schema.json");
526
- const baseSchema = JSON.parse(readFileSync2(baseFile, "utf8"));
505
+ const baseFile = resolve4(specRoot, "schemas/frontmatter/base.schema.json");
506
+ const baseSchema = JSON.parse(readFileSync3(baseFile, "utf8"));
527
507
  ajv.addSchema(baseSchema);
528
508
  registerProviderAuxiliarySchemas(ajv, providers);
529
509
  const compiled = /* @__PURE__ */ new Map();
@@ -560,20 +540,20 @@ function registerProviderAuxiliarySchemas(ajv, providers) {
560
540
  }
561
541
  }
562
542
  }
563
- function resolveSpecRoot2() {
543
+ function resolveSpecRoot() {
564
544
  const require2 = createRequire2(import.meta.url);
565
545
  try {
566
546
  const indexPath = require2.resolve("@skill-map/spec/index.json");
567
- return dirname2(indexPath);
547
+ return dirname(indexPath);
568
548
  } catch {
569
549
  throw new Error(
570
- "@skill-map/spec not resolvable \u2014 ensure the workspace is linked or the package is installed."
550
+ "@skill-map/spec not resolvable: ensure the workspace is linked or the package is installed."
571
551
  );
572
552
  }
573
553
  }
574
554
  function existsSyncSafe(path) {
575
555
  try {
576
- readFileSync2(path, "utf8");
556
+ readFileSync3(path, "utf8");
577
557
  return true;
578
558
  } catch {
579
559
  return false;
@@ -585,45 +565,6 @@ function formatErrorMessage(err) {
585
565
  return err instanceof Error ? err.message : String(err);
586
566
  }
587
567
 
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
568
  // kernel/adapters/silent-logger.ts
628
569
  var SilentLogger = class {
629
570
  trace() {
@@ -657,606 +598,314 @@ function getActiveLogger() {
657
598
  return active;
658
599
  }
659
600
 
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);
601
+ // kernel/extensions/hook-dispatcher.ts
602
+ function makeHookDispatcher(hooks, emitter) {
603
+ if (hooks.length === 0) {
604
+ return { dispatch: async () => {
605
+ } };
606
+ }
607
+ const byTrigger = /* @__PURE__ */ new Map();
608
+ for (const hook of hooks) {
609
+ if (hook.mode === "probabilistic") {
610
+ const qualifiedId = qualifiedExtensionId(hook.pluginId, hook.id);
611
+ log.warn(
612
+ `Probabilistic hook ${qualifiedId} deferred to job subsystem (future job subsystem). The hook is registered but will not dispatch in-scan.`,
613
+ { hookId: qualifiedId, mode: "probabilistic" }
614
+ );
615
+ continue;
693
616
  }
694
- };
695
- }
696
- function makeDedicatedStoreWrapper(opts) {
697
- const { pluginId, schemas, persist } = opts;
617
+ for (const trig of hook.triggers) {
618
+ const bucket = byTrigger.get(trig);
619
+ if (bucket) bucket.push(hook);
620
+ else byTrigger.set(trig, [hook]);
621
+ }
622
+ }
698
623
  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)
624
+ async dispatch(trigger, event) {
625
+ const subs = byTrigger.get(trigger);
626
+ if (!subs || subs.length === 0) return;
627
+ for (const hook of subs) {
628
+ if (!matchesFilter(hook, event)) continue;
629
+ const ctx = buildHookContext(hook, trigger, event);
630
+ try {
631
+ await hook.on(ctx);
632
+ } catch (err) {
633
+ const qualifiedId = qualifiedExtensionId(hook.pluginId, hook.id);
634
+ const message = formatErrorMessage(err);
635
+ emitter.emit(
636
+ makeEvent("extension.error", {
637
+ kind: "hook-error",
638
+ extensionId: qualifiedId,
639
+ trigger,
640
+ message
709
641
  })
710
642
  );
711
643
  }
712
644
  }
713
- await persist(table, row);
714
645
  }
715
646
  };
716
647
  }
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;
648
+ function makeEvent(type, data) {
649
+ return { type, timestamp: (/* @__PURE__ */ new Date()).toISOString(), data };
739
650
  }
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("; ");
651
+ function matchesFilter(hook, event) {
652
+ if (!hook.filter) return true;
653
+ const data = event.data ?? {};
654
+ for (const [key, expected] of Object.entries(hook.filter)) {
655
+ if (data[key] !== expected) return false;
656
+ }
657
+ return true;
743
658
  }
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;
659
+ function buildHookContext(_hook, trigger, event) {
660
+ const data = event.data ?? {};
661
+ const ctx = {
662
+ event: {
663
+ type: trigger,
664
+ timestamp: event.timestamp,
665
+ ...event.runId !== void 0 ? { runId: event.runId } : {},
666
+ ...event.jobId !== void 0 ? { jobId: event.jobId } : {},
667
+ data: event.data
668
+ }
669
+ };
670
+ if (typeof data["extractorId"] === "string") ctx.extractorId = data["extractorId"];
671
+ if (typeof data["analyzerId"] === "string") ctx.analyzerId = data["analyzerId"];
672
+ if (typeof data["actionId"] === "string") ctx.actionId = data["actionId"];
673
+ if (data["node"] && typeof data["node"] === "object") {
674
+ ctx.node = data["node"];
675
+ }
676
+ if (data["jobResult"] !== void 0) ctx.jobResult = data["jobResult"];
677
+ return ctx;
769
678
  }
770
679
 
771
680
  // kernel/i18n/orchestrator.texts.ts
772
681
  var ORCHESTRATOR_TEXTS = {
773
682
  frontmatterInvalid: "Frontmatter for {{path}} ({{kind}}) failed schema validation: {{errors}}",
774
- frontmatterMalformedPasteWithIndent: "Frontmatter fence in {{path}} appears indented; YAML frontmatter MUST start with `---` at column 0. The file was scanned as body-only \u2014 the metadata block was silently lost. Move the `---` lines to the start of the line.",
683
+ frontmatterMalformedPasteWithIndent: "Frontmatter fence in {{path}} appears indented; YAML frontmatter MUST start with `---` at column 0. The file was scanned as body-only; the metadata block was silently lost. Move the `---` lines to the start of the line.",
775
684
  frontmatterMalformedByteOrderMark: "Frontmatter fence in {{path}} is preceded by a UTF-8 byte-order mark (BOM); the file was scanned as body-only. Re-save the file as UTF-8 without BOM. The metadata block was silently lost.",
776
- frontmatterMalformedMissingClose: "Frontmatter in {{path}} opens with `---` but never closes \u2014 no matching `---` line at column 0 was found. The file was scanned as body-only and every metadata field was silently lost. Add a closing `---` line below the metadata block.",
685
+ frontmatterMalformedMissingClose: "Frontmatter in {{path}} opens with `---` but never closes (no matching `---` line at column 0 was found). The file was scanned as body-only and every metadata field was silently lost. Add a closing `---` line below the metadata block.",
777
686
  extensionErrorLinkKindNotDeclared: 'Extractor "{{extractorId}}" emitted a link of kind "{{linkKind}}" outside its declared `emitsLinkKinds` set [{{declaredKinds}}]. Link dropped.',
778
687
  extensionErrorIssueInvalidSeverity: `Rule "{{analyzerId}}" emitted an issue with invalid severity {{severity}} (allowed: 'error' | 'warn' | 'info'). Issue dropped.`,
779
688
  extensionErrorContributionUnknownId: 'Extractor "{{extractorId}}" emitted contribution "{{contributionId}}" on {{nodePath}} but did not declare it in its `viewContributions` map. Contribution dropped.',
780
689
  extensionErrorContributionPayloadInvalid: 'Extractor "{{extractorId}}" emitted contribution "{{contributionId}}" on {{nodePath}}; payload failed the "{{slot}}" schema: {{errors}}. Contribution dropped.',
690
+ extensionErrorRecommendedActionMissing: 'Analyzer "{{analyzerId}}" declares recommendedAction "{{actionId}}" but no Action is registered under that qualified id. The analyzer stays registered; the recommendation will not surface in the inspector.',
781
691
  runScanRootEmptyArray: "runScan: roots must contain at least one path (spec requires minItems: 1)",
782
692
  runScanRootMissing: "runScan: root path '{{root}}' does not exist or is not a directory"
783
693
  };
784
694
 
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;
695
+ // kernel/orchestrator/extractors.ts
696
+ async function runExtractorsForNode(opts) {
697
+ const internalLinks = [];
698
+ const externalLinks = [];
699
+ const enrichmentBuffer = /* @__PURE__ */ new Map();
700
+ const contributions = [];
701
+ const validators = loadSchemaValidators();
702
+ for (const extractor of opts.extractors) {
703
+ const qualifiedId = qualifiedExtensionId(extractor.pluginId, extractor.id);
704
+ const emitLink = (link) => {
705
+ const validated = validateLink(extractor, link, opts.emitter);
706
+ if (!validated) return;
707
+ if (isExternalUrlLink(validated)) externalLinks.push(validated);
708
+ else internalLinks.push(validated);
709
+ };
710
+ const enrichNode = (partial) => {
711
+ const key = `${opts.node.path}\0${qualifiedId}`;
712
+ const existing = enrichmentBuffer.get(key);
713
+ if (existing) {
714
+ existing.value = { ...existing.value, ...partial };
715
+ existing.enrichedAt = Date.now();
716
+ } else {
717
+ enrichmentBuffer.set(key, {
718
+ nodePath: opts.node.path,
719
+ extractorId: qualifiedId,
720
+ bodyHashAtEnrichment: opts.bodyHash,
721
+ value: { ...partial },
722
+ enrichedAt: Date.now(),
723
+ // Extractors are deterministic-only; `is_probabilistic` is
724
+ // reserved on the row for future Action-issued enrichments.
725
+ isProbabilistic: false
726
+ });
809
727
  }
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 {
728
+ };
729
+ const declaredContributions = readDeclaredContributions(extractor);
730
+ const emitContribution = (contributionId, payload) => {
731
+ const declared = declaredContributions.get(contributionId);
732
+ if (!declared) {
733
+ emitExtensionError(opts.emitter, qualifiedId, opts.node.path, {
734
+ phase: "emitContribution",
735
+ contributionId,
736
+ reason: "unknown-contribution-id",
737
+ message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {
738
+ extractorId: qualifiedId,
739
+ contributionId,
740
+ nodePath: opts.node.path
741
+ })
742
+ });
743
+ return;
836
744
  }
837
- }
838
- }
839
- return "";
840
- }
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
- }
745
+ const result = validators.validateContributionPayload(declared.slot, payload);
746
+ if (!result.ok) {
747
+ emitExtensionError(opts.emitter, qualifiedId, opts.node.path, {
748
+ phase: "emitContribution",
749
+ contributionId,
750
+ slot: declared.slot,
751
+ reason: result.errors,
752
+ message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
753
+ extractorId: qualifiedId,
754
+ contributionId,
755
+ nodePath: opts.node.path,
756
+ slot: declared.slot,
757
+ errors: result.errors
758
+ })
759
+ });
760
+ return;
861
761
  }
862
- } catch {
863
- }
864
- return { frontmatterRaw, frontmatter: parsed, body };
865
- }
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 };
762
+ contributions.push({
763
+ pluginId: extractor.pluginId,
764
+ extensionId: extractor.id,
765
+ nodePath: opts.node.path,
766
+ contributionId,
767
+ slot: declared.slot,
768
+ payload,
769
+ emittedAt: Date.now()
770
+ });
771
+ };
772
+ const store = opts.pluginStores?.get(extractor.pluginId);
773
+ const ctx = buildExtractorContext(
774
+ extractor,
775
+ opts.node,
776
+ opts.body,
777
+ opts.frontmatter,
778
+ emitLink,
779
+ enrichNode,
780
+ emitContribution,
781
+ store
782
+ );
783
+ await extractor.extract(ctx);
873
784
  }
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);
785
+ return {
786
+ internalLinks,
787
+ externalLinks,
788
+ enrichments: Array.from(enrichmentBuffer.values()),
789
+ contributions
790
+ };
884
791
  }
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";
792
+ function readDeclaredContributions(extension) {
793
+ const out = /* @__PURE__ */ new Map();
794
+ const raw = extension.viewContributions;
795
+ if (typeof raw !== "object" || raw === null) return out;
796
+ for (const [id, value] of Object.entries(raw)) {
797
+ if (typeof value !== "object" || value === null) continue;
798
+ const slot = value.slot;
799
+ if (typeof slot !== "string") continue;
800
+ out.set(id, { slot });
891
801
  }
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
- }
802
+ return out;
803
+ }
804
+ function emitExtensionError(emitter, qualifiedId, nodePath, data) {
805
+ emitter.emit(
806
+ makeEvent("extension.error", {
807
+ kind: "contribution-rejected",
808
+ extensionId: qualifiedId,
809
+ nodePath,
810
+ ...data
811
+ })
812
+ );
813
+ }
814
+ function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, emitContribution, store) {
815
+ const scope = extractor.scope;
816
+ return {
817
+ node,
818
+ body: scope === "frontmatter" ? "" : body,
819
+ frontmatter: scope === "body" ? {} : frontmatter,
820
+ emitLink,
821
+ enrichNode,
822
+ emitContribution,
823
+ ...store !== void 0 ? { store } : {}
824
+ };
825
+ }
826
+ function validateLink(extractor, link, emitter) {
827
+ if (!extractor.emitsLinkKinds.includes(link.kind)) {
828
+ const qualifiedId = `${extractor.pluginId}/${extractor.id}`;
829
+ emitter.emit(
830
+ makeEvent("extension.error", {
831
+ kind: "link-kind-not-declared",
832
+ extensionId: qualifiedId,
833
+ linkKind: link.kind,
834
+ declaredKinds: extractor.emitsLinkKinds,
835
+ link: { source: link.source, target: link.target, kind: link.kind },
836
+ message: tx(ORCHESTRATOR_TEXTS.extensionErrorLinkKindNotDeclared, {
837
+ extractorId: qualifiedId,
838
+ linkKind: link.kind,
839
+ declaredKinds: extractor.emitsLinkKinds.join(", ")
840
+ })
841
+ })
842
+ );
843
+ return null;
915
844
  }
845
+ const confidence = link.confidence ?? extractor.defaultConfidence;
846
+ return { ...link, confidence };
916
847
  }
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;
848
+ function recomputeLinkCounts(nodes, links) {
849
+ const byPath2 = /* @__PURE__ */ new Map();
850
+ for (const node of nodes) {
851
+ node.linksOutCount = 0;
852
+ node.linksInCount = 0;
853
+ byPath2.set(node.path, node);
923
854
  }
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
- }
855
+ for (const link of links) {
856
+ const source = byPath2.get(link.source);
857
+ if (source) source.linksOutCount += 1;
858
+ const target = byPath2.get(link.target);
859
+ if (target) target.linksInCount += 1;
939
860
  }
940
861
  }
941
- function hasMatchingExtension(name, extensions) {
942
- for (const ext of extensions) {
943
- if (name.endsWith(ext)) return true;
862
+ function recomputeExternalRefsCount(nodes, externalLinks, cachedPaths) {
863
+ const byPath2 = /* @__PURE__ */ new Map();
864
+ for (const node of nodes) {
865
+ if (!cachedPaths.has(node.path)) node.externalRefsCount = 0;
866
+ byPath2.set(node.path, node);
944
867
  }
945
- return false;
946
- }
947
-
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;
868
+ for (const link of externalLinks) {
869
+ const source = byPath2.get(link.source);
870
+ if (source && !cachedPaths.has(source.path)) source.externalRefsCount += 1;
957
871
  }
958
- const read = provider.read ?? DEFAULT_READ_CONFIG;
959
- return (roots, options) => {
960
- const walkOptions = {
961
- extensions: read.extensions,
962
- parser: read.parser
963
- };
964
- if (options?.ignoreFilter) walkOptions.ignoreFilter = options.ignoreFilter;
965
- return walkContent(roots, walkOptions);
966
- };
872
+ }
873
+ var EXTERNAL_URL_SCHEME_RE = /^[a-z][a-z0-9+\-.]+:/i;
874
+ function isExternalUrlLink(link) {
875
+ return EXTERNAL_URL_SCHEME_RE.test(link.target);
967
876
  }
968
877
 
969
- // kernel/extensions/hook-dispatcher.ts
970
- function makeHookDispatcher(hooks, emitter) {
971
- if (hooks.length === 0) {
972
- return { dispatch: async () => {
973
- } };
974
- }
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
- }
990
- }
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
- }
1016
- function makeEvent(type, data) {
1017
- return { type, timestamp: (/* @__PURE__ */ new Date()).toISOString(), data };
1018
- }
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;
1024
- }
1025
- return true;
1026
- }
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"];
1043
- }
1044
- if (data["jobResult"] !== void 0) ctx.jobResult = data["jobResult"];
1045
- return ctx;
1046
- }
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";
1059
- }
1060
- }
1061
- async function runScanWithRenames(_kernel, options) {
1062
- return runScanInternal(_kernel, options);
1063
- }
1064
- async function runScan(_kernel, options) {
1065
- const { result } = await runScanInternal(_kernel, options);
1066
- return result;
1067
- }
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);
1109
- }
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
1123
- );
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}`);
1130
- }
1131
- }
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
- };
1169
- }
1170
- function validateRoots(roots) {
1171
- if (roots.length === 0) {
1172
- throw new Error(ORCHESTRATOR_TEXTS.runScanRootEmptyArray);
1173
- }
1174
- for (const root of roots) {
1175
- if (!existsSync8(root) || !statSync2(root).isDirectory()) {
1176
- throw new Error(tx(ORCHESTRATOR_TEXTS.runScanRootMissing, { root }));
1177
- }
1178
- }
1179
- }
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
- }
1188
- for (const node of prior.nodes) {
1189
- priorNodesByPath.set(node.path, node);
1190
- priorNodePaths.add(node.path);
1191
- }
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]);
1197
- }
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]);
1205
- }
1206
- return { priorNodesByPath, priorNodePaths, priorLinksByOriginating, priorFrontmatterIssuesByNode };
1207
- }
1208
- async function runExtractorsForNode(opts) {
1209
- const internalLinks = [];
1210
- const externalLinks = [];
1211
- const enrichmentBuffer = /* @__PURE__ */ new Map();
878
+ // kernel/orchestrator/analyzers.ts
879
+ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher) {
880
+ const issues = [];
1212
881
  const contributions = [];
1213
882
  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
- });
1239
- }
1240
- };
1241
- const declaredContributions = readDeclaredContributions(extractor);
1242
- const emitContribution = (contributionId, payload) => {
883
+ validateRecommendedActions(analyzers, registeredActionIds, emitter);
884
+ const analyzerOrphans = orphanSidecars.map((o) => ({
885
+ relativePath: o.relativePath,
886
+ expectedMdPath: o.expectedMdPath
887
+ }));
888
+ for (const analyzer of analyzers) {
889
+ const qualifiedId = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
890
+ const declaredContributions = readDeclaredContributions(analyzer);
891
+ const emitContribution = (nodePath, contributionId, payload) => {
1243
892
  const declared = declaredContributions.get(contributionId);
1244
893
  if (!declared) {
1245
- emitExtensionError(opts.emitter, qualifiedId, opts.node.path, {
894
+ emitExtensionError(emitter, qualifiedId, nodePath, {
1246
895
  phase: "emitContribution",
1247
896
  contributionId,
1248
897
  reason: "unknown-contribution-id",
1249
898
  message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {
1250
899
  extractorId: qualifiedId,
1251
900
  contributionId,
1252
- nodePath: opts.node.path
901
+ nodePath
1253
902
  })
1254
903
  });
1255
904
  return;
1256
905
  }
1257
906
  const result = validators.validateContributionPayload(declared.slot, payload);
1258
907
  if (!result.ok) {
1259
- emitExtensionError(opts.emitter, qualifiedId, opts.node.path, {
908
+ emitExtensionError(emitter, qualifiedId, nodePath, {
1260
909
  phase: "emitContribution",
1261
910
  contributionId,
1262
911
  slot: declared.slot,
@@ -1264,7 +913,7 @@ async function runExtractorsForNode(opts) {
1264
913
  message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {
1265
914
  extractorId: qualifiedId,
1266
915
  contributionId,
1267
- nodePath: opts.node.path,
916
+ nodePath,
1268
917
  slot: declared.slot,
1269
918
  errors: result.errors
1270
919
  })
@@ -1272,56 +921,133 @@ async function runExtractorsForNode(opts) {
1272
921
  return;
1273
922
  }
1274
923
  contributions.push({
1275
- pluginId: extractor.pluginId,
1276
- extensionId: extractor.id,
1277
- nodePath: opts.node.path,
924
+ pluginId: analyzer.pluginId,
925
+ extensionId: analyzer.id,
926
+ nodePath,
1278
927
  contributionId,
1279
928
  slot: declared.slot,
1280
929
  payload,
1281
930
  emittedAt: Date.now()
1282
931
  });
1283
932
  };
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
933
+ const emitted = await analyzer.evaluate({
934
+ nodes,
935
+ links: internalLinks,
936
+ orphanSidecars: analyzerOrphans,
937
+ sidecarRoots,
938
+ annotationContributions,
939
+ viewContributions,
940
+ orphanJobFiles,
941
+ ...referenceablePaths ? { referenceablePaths } : {},
942
+ ...cwd ? { cwd } : {},
943
+ emitContribution
944
+ });
945
+ for (const issue of emitted) {
946
+ const validated = validateIssue(analyzer, issue, emitter);
947
+ if (validated) issues.push(validated);
948
+ }
949
+ const evt = makeEvent("analyzer.completed", { analyzerId: qualifiedId });
950
+ emitter.emit(evt);
951
+ await hookDispatcher.dispatch("analyzer.completed", evt);
952
+ }
953
+ return { issues, contributions };
954
+ }
955
+ function validateRecommendedActions(analyzers, registeredActionIds, emitter) {
956
+ for (const analyzer of analyzers) {
957
+ const refs = analyzer.recommendedActions;
958
+ if (refs === void 0 || refs.length === 0) continue;
959
+ const analyzerId = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
960
+ for (const actionId of refs) {
961
+ if (registeredActionIds.has(actionId)) continue;
962
+ emitter.emit(
963
+ makeEvent("extension.error", {
964
+ kind: "recommended-action-missing",
965
+ extensionId: analyzerId,
966
+ actionId,
967
+ message: tx(ORCHESTRATOR_TEXTS.extensionErrorRecommendedActionMissing, {
968
+ analyzerId,
969
+ actionId
970
+ })
971
+ })
972
+ );
973
+ }
974
+ }
975
+ }
976
+ function validateIssue(analyzer, issue, emitter) {
977
+ const severity = issue.severity;
978
+ if (severity !== "error" && severity !== "warn" && severity !== "info") {
979
+ const qualifiedId = `${analyzer.pluginId}/${analyzer.id}`;
980
+ emitter.emit(
981
+ makeEvent("extension.error", {
982
+ kind: "issue-invalid-severity",
983
+ extensionId: qualifiedId,
984
+ severity,
985
+ issue: { analyzerId: issue.analyzerId || analyzer.id, message: issue.message, nodeIds: issue.nodeIds },
986
+ message: tx(ORCHESTRATOR_TEXTS.extensionErrorIssueInvalidSeverity, {
987
+ analyzerId: qualifiedId,
988
+ severity: JSON.stringify(severity)
989
+ })
990
+ })
1294
991
  );
1295
- await extractor.extract(ctx);
992
+ return null;
1296
993
  }
1297
- return {
1298
- internalLinks,
1299
- externalLinks,
1300
- enrichments: Array.from(enrichmentBuffer.values()),
1301
- contributions
1302
- };
994
+ return { ...issue, analyzerId: issue.analyzerId || analyzer.id };
1303
995
  }
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 });
996
+
997
+ // kernel/orchestrator/cache.ts
998
+ function indexPriorSnapshot(prior) {
999
+ const priorNodesByPath = /* @__PURE__ */ new Map();
1000
+ const priorNodePaths = /* @__PURE__ */ new Set();
1001
+ const priorLinksByOriginating = /* @__PURE__ */ new Map();
1002
+ const priorFrontmatterIssuesByNode = /* @__PURE__ */ new Map();
1003
+ if (!prior) {
1004
+ return { priorNodesByPath, priorNodePaths, priorLinksByOriginating, priorFrontmatterIssuesByNode };
1313
1005
  }
1314
- return out;
1006
+ indexPriorNodes(prior.nodes, priorNodesByPath, priorNodePaths);
1007
+ indexPriorLinks(prior.links, priorNodePaths, priorLinksByOriginating);
1008
+ indexPriorFrontmatterIssues(prior.issues, priorFrontmatterIssuesByNode);
1009
+ return { priorNodesByPath, priorNodePaths, priorLinksByOriginating, priorFrontmatterIssuesByNode };
1315
1010
  }
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
- );
1011
+ function indexPriorNodes(nodes, byPath2, paths) {
1012
+ for (const node of nodes) {
1013
+ byPath2.set(node.path, node);
1014
+ paths.add(node.path);
1015
+ }
1016
+ }
1017
+ function indexPriorLinks(links, priorNodePaths, byOriginating) {
1018
+ for (const link of links) {
1019
+ const key = originatingNodeOf(link, priorNodePaths);
1020
+ const list = byOriginating.get(key);
1021
+ if (list) list.push(link);
1022
+ else byOriginating.set(key, [link]);
1023
+ }
1024
+ }
1025
+ var FRONTMATTER_ISSUE_ANALYZERS = /* @__PURE__ */ new Set([
1026
+ "frontmatter-invalid",
1027
+ "frontmatter-malformed",
1028
+ // Audit L1: parser parse-error is emitted by
1029
+ // `buildFreshNodeAndValidateFrontmatter` from `raw.parseIssues`. The
1030
+ // raw.parseIssues only flows through the non-cache path; a cached
1031
+ // node skips the rebuild, so the prior issue MUST survive the
1032
+ // incremental scan or the warning silently disappears on a clean
1033
+ // re-scan of an unchanged file.
1034
+ "frontmatter-parse-error"
1035
+ ]);
1036
+ function indexPriorFrontmatterIssues(issues, byNode) {
1037
+ for (const issue of issues) {
1038
+ if (!FRONTMATTER_ISSUE_ANALYZERS.has(issue.analyzerId)) continue;
1039
+ if (issue.nodeIds.length !== 1) continue;
1040
+ const path = issue.nodeIds[0];
1041
+ const list = byNode.get(path);
1042
+ if (list) list.push(issue);
1043
+ else byNode.set(path, [issue]);
1044
+ }
1045
+ }
1046
+ function originatingNodeOf(link, priorNodePaths) {
1047
+ if (link.kind === "supersedes" && !priorNodePaths.has(link.source)) {
1048
+ return link.target;
1049
+ }
1050
+ return link.source;
1325
1051
  }
1326
1052
  function computeCacheDecision(opts) {
1327
1053
  const applicableExtractors = opts.extractors.filter(
@@ -1330,35 +1056,39 @@ function computeCacheDecision(opts) {
1330
1056
  const applicableQualifiedIds = new Set(
1331
1057
  applicableExtractors.map((ex) => qualifiedExtensionId(ex.pluginId, ex.id))
1332
1058
  );
1059
+ const split = opts.priorExtractorRuns === void 0 ? splitLegacy(applicableExtractors, applicableQualifiedIds, opts.nodeHashCacheEligible) : splitFineGrained(applicableExtractors, opts);
1060
+ return {
1061
+ applicableExtractors,
1062
+ applicableQualifiedIds,
1063
+ cachedQualifiedIds: split.cachedQualifiedIds,
1064
+ missingExtractors: split.missingExtractors,
1065
+ fullCacheHit: opts.nodeHashCacheEligible && split.missingExtractors.length === 0
1066
+ };
1067
+ }
1068
+ function splitLegacy(applicableExtractors, applicableQualifiedIds, nodeHashCacheEligible) {
1333
1069
  const cachedQualifiedIds = /* @__PURE__ */ new Set();
1334
1070
  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
- }
1071
+ if (nodeHashCacheEligible) {
1072
+ for (const id of applicableQualifiedIds) cachedQualifiedIds.add(id);
1341
1073
  } 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);
1352
- }
1074
+ for (const ex of applicableExtractors) missingExtractors.push(ex);
1075
+ }
1076
+ return { cachedQualifiedIds, missingExtractors };
1077
+ }
1078
+ function splitFineGrained(applicableExtractors, opts) {
1079
+ const cachedQualifiedIds = /* @__PURE__ */ new Set();
1080
+ const missingExtractors = [];
1081
+ const priorRunsForNode = opts.priorExtractorRuns.get(opts.nodePath) ?? /* @__PURE__ */ new Map();
1082
+ for (const ex of applicableExtractors) {
1083
+ const qualified = qualifiedExtensionId(ex.pluginId, ex.id);
1084
+ const prior = priorRunsForNode.get(qualified);
1085
+ if (opts.nodeHashCacheEligible && prior !== void 0 && prior.bodyHash === opts.bodyHash && prior.sidecarAnnotationsHash === opts.sidecarAnnotationsHash) {
1086
+ cachedQualifiedIds.add(qualified);
1087
+ } else {
1088
+ missingExtractors.push(ex);
1353
1089
  }
1354
1090
  }
1355
- return {
1356
- applicableExtractors,
1357
- applicableQualifiedIds,
1358
- cachedQualifiedIds,
1359
- missingExtractors,
1360
- fullCacheHit: opts.nodeHashCacheEligible && missingExtractors.length === 0
1361
- };
1091
+ return { cachedQualifiedIds, missingExtractors };
1362
1092
  }
1363
1093
  function cloneNodeAndReshapeLinks(opts) {
1364
1094
  const node = { ...opts.priorNode, bytes: { ...opts.priorNode.bytes } };
@@ -1396,467 +1126,645 @@ function reusePriorNode(opts) {
1396
1126
  }
1397
1127
  return { ...base, extractorRuns };
1398
1128
  }
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
1129
+ function reuseCachedLink(link, shortIdToQualified, cachedQualifiedIds, applicableQualifiedIds) {
1130
+ if (!Array.isArray(link.sources) || link.sources.length === 0) return null;
1131
+ const partition = partitionLinkSources(
1132
+ link.sources,
1133
+ shortIdToQualified,
1134
+ cachedQualifiedIds,
1135
+ applicableQualifiedIds
1136
+ );
1137
+ if (partition.hasMissing) return null;
1138
+ if (partition.cached.length === 0) return null;
1139
+ if (partition.obsolete.length === 0) return link;
1140
+ return { ...link, sources: partition.cached };
1141
+ }
1142
+ function partitionLinkSources(sources, shortIdToQualified, cachedQualifiedIds, applicableQualifiedIds) {
1143
+ const cached = [];
1144
+ const obsolete = [];
1145
+ let hasMissing = false;
1146
+ for (const source of sources) {
1147
+ const category = classifyLinkSource(
1148
+ source,
1149
+ shortIdToQualified,
1150
+ cachedQualifiedIds,
1151
+ applicableQualifiedIds
1420
1152
  );
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);
1153
+ if (category === "cached") cached.push(source);
1154
+ else if (category === "missing") hasMissing = true;
1155
+ else obsolete.push(source);
1425
1156
  }
1426
- return { node, frontmatterIssues };
1157
+ return { cached, obsolete, hasMissing };
1427
1158
  }
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]);
1464
- }
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
- });
1159
+ function classifyLinkSource(source, shortIdToQualified, cachedQualifiedIds, applicableQualifiedIds) {
1160
+ const candidates = shortIdToQualified.get(source);
1161
+ if (!candidates || candidates.length === 0) return "obsolete";
1162
+ if (candidates.some((q) => cachedQualifiedIds.has(q))) return "cached";
1163
+ if (candidates.some((q) => applicableQualifiedIds.has(q))) return "missing";
1164
+ return "obsolete";
1165
+ }
1166
+
1167
+ // kernel/orchestrator/renames.ts
1168
+ function findHighConfidenceRenames(opts) {
1169
+ const ops = [];
1170
+ for (const fromPath of opts.deletedPaths) {
1171
+ if (opts.claimedDeleted.has(fromPath)) continue;
1172
+ const fromNode = opts.priorByPath.get(fromPath);
1173
+ for (const toPath of opts.newPaths) {
1174
+ if (opts.claimedNew.has(toPath)) continue;
1175
+ const toNode = opts.currentByPath.get(toPath);
1176
+ if (toNode.bodyHash === fromNode.bodyHash) {
1177
+ ops.push({ from: fromPath, to: toPath, confidence: "high" });
1178
+ opts.claimedDeleted.add(fromPath);
1179
+ opts.claimedNew.add(toPath);
1180
+ break;
1606
1181
  }
1607
1182
  }
1608
1183
  }
1609
- const orphanSidecars = discoverOrphanSidecars(roots);
1610
- 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
1623
- };
1184
+ return ops;
1624
1185
  }
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;
1186
+ function buildFrontmatterRenameCandidates(opts) {
1187
+ const candidatesByNew = /* @__PURE__ */ new Map();
1188
+ for (const toPath of opts.newPaths) {
1189
+ if (opts.claimedNew.has(toPath)) continue;
1190
+ const toNode = opts.currentByPath.get(toPath);
1191
+ const matches = [];
1192
+ for (const fromPath of opts.deletedPaths) {
1193
+ if (opts.claimedDeleted.has(fromPath)) continue;
1194
+ const fromNode = opts.priorByPath.get(fromPath);
1195
+ if (toNode.frontmatterHash === fromNode.frontmatterHash) {
1196
+ matches.push(fromPath);
1197
+ }
1643
1198
  }
1644
- obsoleteSources.push(source);
1199
+ if (matches.length > 0) candidatesByNew.set(toPath, matches);
1645
1200
  }
1646
- if (hasMissing) return null;
1647
- if (cachedSources.length === 0) return null;
1648
- if (obsoleteSources.length === 0) return link;
1649
- return { ...link, sources: cachedSources };
1201
+ return candidatesByNew;
1650
1202
  }
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
1203
+ function claimSingletonRenames(opts) {
1204
+ const ops = [];
1205
+ for (const toPath of opts.newPaths) {
1206
+ if (opts.claimedNew.has(toPath)) continue;
1207
+ const candidates = opts.candidatesByNew.get(toPath);
1208
+ if (!candidates) continue;
1209
+ const remaining = candidates.filter((p) => !opts.claimedDeleted.has(p));
1210
+ if (remaining.length === 1) {
1211
+ const fromPath = remaining[0];
1212
+ ops.push({ from: fromPath, to: toPath, confidence: "medium" });
1213
+ opts.issues.push({
1214
+ analyzerId: "auto-rename-medium",
1215
+ severity: "warn",
1216
+ nodeIds: [toPath],
1217
+ message: `Auto-rename (medium confidence): ${fromPath} \u2192 ${toPath}`,
1218
+ data: { from: fromPath, to: toPath, confidence: "medium" }
1219
+ });
1220
+ opts.claimedDeleted.add(fromPath);
1221
+ opts.claimedNew.add(toPath);
1222
+ }
1223
+ }
1224
+ return ops;
1225
+ }
1226
+ function flagAmbiguousRenames(opts) {
1227
+ for (const toPath of opts.newPaths) {
1228
+ if (opts.claimedNew.has(toPath)) continue;
1229
+ const candidates = opts.candidatesByNew.get(toPath);
1230
+ if (!candidates) continue;
1231
+ const remaining = candidates.filter((p) => !opts.claimedDeleted.has(p));
1232
+ if (remaining.length > 1) {
1233
+ opts.issues.push({
1234
+ analyzerId: "auto-rename-ambiguous",
1235
+ severity: "warn",
1236
+ nodeIds: [toPath],
1237
+ message: `Auto-rename ambiguous: ${toPath} matches ${remaining.length} prior frontmatters; pick one with \`sm orphans undo-rename ${toPath} --from <old.path>\`.`,
1238
+ data: { to: toPath, candidates: remaining }
1239
+ });
1240
+ }
1241
+ }
1242
+ }
1243
+ function flagOrphans(opts) {
1244
+ for (const fromPath of opts.deletedPaths) {
1245
+ if (opts.claimedDeleted.has(fromPath)) continue;
1246
+ opts.issues.push({
1247
+ analyzerId: "orphan",
1248
+ severity: "info",
1249
+ nodeIds: [fromPath],
1250
+ message: `Orphan history: ${fromPath} was deleted; no rename match found.`,
1251
+ data: { path: fromPath }
1252
+ });
1253
+ }
1254
+ }
1255
+ function detectRenamesAndOrphans(prior, current, issues) {
1256
+ const priorByPath = /* @__PURE__ */ new Map();
1257
+ for (const n of prior.nodes) priorByPath.set(n.path, n);
1258
+ const currentByPath = /* @__PURE__ */ new Map();
1259
+ for (const n of current) currentByPath.set(n.path, n);
1260
+ const deletedPaths = [...priorByPath.keys()].filter((p) => !currentByPath.has(p)).sort();
1261
+ const newPaths = [...currentByPath.keys()].filter((p) => !priorByPath.has(p)).sort();
1262
+ const claimedDeleted = /* @__PURE__ */ new Set();
1263
+ const claimedNew = /* @__PURE__ */ new Set();
1264
+ const ops = [];
1265
+ ops.push(...findHighConfidenceRenames({
1266
+ deletedPaths,
1267
+ newPaths,
1268
+ priorByPath,
1269
+ currentByPath,
1270
+ claimedDeleted,
1271
+ claimedNew
1658
1272
  }));
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;
1273
+ const candidatesByNew = buildFrontmatterRenameCandidates({
1274
+ deletedPaths,
1275
+ newPaths,
1276
+ priorByPath,
1277
+ currentByPath,
1278
+ claimedDeleted,
1279
+ claimedNew
1280
+ });
1281
+ ops.push(...claimSingletonRenames({
1282
+ newPaths,
1283
+ candidatesByNew,
1284
+ claimedDeleted,
1285
+ claimedNew,
1286
+ issues
1287
+ }));
1288
+ flagAmbiguousRenames({ newPaths, candidatesByNew, claimedDeleted, claimedNew, issues });
1289
+ flagOrphans({ deletedPaths, claimedDeleted, issues });
1290
+ return ops;
1291
+ }
1292
+
1293
+ // kernel/scan/walk-content.ts
1294
+ import { readFile, readdir, lstat } from "fs/promises";
1295
+ import { join as join2, relative as relative2, sep } from "path";
1296
+
1297
+ // kernel/scan/ignore.ts
1298
+ import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
1299
+ import { dirname as dirname2, resolve as resolve5 } from "path";
1300
+ import { fileURLToPath } from "url";
1301
+ import ignoreFactory from "ignore";
1302
+ function buildIgnoreFilter(opts = {}) {
1303
+ const ig = ignoreFactory();
1304
+ if (opts.includeDefaults !== false) {
1305
+ ig.add(loadDefaultsText());
1306
+ }
1307
+ if (opts.configIgnore && opts.configIgnore.length > 0) {
1308
+ ig.add(opts.configIgnore);
1309
+ }
1310
+ if (opts.ignoreFileText && opts.ignoreFileText.length > 0) {
1311
+ ig.add(opts.ignoreFileText);
1312
+ }
1313
+ return {
1314
+ ignores(relativePath) {
1315
+ if (relativePath === "" || relativePath === "." || relativePath === "./") {
1316
+ return false;
1676
1317
  }
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;
1318
+ const normalised = relativePath.replace(/^\.\//, "").replace(/\\/g, "/").replace(/^\//, "");
1319
+ if (normalised === "") return false;
1320
+ return ig.ignores(normalised);
1321
+ }
1322
+ };
1323
+ }
1324
+ var cachedDefaults = null;
1325
+ function loadDefaultsText() {
1326
+ if (cachedDefaults !== null) return cachedDefaults;
1327
+ cachedDefaults = readDefaultsFromDisk();
1328
+ return cachedDefaults;
1329
+ }
1330
+ function readDefaultsFromDisk() {
1331
+ const here = dirname2(fileURLToPath(import.meta.url));
1332
+ const candidates = [
1333
+ resolve5(here, "../../config/defaults/skillmapignore"),
1334
+ // src/kernel/scan/ → src/config/defaults/
1335
+ resolve5(here, "../config/defaults/skillmapignore"),
1336
+ // dist/cli.js → dist/config/defaults/ (siblings)
1337
+ resolve5(here, "config/defaults/skillmapignore")
1338
+ ];
1339
+ for (const candidate of candidates) {
1340
+ if (existsSync2(candidate)) {
1341
+ try {
1342
+ return readFileSync4(candidate, "utf8");
1343
+ } catch {
1693
1344
  }
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()
1345
+ }
1346
+ }
1347
+ return "";
1348
+ }
1349
+
1350
+ // built-in-plugins/parsers/frontmatter-yaml/index.ts
1351
+ import yaml from "js-yaml";
1352
+
1353
+ // kernel/util/strip-prototype-pollution.ts
1354
+ var FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
1355
+ "__proto__",
1356
+ "constructor",
1357
+ "prototype"
1358
+ ]);
1359
+ function stripPrototypePollution(value) {
1360
+ return strip(value);
1361
+ }
1362
+ function strip(value) {
1363
+ if (value === null || value === void 0) return value;
1364
+ if (typeof value !== "object") return value;
1365
+ if (Array.isArray(value)) return value.map(strip);
1366
+ const out = {};
1367
+ for (const [k, v] of Object.entries(value)) {
1368
+ if (FORBIDDEN_KEYS.has(k)) continue;
1369
+ out[k] = strip(v);
1370
+ }
1371
+ return out;
1372
+ }
1373
+
1374
+ // built-in-plugins/parsers/frontmatter-yaml/index.ts
1375
+ var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
1376
+ var frontmatterYamlParser = {
1377
+ id: "frontmatter-yaml",
1378
+ parse(raw, _path) {
1379
+ const match = FRONTMATTER_RE.exec(raw);
1380
+ if (!match) return { frontmatterRaw: "", frontmatter: {}, body: raw };
1381
+ const frontmatterRaw = match[1];
1382
+ const body = match[2];
1383
+ let parsed = {};
1384
+ const issues = [];
1385
+ try {
1386
+ const doc = yaml.load(frontmatterRaw, { schema: yaml.JSON_SCHEMA });
1387
+ if (doc && typeof doc === "object" && !Array.isArray(doc)) {
1388
+ parsed = stripPrototypePollution(doc);
1389
+ }
1390
+ } catch (err) {
1391
+ issues.push({
1392
+ code: "frontmatter-parse-error",
1393
+ message: sanitiseParseErrorMessage(err)
1702
1394
  });
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
1395
  }
1720
- const evt = makeEvent("analyzer.completed", { analyzerId: qualifiedId });
1721
- emitter.emit(evt);
1722
- await hookDispatcher.dispatch("analyzer.completed", evt);
1396
+ const out = { frontmatterRaw, frontmatter: parsed, body };
1397
+ if (issues.length > 0) {
1398
+ return { ...out, issues };
1399
+ }
1400
+ return out;
1723
1401
  }
1724
- return { issues, contributions };
1402
+ };
1403
+ function sanitiseParseErrorMessage(err) {
1404
+ const raw = err instanceof Error ? err.message : String(err);
1405
+ return raw.replace(/[-]+/g, " ").replace(/\s+/g, " ").trim();
1725
1406
  }
1726
- function originatingNodeOf(link, priorNodePaths) {
1727
- if (link.kind === "supersedes" && !priorNodePaths.has(link.source)) {
1728
- return link.target;
1407
+
1408
+ // built-in-plugins/parsers/plain/index.ts
1409
+ var plainParser = {
1410
+ id: "plain",
1411
+ parse(raw, _path) {
1412
+ return { frontmatter: {}, frontmatterRaw: "", body: raw };
1729
1413
  }
1730
- return link.source;
1414
+ };
1415
+
1416
+ // kernel/scan/parsers/index.ts
1417
+ var REGISTRY = /* @__PURE__ */ new Map([
1418
+ [frontmatterYamlParser.id, frontmatterYamlParser],
1419
+ [plainParser.id, plainParser]
1420
+ ]);
1421
+ var FROZEN_IDS = new Set(REGISTRY.keys());
1422
+ function getParser(id) {
1423
+ return REGISTRY.get(id);
1731
1424
  }
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;
1425
+
1426
+ // kernel/scan/walk-content.ts
1427
+ var UnknownParserError = class extends Error {
1428
+ constructor(parserId) {
1429
+ super(`Unknown parser id '${parserId}'. Built-in parsers: 'frontmatter-yaml', 'plain'.`);
1430
+ this.name = "UnknownParserError";
1431
+ }
1432
+ };
1433
+ async function* walkContent(roots, options) {
1434
+ const parser = getParser(options.parser);
1435
+ if (!parser) throw new UnknownParserError(options.parser);
1436
+ const filter = options.ignoreFilter ?? buildIgnoreFilter();
1437
+ const extensions = options.extensions;
1438
+ for (const root of roots) {
1439
+ for await (const file of walkRoot(root, root, filter, extensions)) {
1440
+ const relPath = relative2(root, file).split(sep).join("/");
1441
+ let raw;
1442
+ try {
1443
+ raw = await readFile(file, "utf8");
1444
+ } catch {
1445
+ continue;
1446
+ }
1447
+ const parsed = parser.parse(raw, relPath);
1448
+ yield {
1449
+ path: relPath,
1450
+ body: parsed.body,
1451
+ frontmatterRaw: parsed.frontmatterRaw,
1452
+ frontmatter: parsed.frontmatter,
1453
+ // Audit L1: forward parser diagnostics (e.g. malformed YAML)
1454
+ // through the IRawNode surface so the orchestrator can
1455
+ // convert them into warn-level kernel `Issue` rows. Omitted
1456
+ // when the parser reported no issues (happy path).
1457
+ ...parsed.issues && parsed.issues.length > 0 ? { parseIssues: parsed.issues } : {}
1458
+ };
1459
+ }
1460
+ }
1461
+ }
1462
+ async function* walkRoot(root, current, filter, extensions) {
1463
+ let entries;
1464
+ try {
1465
+ entries = await readdir(current, { withFileTypes: true, encoding: "utf8" });
1466
+ } catch {
1467
+ return;
1468
+ }
1469
+ for (const entry of entries) {
1470
+ const name = entry.name;
1471
+ const full = join2(current, name);
1472
+ const rel = relative2(root, full).split(sep).join("/");
1473
+ if (filter.ignores(rel)) continue;
1474
+ if (entry.isSymbolicLink()) continue;
1475
+ if (entry.isDirectory()) {
1476
+ yield* walkRoot(root, full, filter, extensions);
1477
+ } else if (entry.isFile() && hasMatchingExtension(name, extensions)) {
1478
+ try {
1479
+ const s = await lstat(full);
1480
+ if (s.isFile()) yield full;
1481
+ } catch {
1745
1482
  }
1746
1483
  }
1747
1484
  }
1748
- return ops;
1749
1485
  }
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);
1486
+ function hasMatchingExtension(name, extensions) {
1487
+ for (const ext of extensions) {
1488
+ if (name.endsWith(ext)) return true;
1489
+ }
1490
+ return false;
1491
+ }
1492
+
1493
+ // kernel/extensions/provider.ts
1494
+ var DEFAULT_READ_CONFIG = Object.freeze({
1495
+ extensions: Object.freeze([".md"]),
1496
+ parser: "frontmatter-yaml"
1497
+ });
1498
+ function resolveProviderWalk(provider) {
1499
+ if (provider.walk) {
1500
+ const walk2 = provider.walk.bind(provider);
1501
+ return walk2;
1502
+ }
1503
+ const read = provider.read ?? DEFAULT_READ_CONFIG;
1504
+ return (roots, options) => {
1505
+ const walkOptions = {
1506
+ extensions: read.extensions,
1507
+ parser: read.parser
1508
+ };
1509
+ if (options?.ignoreFilter) walkOptions.ignoreFilter = options.ignoreFilter;
1510
+ return walkContent(roots, walkOptions);
1511
+ };
1512
+ }
1513
+
1514
+ // kernel/sidecar/parse.ts
1515
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
1516
+ import { dirname as dirname3, resolve as resolve6 } from "path";
1517
+ import { createRequire as createRequire3 } from "module";
1518
+ import { Ajv2020 as Ajv20204 } from "ajv/dist/2020.js";
1519
+ import yaml2 from "js-yaml";
1520
+ function readSidecarFor(mdAbsolutePath) {
1521
+ const sidecarPath = sidecarPathFor(mdAbsolutePath);
1522
+ if (!existsSync3(sidecarPath)) {
1523
+ return { parsed: null, present: false, issues: [] };
1524
+ }
1525
+ let raw;
1526
+ try {
1527
+ raw = readFileSync5(sidecarPath, "utf8");
1528
+ } catch (err) {
1529
+ return {
1530
+ parsed: null,
1531
+ present: true,
1532
+ issues: [{ message: `cannot read ${sidecarPath}: ${err.message}` }]
1533
+ };
1534
+ }
1535
+ let parsedYaml;
1536
+ try {
1537
+ parsedYaml = yaml2.load(raw);
1538
+ } catch (err) {
1539
+ return {
1540
+ parsed: null,
1541
+ present: true,
1542
+ issues: [{ message: `malformed YAML in ${sidecarPath}: ${err.message}` }]
1543
+ };
1544
+ }
1545
+ parsedYaml = stripPrototypePollution(parsedYaml);
1546
+ if (!isPlainObject(parsedYaml)) {
1547
+ return {
1548
+ parsed: null,
1549
+ present: true,
1550
+ issues: [{ message: `sidecar root must be a YAML mapping at ${sidecarPath}` }]
1551
+ };
1552
+ }
1553
+ const sidecarValidator = getSidecarValidator();
1554
+ if (!sidecarValidator(parsedYaml)) {
1555
+ const errors = (sidecarValidator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
1556
+ return {
1557
+ parsed: null,
1558
+ present: true,
1559
+ issues: [{ message: `sidecar schema validation failed at ${sidecarPath}: ${errors}` }]
1560
+ };
1561
+ }
1562
+ const root = parsedYaml;
1563
+ const identityBlock = root["identity"];
1564
+ const annotationsRaw = root["annotations"];
1565
+ const annotations = isPlainObject(annotationsRaw) ? Object.keys(annotationsRaw).length === 0 ? null : annotationsRaw : null;
1566
+ return {
1567
+ parsed: {
1568
+ filePath: sidecarPath,
1569
+ identityBodyHash: String(identityBlock["bodyHash"]),
1570
+ identityFrontmatterHash: String(identityBlock["frontmatterHash"]),
1571
+ identityPath: String(identityBlock["path"]),
1572
+ annotations,
1573
+ raw: root
1574
+ },
1575
+ present: true,
1576
+ issues: []
1577
+ };
1578
+ }
1579
+ function sidecarPathFor(mdAbsolutePath) {
1580
+ if (mdAbsolutePath.endsWith(".md")) {
1581
+ return `${mdAbsolutePath.slice(0, -".md".length)}.sm`;
1582
+ }
1583
+ return `${mdAbsolutePath}.sm`;
1584
+ }
1585
+ function isPlainObject(value) {
1586
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1587
+ }
1588
+ var cachedSidecarValidator = null;
1589
+ function getSidecarValidator() {
1590
+ if (cachedSidecarValidator) return cachedSidecarValidator;
1591
+ const ajv = new Ajv20204({ strict: false, allErrors: true, allowUnionTypes: true });
1592
+ applyAjvFormats(ajv);
1593
+ const specRoot = resolveSpecRoot2();
1594
+ const annotationsSchema = JSON.parse(
1595
+ readFileSync5(resolve6(specRoot, "schemas/annotations.schema.json"), "utf8")
1596
+ );
1597
+ const sidecarSchema = JSON.parse(
1598
+ readFileSync5(resolve6(specRoot, "schemas/sidecar.schema.json"), "utf8")
1599
+ );
1600
+ ajv.addSchema(annotationsSchema);
1601
+ cachedSidecarValidator = ajv.compile(sidecarSchema);
1602
+ return cachedSidecarValidator;
1603
+ }
1604
+ function resolveSpecRoot2() {
1605
+ const require2 = createRequire3(import.meta.url);
1606
+ try {
1607
+ const indexPath = require2.resolve("@skill-map/spec/index.json");
1608
+ return dirname3(indexPath);
1609
+ } catch {
1610
+ throw new Error(
1611
+ "@skill-map/spec not resolvable: sidecar reader cannot load schemas."
1612
+ );
1613
+ }
1614
+ }
1615
+
1616
+ // kernel/sidecar/drift.ts
1617
+ function computeDriftStatus(args) {
1618
+ const bodyDrift = args.storedBodyHash !== args.liveBodyHash;
1619
+ const fmDrift = args.storedFrontmatterHash !== args.liveFrontmatterHash;
1620
+ if (bodyDrift && fmDrift) return "stale-both";
1621
+ if (bodyDrift) return "stale-body";
1622
+ if (fmDrift) return "stale-frontmatter";
1623
+ return "fresh";
1624
+ }
1625
+
1626
+ // kernel/sidecar/discover-orphans.ts
1627
+ import { existsSync as existsSync4, readdirSync as readdirSync2, statSync } from "fs";
1628
+ import { join as join3, relative as relative3, sep as sep2 } from "path";
1629
+ function discoverOrphanSidecars(roots, shouldSkip) {
1630
+ const out = [];
1631
+ for (const root of roots) {
1632
+ walk(root, root, shouldSkip ?? (() => false), out);
1633
+ }
1634
+ return out;
1635
+ }
1636
+ function walk(root, current, shouldSkip, out) {
1637
+ let entries;
1638
+ try {
1639
+ entries = readdirSync2(current, { withFileTypes: true, encoding: "utf8" });
1640
+ } catch {
1641
+ return;
1642
+ }
1643
+ for (const entry of entries) {
1644
+ const full = join3(current, entry.name);
1645
+ const rel = relative3(root, full).split(sep2).join("/");
1646
+ if (shouldSkip(rel)) continue;
1647
+ if (entry.isSymbolicLink()) continue;
1648
+ if (entry.isDirectory()) {
1649
+ walk(root, full, shouldSkip, out);
1650
+ continue;
1651
+ }
1652
+ if (!entry.isFile()) continue;
1653
+ if (!entry.name.endsWith(".sm")) continue;
1654
+ const expectedMd = `${full.slice(0, -".sm".length)}.md`;
1655
+ if (existsSync4(expectedMd) && safeIsFile(expectedMd)) continue;
1656
+ out.push({ sidecarPath: full, relativePath: rel, expectedMdPath: expectedMd });
1657
+ }
1658
+ }
1659
+ function safeIsFile(path) {
1660
+ try {
1661
+ return statSync(path).isFile();
1662
+ } catch {
1663
+ return false;
1764
1664
  }
1765
- return candidatesByNew;
1766
1665
  }
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);
1666
+
1667
+ // kernel/sidecar/store.ts
1668
+ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
1669
+ import { dirname as dirname5, resolve as resolve9 } from "path";
1670
+ import { createRequire as createRequire4 } from "module";
1671
+ import { Ajv2020 as Ajv20205 } from "ajv/dist/2020.js";
1672
+ import yaml3 from "js-yaml";
1673
+
1674
+ // core/config/atomic-write.ts
1675
+ import {
1676
+ closeSync,
1677
+ constants as fsConstants,
1678
+ existsSync as existsSync5,
1679
+ mkdirSync,
1680
+ openSync,
1681
+ readFileSync as readFileSync6,
1682
+ renameSync,
1683
+ unlinkSync,
1684
+ writeSync
1685
+ } from "fs";
1686
+ import { randomBytes } from "crypto";
1687
+ import { dirname as dirname4 } from "path";
1688
+
1689
+ // core/config/helper.ts
1690
+ import { isAbsolute as isAbsolute2, resolve as resolve8 } from "path";
1691
+
1692
+ // kernel/config/loader.ts
1693
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
1694
+
1695
+ // kernel/util/skill-map-paths.ts
1696
+ import { join as join5 } from "path";
1697
+
1698
+ // core/paths/db-path.ts
1699
+ import { join as join4, resolve as resolve7 } from "path";
1700
+ var SKILL_MAP_DIR = ".skill-map";
1701
+ var DB_FILENAME = "skill-map.db";
1702
+ var LOCAL_SETTINGS_FILENAME = "settings.local.json";
1703
+ var DEFAULT_DB_REL = `${SKILL_MAP_DIR}/${DB_FILENAME}`;
1704
+ var GITIGNORE_ENTRIES = [
1705
+ `${SKILL_MAP_DIR}/${LOCAL_SETTINGS_FILENAME}`,
1706
+ `${SKILL_MAP_DIR}/${DB_FILENAME}`
1707
+ ];
1708
+
1709
+ // kernel/orchestrator/node-build.ts
1710
+ import { createHash } from "crypto";
1711
+ import { existsSync as existsSync8 } from "fs";
1712
+ import { isAbsolute as isAbsolute3, resolve as resolvePath } from "path";
1713
+ import "js-tiktoken/lite";
1714
+ import yaml4 from "js-yaml";
1715
+
1716
+ // kernel/orchestrator/frontmatter.ts
1717
+ function validateFrontmatter(providerFrontmatter, provider, kind, frontmatter, path, strict) {
1718
+ const result = providerFrontmatter.validate(provider, kind, frontmatter);
1719
+ if (result.ok) return null;
1720
+ return {
1721
+ analyzerId: "frontmatter-invalid",
1722
+ severity: strict ? "error" : "warn",
1723
+ nodeIds: [path],
1724
+ message: tx(ORCHESTRATOR_TEXTS.frontmatterInvalid, { path, kind, errors: result.errors }),
1725
+ data: { kind, errors: result.errors }
1726
+ };
1727
+ }
1728
+ function detectMalformedFrontmatter(body, path, strict) {
1729
+ const hint = classifyMalformedFrontmatter(body);
1730
+ if (!hint) return null;
1731
+ return {
1732
+ analyzerId: "frontmatter-malformed",
1733
+ severity: strict ? "error" : "warn",
1734
+ nodeIds: [path],
1735
+ message: malformedMessage(hint, path),
1736
+ data: { hint }
1737
+ };
1738
+ }
1739
+ function classifyMalformedFrontmatter(body) {
1740
+ if (body.startsWith("\uFEFF")) {
1741
+ if (/^---\r?\n[\s\S]*?[A-Za-z0-9_-]+\s*:/.test(body)) {
1742
+ return "byte-order-mark";
1786
1743
  }
1787
1744
  }
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
- });
1745
+ if (/^[ \t]+---\r?\n[ \t]*[A-Za-z0-9_-]+\s*:/.test(body)) {
1746
+ return "paste-with-indent";
1747
+ }
1748
+ if (/^---\r?\n[ \t]*[A-Za-z0-9_-]+\s*:/.test(body)) {
1749
+ const hasCloseFence = /\r?\n---(?:\r?\n|$)/.test(body);
1750
+ if (!hasCloseFence) {
1751
+ return "missing-close";
1804
1752
  }
1805
1753
  }
1754
+ return null;
1806
1755
  }
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
- });
1756
+ function malformedMessage(hint, path) {
1757
+ switch (hint) {
1758
+ case "paste-with-indent":
1759
+ return tx(ORCHESTRATOR_TEXTS.frontmatterMalformedPasteWithIndent, { path });
1760
+ case "byte-order-mark":
1761
+ return tx(ORCHESTRATOR_TEXTS.frontmatterMalformedByteOrderMark, { path });
1762
+ case "missing-close":
1763
+ return tx(ORCHESTRATOR_TEXTS.frontmatterMalformedMissingClose, { path });
1817
1764
  }
1818
1765
  }
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
- }
1766
+
1767
+ // kernel/orchestrator/node-build.ts
1860
1768
  function buildNode(args) {
1861
1769
  const bytesFrontmatter = Buffer.byteLength(args.frontmatterRaw, "utf8");
1862
1770
  const bytesBody = Buffer.byteLength(args.body, "utf8");
@@ -1946,11 +1854,11 @@ function resolveSidecarOverlay(relativePath, nodePathForIssue, roots, liveBodyHa
1946
1854
  liveFrontmatterHash
1947
1855
  });
1948
1856
  return {
1949
- // R15 closure (2026-05-07) surface the full parsed root on the
1857
+ // R15 closure (2026-05-07), surface the full parsed root on the
1950
1858
  // overlay so BFF consumers (UI inspector audit / plugin-contributions
1951
1859
  // / debug panels) can read `for.*`, `audit.*`, `settings.*`, and
1952
1860
  // plugin-namespaced sub-keys without re-reading the file. The
1953
- // `annotations` field above stays it duplicates `root.annotations`
1861
+ // `annotations` field above stays, it duplicates `root.annotations`
1954
1862
  // by design so existing consumers keep working unchanged.
1955
1863
  overlay: {
1956
1864
  present: true,
@@ -1981,157 +1889,460 @@ function relativePathFromRoots(absolutePath, roots) {
1981
1889
  }
1982
1890
  return absolutePath;
1983
1891
  }
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
- })
1892
+ function buildFreshNodeAndValidateFrontmatter(opts) {
1893
+ const node = buildNode({
1894
+ path: opts.raw.path,
1895
+ kind: opts.kind,
1896
+ providerId: opts.provider.id,
1897
+ frontmatterRaw: opts.raw.frontmatterRaw,
1898
+ body: opts.raw.body,
1899
+ frontmatter: opts.raw.frontmatter,
1900
+ bodyHash: opts.bodyHash,
1901
+ frontmatterHash: opts.frontmatterHash,
1902
+ encoder: opts.encoder
1903
+ });
1904
+ const frontmatterIssues = [];
1905
+ if (opts.raw.parseIssues && opts.raw.parseIssues.length > 0) {
1906
+ for (const pi of opts.raw.parseIssues) {
1907
+ frontmatterIssues.push({
1908
+ analyzerId: pi.code,
1909
+ severity: opts.strict ? "error" : "warn",
1910
+ nodeIds: [opts.raw.path],
1911
+ message: pi.message
1912
+ });
1913
+ }
1914
+ }
1915
+ if (opts.raw.frontmatterRaw.length > 0) {
1916
+ const fmIssue = validateFrontmatter(
1917
+ opts.providerFrontmatter,
1918
+ opts.provider,
1919
+ opts.kind,
1920
+ opts.raw.frontmatter,
1921
+ opts.raw.path,
1922
+ opts.strict
2012
1923
  );
2013
- return null;
1924
+ if (fmIssue) frontmatterIssues.push(fmIssue);
1925
+ } else {
1926
+ const malformed = detectMalformedFrontmatter(opts.raw.body, opts.raw.path, opts.strict);
1927
+ if (malformed) frontmatterIssues.push(malformed);
1928
+ }
1929
+ return { node, frontmatterIssues };
1930
+ }
1931
+ function mergeNodeWithEnrichments(node, enrichments, opts = {}) {
1932
+ const includeStale = opts.includeStale === true;
1933
+ const applicable = enrichments.filter((e) => e.nodePath === node.path).filter((e) => includeStale || !e.stale).sort((a, b) => a.enrichedAt - b.enrichedAt);
1934
+ const base = {};
1935
+ assignSafe(base, node.frontmatter ?? {});
1936
+ for (const row of applicable) {
1937
+ assignSafe(base, row.value);
2014
1938
  }
2015
- const confidence = link.confidence ?? extractor.defaultConfidence;
2016
- return { ...link, confidence };
1939
+ return base;
2017
1940
  }
2018
- function validateFrontmatter(providerFrontmatter, provider, kind, frontmatter, path, strict) {
2019
- const result = providerFrontmatter.validate(provider, kind, frontmatter);
2020
- if (result.ok) return null;
1941
+ function assignSafe(target, source) {
1942
+ const safe = stripPrototypePollution(source);
1943
+ for (const [k, v] of Object.entries(safe)) {
1944
+ target[k] = v;
1945
+ }
1946
+ }
1947
+
1948
+ // kernel/orchestrator/walk.ts
1949
+ async function walkAndExtract(opts) {
1950
+ const accum = createWalkAccumulators();
1951
+ const wctx = buildWalkContext(opts);
1952
+ const claimedPaths = /* @__PURE__ */ new Set();
1953
+ const walkOptions = opts.ignoreFilter ? { ignoreFilter: opts.ignoreFilter } : {};
1954
+ let filesWalked = 0;
1955
+ let index = 0;
1956
+ for (const provider of opts.providers) {
1957
+ for await (const raw of resolveProviderWalk(provider)(opts.roots, walkOptions)) {
1958
+ filesWalked += 1;
1959
+ if (claimedPaths.has(raw.path)) continue;
1960
+ const advanced = await processRawNode(raw, provider, wctx, accum, claimedPaths, index + 1);
1961
+ if (advanced) index += 1;
1962
+ }
1963
+ }
1964
+ const orphanSidecars = discoverOrphanSidecars(opts.roots);
2021
1965
  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 }
1966
+ nodes: accum.nodes,
1967
+ internalLinks: accum.internalLinks,
1968
+ externalLinks: accum.externalLinks,
1969
+ cachedPaths: accum.cachedPaths,
1970
+ frontmatterIssues: accum.frontmatterIssues,
1971
+ filesWalked,
1972
+ enrichments: [...accum.enrichmentBuffer.values()],
1973
+ extractorRuns: accum.extractorRuns,
1974
+ contributions: accum.contributionsBuffer,
1975
+ freshlyRunTuples: accum.freshlyRunTuples,
1976
+ orphanSidecars,
1977
+ sidecarRoots: accum.sidecarRoots
2027
1978
  };
2028
1979
  }
2029
- function detectMalformedFrontmatter(body, path, strict) {
2030
- const hint = classifyMalformedFrontmatter(body);
2031
- if (!hint) return null;
1980
+ function createWalkAccumulators() {
2032
1981
  return {
2033
- analyzerId: "frontmatter-malformed",
2034
- severity: strict ? "error" : "warn",
2035
- nodeIds: [path],
2036
- message: malformedMessage(hint, path),
2037
- data: { hint }
1982
+ nodes: [],
1983
+ internalLinks: [],
1984
+ externalLinks: [],
1985
+ cachedPaths: /* @__PURE__ */ new Set(),
1986
+ frontmatterIssues: [],
1987
+ enrichmentBuffer: /* @__PURE__ */ new Map(),
1988
+ contributionsBuffer: [],
1989
+ freshlyRunTuples: /* @__PURE__ */ new Set(),
1990
+ extractorRuns: [],
1991
+ sidecarRoots: /* @__PURE__ */ new Map()
2038
1992
  };
2039
1993
  }
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
- }
1994
+ function buildWalkContext(opts) {
1995
+ const { priorNodesByPath, priorLinksByOriginating, priorFrontmatterIssuesByNode } = opts.priorIndex;
1996
+ const shortIdToQualified = /* @__PURE__ */ new Map();
1997
+ for (const ex of opts.extractors) {
1998
+ const qualified = qualifiedExtensionId(ex.pluginId, ex.id);
1999
+ const list = shortIdToQualified.get(ex.id);
2000
+ if (list) list.push(qualified);
2001
+ else shortIdToQualified.set(ex.id, [qualified]);
2045
2002
  }
2046
- if (/^[ \t]+---\r?\n[ \t]*[A-Za-z0-9_-]+\s*:/.test(body)) {
2047
- return "paste-with-indent";
2003
+ return { opts, priorNodesByPath, priorLinksByOriginating, priorFrontmatterIssuesByNode, shortIdToQualified };
2004
+ }
2005
+ async function processRawNode(raw, provider, wctx, accum, claimedPaths, nextIndex) {
2006
+ const bodyHash = sha256(raw.body);
2007
+ const frontmatterHash = sha256(canonicalFrontmatter(raw.frontmatter, raw.frontmatterRaw));
2008
+ const kind = provider.classify(raw.path, raw.frontmatter);
2009
+ if (kind === null) {
2010
+ return false;
2048
2011
  }
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
- }
2012
+ claimedPaths.add(raw.path);
2013
+ const priorNode = wctx.priorNodesByPath.get(raw.path);
2014
+ const nodeHashCacheEligible = wctx.opts.enableCache && wctx.opts.prior !== null && priorNode !== void 0 && priorNode.bodyHash === bodyHash && priorNode.frontmatterHash === frontmatterHash;
2015
+ const sidecarResolution = resolveSidecarOverlay(
2016
+ raw.path,
2017
+ raw.path,
2018
+ wctx.opts.roots,
2019
+ bodyHash,
2020
+ frontmatterHash
2021
+ );
2022
+ const sidecarAnnotationsHash = sha256(
2023
+ canonicalSidecarAnnotations(sidecarResolution.overlay.annotations)
2024
+ );
2025
+ const cacheDecision = computeCacheDecision({
2026
+ extractors: wctx.opts.extractors,
2027
+ kind,
2028
+ nodePath: raw.path,
2029
+ bodyHash,
2030
+ sidecarAnnotationsHash,
2031
+ nodeHashCacheEligible,
2032
+ priorExtractorRuns: wctx.opts.priorExtractorRuns
2033
+ });
2034
+ const ctx = {
2035
+ raw,
2036
+ provider,
2037
+ kind,
2038
+ bodyHash,
2039
+ frontmatterHash,
2040
+ sidecarResolution,
2041
+ sidecarAnnotationsHash,
2042
+ nodeHashCacheEligible,
2043
+ cacheDecision,
2044
+ priorNode,
2045
+ index: nextIndex
2046
+ };
2047
+ if (cacheDecision.fullCacheHit && priorNode) {
2048
+ applyFullCacheHit(ctx, wctx, accum);
2049
+ } else {
2050
+ await applyExtractPath(ctx, wctx, accum);
2054
2051
  }
2055
- return null;
2052
+ return true;
2056
2053
  }
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 });
2054
+ function attachSidecar(node, resolution, sidecarRoots) {
2055
+ node.sidecar = resolution.overlay;
2056
+ if (resolution.parsedRoot !== null) {
2057
+ sidecarRoots.set(node.path, resolution.parsedRoot);
2065
2058
  }
2059
+ return resolution.issues.map(
2060
+ (i) => i.nodeIds.length > 0 ? i : { ...i, nodeIds: [node.path] }
2061
+ );
2066
2062
  }
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 };
2063
+ function applyFullCacheHit(ctx, wctx, accum) {
2064
+ const reused = reusePriorNode({
2065
+ priorNode: ctx.priorNode,
2066
+ bodyHash: ctx.bodyHash,
2067
+ sidecarAnnotationsHash: ctx.sidecarAnnotationsHash,
2068
+ strict: wctx.opts.strict,
2069
+ cachedQualifiedIds: ctx.cacheDecision.cachedQualifiedIds,
2070
+ applicableQualifiedIds: ctx.cacheDecision.applicableQualifiedIds,
2071
+ shortIdToQualified: wctx.shortIdToQualified,
2072
+ priorLinksByOriginating: wctx.priorLinksByOriginating,
2073
+ priorFrontmatterIssuesByNode: wctx.priorFrontmatterIssuesByNode
2074
+ });
2075
+ const reusedSidecarIssues = attachSidecar(reused.node, ctx.sidecarResolution, accum.sidecarRoots);
2076
+ accum.nodes.push(reused.node);
2077
+ accum.cachedPaths.add(reused.node.path);
2078
+ for (const link of reused.internalLinks) accum.internalLinks.push(link);
2079
+ for (const issue of reused.frontmatterIssues) accum.frontmatterIssues.push(issue);
2080
+ for (const issue of reusedSidecarIssues) accum.frontmatterIssues.push(issue);
2081
+ for (const run of reused.extractorRuns) accum.extractorRuns.push(run);
2082
+ wctx.opts.emitter.emit(makeEvent("scan.progress", {
2083
+ index: ctx.index,
2084
+ path: ctx.raw.path,
2085
+ kind: ctx.kind,
2086
+ cached: true
2087
+ }));
2086
2088
  }
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);
2093
- }
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;
2089
+ async function applyExtractPath(ctx, wctx, accum) {
2090
+ const node = buildOrReuseNode(ctx, wctx, accum);
2091
+ const sidecarIssues = attachSidecar(node, ctx.sidecarResolution, accum.sidecarRoots);
2092
+ for (const issue of sidecarIssues) accum.frontmatterIssues.push(issue);
2093
+ const partialCacheHit = isPartialCacheHit(ctx);
2094
+ emitExtractProgress(ctx, wctx, partialCacheHit);
2095
+ const extractorsToRun = partialCacheHit ? ctx.cacheDecision.missingExtractors : ctx.cacheDecision.applicableExtractors;
2096
+ recordFreshlyRunTuples(extractorsToRun, node.path, accum);
2097
+ const extractResult = await runExtractorsForNode({
2098
+ extractors: extractorsToRun,
2099
+ node,
2100
+ body: ctx.raw.body,
2101
+ frontmatter: ctx.raw.frontmatter,
2102
+ bodyHash: ctx.bodyHash,
2103
+ emitter: wctx.opts.emitter,
2104
+ ...wctx.opts.pluginStores ? { pluginStores: wctx.opts.pluginStores } : {}
2105
+ });
2106
+ mergeExtractResult(extractResult, accum);
2107
+ recordExtractorRuns(node.path, ctx, accum);
2108
+ }
2109
+ function emitExtractProgress(ctx, wctx, partialCacheHit) {
2110
+ wctx.opts.emitter.emit(makeEvent("scan.progress", {
2111
+ index: ctx.index,
2112
+ path: ctx.raw.path,
2113
+ kind: ctx.kind,
2114
+ cached: false,
2115
+ ...partialCacheHit ? { partialCache: true } : {}
2116
+ }));
2117
+ }
2118
+ function recordFreshlyRunTuples(extractors, nodePath, accum) {
2119
+ for (const ex of extractors) {
2120
+ accum.freshlyRunTuples.add(`${ex.pluginId}\0${ex.id}\0${nodePath}`);
2121
+ }
2122
+ }
2123
+ function mergeExtractResult(extractResult, accum) {
2124
+ for (const link of extractResult.internalLinks) accum.internalLinks.push(link);
2125
+ for (const link of extractResult.externalLinks) accum.externalLinks.push(link);
2126
+ for (const enr of extractResult.enrichments) {
2127
+ accum.enrichmentBuffer.set(`${enr.nodePath}\0${enr.extractorId}`, enr);
2128
+ }
2129
+ for (const c of extractResult.contributions) accum.contributionsBuffer.push(c);
2130
+ }
2131
+ function isPartialCacheHit(ctx) {
2132
+ return ctx.nodeHashCacheEligible && ctx.cacheDecision.cachedQualifiedIds.size > 0 && ctx.priorNode !== void 0;
2133
+ }
2134
+ function buildOrReuseNode(ctx, wctx, accum) {
2135
+ if (isPartialCacheHit(ctx) && ctx.priorNode) {
2136
+ const partial = cloneNodeAndReshapeLinks({
2137
+ priorNode: ctx.priorNode,
2138
+ strict: wctx.opts.strict,
2139
+ cachedQualifiedIds: ctx.cacheDecision.cachedQualifiedIds,
2140
+ applicableQualifiedIds: ctx.cacheDecision.applicableQualifiedIds,
2141
+ shortIdToQualified: wctx.shortIdToQualified,
2142
+ priorLinksByOriginating: wctx.priorLinksByOriginating,
2143
+ priorFrontmatterIssuesByNode: wctx.priorFrontmatterIssuesByNode
2144
+ });
2145
+ for (const link of partial.internalLinks) accum.internalLinks.push(link);
2146
+ for (const issue of partial.frontmatterIssues) accum.frontmatterIssues.push(issue);
2147
+ accum.nodes.push(partial.node);
2148
+ return partial.node;
2149
+ }
2150
+ const fresh = buildFreshNodeAndValidateFrontmatter({
2151
+ raw: ctx.raw,
2152
+ kind: ctx.kind,
2153
+ provider: ctx.provider,
2154
+ bodyHash: ctx.bodyHash,
2155
+ frontmatterHash: ctx.frontmatterHash,
2156
+ encoder: wctx.opts.encoder,
2157
+ providerFrontmatter: wctx.opts.providerFrontmatter,
2158
+ strict: wctx.opts.strict
2159
+ });
2160
+ accum.nodes.push(fresh.node);
2161
+ for (const issue of fresh.frontmatterIssues) accum.frontmatterIssues.push(issue);
2162
+ return fresh.node;
2163
+ }
2164
+ function recordExtractorRuns(nodePath, ctx, accum) {
2165
+ const ranAt = Date.now();
2166
+ for (const ex of ctx.cacheDecision.applicableExtractors) {
2167
+ accum.extractorRuns.push({
2168
+ nodePath,
2169
+ extractorId: qualifiedExtensionId(ex.pluginId, ex.id),
2170
+ bodyHashAtRun: ctx.bodyHash,
2171
+ ranAt,
2172
+ sidecarAnnotationsHashAtRun: ctx.sidecarAnnotationsHash
2173
+ });
2099
2174
  }
2100
2175
  }
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);
2176
+
2177
+ // kernel/orchestrator/index.ts
2178
+ var SCANNED_BY = {
2179
+ name: "skill-map",
2180
+ version: package_default.version,
2181
+ specVersion: resolveSpecVersionSafe()
2182
+ };
2183
+ function resolveSpecVersionSafe() {
2184
+ try {
2185
+ return installedSpecVersion();
2186
+ } catch {
2187
+ return "unknown";
2106
2188
  }
2107
- for (const link of externalLinks) {
2108
- const source = byPath2.get(link.source);
2109
- if (source && !cachedPaths.has(source.path)) source.externalRefsCount += 1;
2189
+ }
2190
+ async function runScanWithRenames(_kernel, options) {
2191
+ return runScanInternal(_kernel, options);
2192
+ }
2193
+ async function runScan(_kernel, options) {
2194
+ const { result } = await runScanInternal(_kernel, options);
2195
+ return result;
2196
+ }
2197
+ async function runScanInternal(_kernel, options) {
2198
+ validateRoots(options.roots);
2199
+ const setup = buildScanSetup(options);
2200
+ const { emitter, exts, hookDispatcher, encoder, prior, start } = setup;
2201
+ const scanStartedEvent = makeEvent("scan.started", { roots: options.roots });
2202
+ emitter.emit(scanStartedEvent);
2203
+ await hookDispatcher.dispatch("scan.started", scanStartedEvent);
2204
+ const walked = await walkAndExtract({
2205
+ providers: exts.providers,
2206
+ extractors: exts.extractors,
2207
+ roots: options.roots,
2208
+ ...options.ignoreFilter ? { ignoreFilter: options.ignoreFilter } : {},
2209
+ emitter,
2210
+ encoder,
2211
+ strict: setup.strict,
2212
+ enableCache: setup.enableCache,
2213
+ prior,
2214
+ priorIndex: setup.priorIndex,
2215
+ priorExtractorRuns: setup.priorExtractorRuns,
2216
+ providerFrontmatter: setup.providerFrontmatter,
2217
+ pluginStores: options.pluginStores
2218
+ });
2219
+ recomputeLinkCounts(walked.nodes, walked.internalLinks);
2220
+ recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);
2221
+ await dispatchExtractorCompleted(exts.extractors, emitter, hookDispatcher);
2222
+ const registeredActionIds = new Set(
2223
+ _kernel.registry.all("action").map((a) => qualifiedExtensionId(a.pluginId, a.id))
2224
+ );
2225
+ const analyzerResult = await runAnalyzers(
2226
+ exts.analyzers,
2227
+ walked.nodes,
2228
+ walked.internalLinks,
2229
+ walked.orphanSidecars,
2230
+ walked.sidecarRoots,
2231
+ options.annotationContributions ?? [],
2232
+ options.viewContributions ?? [],
2233
+ options.orphanJobFiles ?? [],
2234
+ options.referenceablePaths,
2235
+ options.cwd,
2236
+ registeredActionIds,
2237
+ emitter,
2238
+ hookDispatcher
2239
+ );
2240
+ mergeAnalyzerEmissions(walked, analyzerResult, exts.analyzers);
2241
+ const issues = analyzerResult.issues;
2242
+ for (const issue of walked.frontmatterIssues) issues.push(issue);
2243
+ const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues) : [];
2244
+ const stats = buildScanStats(walked, issues, start);
2245
+ const scanCompletedEvent = makeEvent("scan.completed", { stats });
2246
+ emitter.emit(scanCompletedEvent);
2247
+ await hookDispatcher.dispatch("scan.completed", scanCompletedEvent);
2248
+ return buildScanReturn(walked, issues, renameOps, stats, options, setup);
2249
+ }
2250
+ function buildScanSetup(options) {
2251
+ const start = Date.now();
2252
+ const emitter = options.emitter ?? new InMemoryProgressEmitter();
2253
+ const exts = options.extensions ?? { providers: [], extractors: [], analyzers: [] };
2254
+ const hookDispatcher = makeHookDispatcher(exts.hooks ?? [], emitter);
2255
+ const tokenize = options.tokenize !== false;
2256
+ const encoder = tokenize ? new Tiktoken2(cl100k_base) : null;
2257
+ const prior = options.priorSnapshot ?? null;
2258
+ const priorIndex = indexPriorSnapshot(prior);
2259
+ const providerFrontmatter = buildProviderFrontmatterValidator(exts.providers);
2260
+ return {
2261
+ start,
2262
+ scannedAt: start,
2263
+ emitter,
2264
+ exts,
2265
+ hookDispatcher,
2266
+ encoder,
2267
+ prior,
2268
+ priorIndex,
2269
+ priorExtractorRuns: options.priorExtractorRuns,
2270
+ providerFrontmatter,
2271
+ scope: options.scope ?? "project",
2272
+ strict: options.strict === true,
2273
+ enableCache: options.enableCache === true
2274
+ };
2275
+ }
2276
+ async function dispatchExtractorCompleted(extractors, emitter, hookDispatcher) {
2277
+ for (const extractor of extractors) {
2278
+ const extractorId = qualifiedExtensionId(extractor.pluginId, extractor.id);
2279
+ const evt = makeEvent("extractor.completed", { extractorId });
2280
+ emitter.emit(evt);
2281
+ await hookDispatcher.dispatch("extractor.completed", evt);
2110
2282
  }
2111
2283
  }
2112
- function mergeNodeWithEnrichments(node, enrichments, opts = {}) {
2113
- const includeStale = opts.includeStale === true;
2114
- const applicable = enrichments.filter((e) => e.nodePath === node.path).filter((e) => includeStale || !e.stale).sort((a, b) => a.enrichedAt - b.enrichedAt);
2115
- const base = {};
2116
- assignSafe(base, node.frontmatter ?? {});
2117
- for (const row of applicable) {
2118
- assignSafe(base, row.value);
2284
+ function mergeAnalyzerEmissions(walked, analyzerResult, analyzers) {
2285
+ for (const c of analyzerResult.contributions) walked.contributions.push(c);
2286
+ for (const analyzer of analyzers ?? []) {
2287
+ if (analyzer.viewContributions === void 0) continue;
2288
+ for (const node of walked.nodes) {
2289
+ walked.freshlyRunTuples.add(`${analyzer.pluginId}\0${analyzer.id}\0${node.path}`);
2290
+ }
2119
2291
  }
2120
- return base;
2121
2292
  }
2122
- var FORBIDDEN_MERGE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2123
- function assignSafe(target, source) {
2124
- for (const [k, v] of Object.entries(source)) {
2125
- if (FORBIDDEN_MERGE_KEYS.has(k)) continue;
2126
- target[k] = v;
2293
+ function buildScanStats(walked, issues, start) {
2294
+ return {
2295
+ // `filesSkipped` is "files walked but not classified by any
2296
+ // Provider". Today every walked file IS classified by its Provider
2297
+ // (the `claude` Provider's `classify()` always returns a kind,
2298
+ // falling back to `'markdown'`), so this is always 0. Wired now
2299
+ // so the field shape is spec-conformant; meaningful once multiple
2300
+ // Providers compete.
2301
+ filesWalked: walked.filesWalked,
2302
+ filesSkipped: 0,
2303
+ nodesCount: walked.nodes.length,
2304
+ linksCount: walked.internalLinks.length,
2305
+ issuesCount: issues.length,
2306
+ durationMs: Date.now() - start
2307
+ };
2308
+ }
2309
+ function buildScanReturn(walked, issues, renameOps, stats, options, setup) {
2310
+ return {
2311
+ result: {
2312
+ schemaVersion: 1,
2313
+ scannedAt: setup.scannedAt,
2314
+ scope: setup.scope,
2315
+ roots: options.roots,
2316
+ providers: setup.exts.providers.map((a) => a.id),
2317
+ scannedBy: SCANNED_BY,
2318
+ nodes: walked.nodes,
2319
+ links: walked.internalLinks,
2320
+ issues,
2321
+ stats
2322
+ },
2323
+ renameOps,
2324
+ extractorRuns: walked.extractorRuns,
2325
+ enrichments: walked.enrichments,
2326
+ contributions: walked.contributions,
2327
+ freshlyRunTuples: walked.freshlyRunTuples
2328
+ };
2329
+ }
2330
+ function validateRoots(roots) {
2331
+ if (roots.length === 0) {
2332
+ throw new Error(ORCHESTRATOR_TEXTS.runScanRootEmptyArray);
2333
+ }
2334
+ for (const root of roots) {
2335
+ if (!existsSync9(root) || !statSync2(root).isDirectory()) {
2336
+ throw new Error(tx(ORCHESTRATOR_TEXTS.runScanRootMissing, { root }));
2337
+ }
2127
2338
  }
2128
2339
  }
2129
2340
 
2130
2341
  // kernel/scan/watcher.ts
2131
- import { resolve as resolve8, relative as relative4, sep as sep3 } from "path";
2342
+ import { resolve as resolve10, relative as relative4, sep as sep3 } from "path";
2132
2343
  import chokidar from "chokidar";
2133
2344
  function createChokidarWatcher(opts) {
2134
- const absRoots = opts.roots.map((r) => resolve8(opts.cwd, r));
2345
+ const absRoots = opts.roots.map((r) => resolve10(opts.cwd, r));
2135
2346
  const ignoreFilterOpt = opts.ignoreFilter;
2136
2347
  const getFilter = ignoreFilterOpt === void 0 ? void 0 : typeof ignoreFilterOpt === "function" ? ignoreFilterOpt : () => ignoreFilterOpt;
2137
2348
  const ignored = getFilter ? (path) => {
@@ -2144,7 +2355,8 @@ function createChokidarWatcher(opts) {
2144
2355
  const watcher = chokidar.watch(absRoots, {
2145
2356
  ignoreInitial: true,
2146
2357
  persistent: true,
2147
- ...ignored ? { ignored } : {}
2358
+ ...ignored ? { ignored } : {},
2359
+ ...opts.depth !== void 0 ? { depth: opts.depth } : {}
2148
2360
  });
2149
2361
  let pending = [];
2150
2362
  let timer = null;