@skill-map/cli 0.61.5 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/cli.js +1346 -479
  2. package/dist/index.js +368 -96
  3. package/dist/kernel/index.d.ts +232 -25
  4. package/dist/kernel/index.js +368 -96
  5. package/dist/migrations/001_initial.sql +18 -8
  6. package/dist/ui/{chunk-4N3NRZEH.js → chunk-276RLZR4.js} +1 -1
  7. package/dist/ui/{chunk-MGWGV4VD.js → chunk-34ZZDYNQ.js} +1 -1
  8. package/dist/ui/chunk-56CBK7LB.js +1 -0
  9. package/dist/ui/{chunk-OVVTCPBJ.js → chunk-7ANZW2OI.js} +1 -1
  10. package/dist/ui/chunk-BJ6X6WBO.js +4 -0
  11. package/dist/ui/{chunk-5SSKJ7AM.js → chunk-BOVJVOLH.js} +1 -1
  12. package/dist/ui/chunk-C42H2UHU.js +3 -0
  13. package/dist/ui/{chunk-GKQA75EF.js → chunk-CJURGJTN.js} +1 -1
  14. package/dist/ui/chunk-CM4YB7L4.js +2 -0
  15. package/dist/ui/{chunk-Q4PXVDJA.js → chunk-CZSLV6YD.js} +1 -1
  16. package/dist/ui/{chunk-7X3DZNG4.js → chunk-DLYJHLJX.js} +2 -2
  17. package/dist/ui/chunk-ECKRC6XD.js +1843 -0
  18. package/dist/ui/{chunk-JTCIY3SL.js → chunk-FC22ZJQZ.js} +1 -1
  19. package/dist/ui/{chunk-FRUHVCND.js → chunk-FYATUDAH.js} +1 -1
  20. package/dist/ui/chunk-IYC5ZW4L.js +2 -0
  21. package/dist/ui/{chunk-MBBJJEUX.js → chunk-JZ2YF7EL.js} +1 -1
  22. package/dist/ui/{chunk-HQ6M2HXK.js → chunk-LPDD2DHE.js} +1 -1
  23. package/dist/ui/{chunk-I52OQIZQ.js → chunk-NC3HOVDG.js} +1 -1
  24. package/dist/ui/{chunk-N6MUHKWR.js → chunk-UTRZTB6V.js} +1 -1
  25. package/dist/ui/chunk-VHEFRMK3.js +1 -0
  26. package/dist/ui/chunk-Y2Z26SRI.js +1 -0
  27. package/dist/ui/index.html +1 -1
  28. package/dist/ui/main-RW5YGD6H.js +4 -0
  29. package/migrations/001_initial.sql +18 -8
  30. package/package.json +2 -2
  31. package/dist/ui/chunk-6NYH7LND.js +0 -3
  32. package/dist/ui/chunk-7VUEZZFJ.js +0 -1
  33. package/dist/ui/chunk-AKKFFP7Y.js +0 -1
  34. package/dist/ui/chunk-L34EUS75.js +0 -2
  35. package/dist/ui/chunk-UTGLW5ON.js +0 -1843
  36. package/dist/ui/chunk-ZYPXVXYF.js +0 -4
  37. package/dist/ui/main-OTDMPZHD.js +0 -4
@@ -1,6 +1,6 @@
1
1
  // kernel/i18n/registry.texts.ts
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="5077263a-bf07-58af-b37d-ea6ab8f9a27c")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="ca2610ab-aae0-5366-8634-73a2d8fd3eda")}catch(e){}}();
4
4
  var REGISTRY_TEXTS = {
5
5
  duplicateExtension: "Extension already registered: {{kind}}:{{qualifiedId}}",
6
6
  unknownKind: "Unknown extension kind: {{kind}}",
@@ -96,13 +96,13 @@ var Registry = class {
96
96
 
97
97
  // kernel/orchestrator/index.ts
98
98
  import { existsSync as existsSync10, statSync as statSync4 } from "fs";
99
- import { isAbsolute as isAbsolute3, resolve as resolve9 } from "path";
99
+ import { isAbsolute as isAbsolute5, resolve as resolve11 } from "path";
100
100
  import { Tiktoken as Tiktoken2 } from "js-tiktoken/lite";
101
101
 
102
102
  // package.json
103
103
  var package_default = {
104
104
  name: "@skill-map/cli",
105
- version: "0.61.5",
105
+ version: "0.62.0",
106
106
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
107
107
  license: "MIT",
108
108
  type: "module",
@@ -2155,9 +2155,12 @@ function detectRenamesAndOrphans(prior, current, issues, silenced) {
2155
2155
  return ops;
2156
2156
  }
2157
2157
 
2158
+ // kernel/orchestrator/walk.ts
2159
+ import { isAbsolute as isAbsolute4, resolve as resolve10 } from "path";
2160
+
2158
2161
  // kernel/scan/walk-content.ts
2159
2162
  import { readFile, readdir, lstat } from "fs/promises";
2160
- import { join as join4, relative as relative2, sep } from "path";
2163
+ import { isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve7, sep } from "path";
2161
2164
 
2162
2165
  // kernel/scan/ignore.ts
2163
2166
  import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
@@ -2336,33 +2339,116 @@ async function* walkContent(roots, options) {
2336
2339
  const filter = options.ignoreFilter ?? buildIgnoreFilter();
2337
2340
  const extensions = options.extensions;
2338
2341
  const sizeLimit = buildSizeLimit(options);
2342
+ if (options.scopedPaths !== void 0) {
2343
+ yield* walkScoped(roots, options.scopedPaths, extensions, sizeLimit, parser);
2344
+ return;
2345
+ }
2339
2346
  for (const root of roots) {
2340
2347
  for await (const entry of walkRoot(root, root, filter, extensions, sizeLimit)) {
2341
2348
  const relPath = relative2(root, entry.full).split(sep).join("/");
2342
- let raw;
2343
- try {
2344
- raw = await readFile(entry.full, "utf8");
2345
- } catch {
2346
- continue;
2347
- }
2348
- const parsed = parser.parse(raw, relPath);
2349
- yield {
2350
- path: relPath,
2351
- body: parsed.body,
2352
- frontmatterRaw: parsed.frontmatterRaw,
2353
- frontmatter: parsed.frontmatter,
2354
- // File mtime from the TOCTOU `lstat` (zero extra syscalls).
2355
- // Threaded onto the persisted `Node` as `modifiedAtMs`.
2356
- modifiedAtMs: entry.modifiedAtMs,
2357
- // Audit L1: forward parser diagnostics (e.g. malformed YAML)
2358
- // through the IRawNode surface so the orchestrator can
2359
- // convert them into warn-level kernel `Issue` rows. Omitted
2360
- // when the parser reported no issues (happy path).
2361
- ...parsed.issues && parsed.issues.length > 0 ? { parseIssues: parsed.issues } : {}
2362
- };
2349
+ const rec = await traversedEntryToNode(entry, relPath, options.priorMtimes, parser);
2350
+ if (rec !== null) yield rec;
2363
2351
  }
2364
2352
  }
2365
2353
  }
2354
+ async function traversedEntryToNode(entry, relPath, priorMtimes, parser) {
2355
+ const priorMtime = priorMtimes?.get(relPath);
2356
+ if (priorMtime !== void 0 && priorMtime === entry.modifiedAtMs) {
2357
+ return buildUnchangedRecord(entry.full, relPath, entry.modifiedAtMs, parser);
2358
+ }
2359
+ const parsed = await readAndParse(entry.full, relPath, parser);
2360
+ if (parsed === null) return null;
2361
+ return {
2362
+ path: relPath,
2363
+ body: parsed.body,
2364
+ frontmatterRaw: parsed.frontmatterRaw,
2365
+ frontmatter: parsed.frontmatter,
2366
+ // File mtime from the TOCTOU `lstat` (zero extra syscalls).
2367
+ // Threaded onto the persisted `Node` as `modifiedAtMs`.
2368
+ modifiedAtMs: entry.modifiedAtMs,
2369
+ // Audit L1: forward parser diagnostics (e.g. malformed YAML)
2370
+ // through the IRawNode surface so the orchestrator can
2371
+ // convert them into warn-level kernel `Issue` rows. Omitted
2372
+ // when the parser reported no issues (happy path).
2373
+ ...parsed.parseIssues ? { parseIssues: parsed.parseIssues } : {}
2374
+ };
2375
+ }
2376
+ function buildUnchangedRecord(full, relPath, modifiedAtMs, parser) {
2377
+ return {
2378
+ path: relPath,
2379
+ body: "",
2380
+ frontmatterRaw: "",
2381
+ frontmatter: {},
2382
+ modifiedAtMs,
2383
+ unchanged: true,
2384
+ reread: async () => {
2385
+ const re = await readAndParse(full, relPath, parser);
2386
+ return re ?? { body: "", frontmatterRaw: "", frontmatter: {} };
2387
+ }
2388
+ };
2389
+ }
2390
+ async function* walkScoped(roots, scopedPaths, extensions, sizeLimit, parser) {
2391
+ const absRoots = roots.map((r) => isAbsolute2(r) ? r : resolve7(r));
2392
+ for (const scoped of scopedPaths) {
2393
+ const rec = await scopedPathToNode(scoped, absRoots, extensions, sizeLimit, parser);
2394
+ if (rec !== null) yield rec;
2395
+ }
2396
+ }
2397
+ async function scopedPathToNode(scoped, absRoots, extensions, sizeLimit, parser) {
2398
+ const full = isAbsolute2(scoped) ? scoped : resolve7(scoped);
2399
+ const relPath = relativeFromRoots(full, absRoots);
2400
+ if (relPath === null) return null;
2401
+ if (!hasMatchingExtension(full, extensions)) return null;
2402
+ const s = await statRegularFile(full, relPath, sizeLimit);
2403
+ if (s === null) return null;
2404
+ const parsed = await readAndParse(full, relPath, parser);
2405
+ if (parsed === null) return null;
2406
+ return {
2407
+ path: relPath,
2408
+ body: parsed.body,
2409
+ frontmatterRaw: parsed.frontmatterRaw,
2410
+ frontmatter: parsed.frontmatter,
2411
+ modifiedAtMs: Math.round(s.mtimeMs),
2412
+ ...parsed.parseIssues ? { parseIssues: parsed.parseIssues } : {}
2413
+ };
2414
+ }
2415
+ async function statRegularFile(full, relPath, sizeLimit) {
2416
+ let s;
2417
+ try {
2418
+ s = await lstat(full);
2419
+ } catch {
2420
+ return null;
2421
+ }
2422
+ if (!s.isFile()) return null;
2423
+ if (sizeLimit.maxFileSizeBytes !== void 0 && s.size > sizeLimit.maxFileSizeBytes) {
2424
+ sizeLimit.onOversizedFile?.({ path: relPath, bytes: s.size });
2425
+ return null;
2426
+ }
2427
+ return s;
2428
+ }
2429
+ function relativeFromRoots(full, absRoots) {
2430
+ for (const root of absRoots) {
2431
+ const rel = relative2(root, full);
2432
+ if (rel === "" || rel.startsWith("..") || isAbsolute2(rel)) continue;
2433
+ return rel.split(sep).join("/");
2434
+ }
2435
+ return null;
2436
+ }
2437
+ async function readAndParse(full, relPath, parser) {
2438
+ let raw;
2439
+ try {
2440
+ raw = await readFile(full, "utf8");
2441
+ } catch {
2442
+ return null;
2443
+ }
2444
+ const parsed = parser.parse(raw, relPath);
2445
+ return {
2446
+ body: parsed.body,
2447
+ frontmatterRaw: parsed.frontmatterRaw,
2448
+ frontmatter: parsed.frontmatter,
2449
+ ...parsed.issues && parsed.issues.length > 0 ? { parseIssues: parsed.issues } : {}
2450
+ };
2451
+ }
2366
2452
  function buildSizeLimit(options) {
2367
2453
  const sizeLimit = {};
2368
2454
  if (options.maxFileSizeBytes !== void 0) {
@@ -2418,23 +2504,29 @@ function resolveProviderWalk(provider) {
2418
2504
  return walk2;
2419
2505
  }
2420
2506
  const read = provider.read ?? DEFAULT_READ_CONFIG;
2421
- return (roots, options) => {
2422
- const walkOptions = {
2423
- extensions: read.extensions,
2424
- parser: read.parser
2425
- };
2426
- if (options?.ignoreFilter) walkOptions.ignoreFilter = options.ignoreFilter;
2427
- if (options?.maxFileSizeBytes !== void 0) {
2428
- walkOptions.maxFileSizeBytes = options.maxFileSizeBytes;
2429
- }
2430
- if (options?.onOversizedFile) walkOptions.onOversizedFile = options.onOversizedFile;
2431
- return walkContent(roots, walkOptions);
2507
+ return (roots, options) => walkContent(roots, buildWalkContentOptions(read, options));
2508
+ }
2509
+ function buildWalkContentOptions(read, options) {
2510
+ const walkOptions = {
2511
+ extensions: read.extensions,
2512
+ parser: read.parser
2432
2513
  };
2514
+ if (options) copyOptionalWalkOptions(walkOptions, options);
2515
+ return walkOptions;
2516
+ }
2517
+ function copyOptionalWalkOptions(walkOptions, options) {
2518
+ if (options.ignoreFilter) walkOptions.ignoreFilter = options.ignoreFilter;
2519
+ if (options.maxFileSizeBytes !== void 0) {
2520
+ walkOptions.maxFileSizeBytes = options.maxFileSizeBytes;
2521
+ }
2522
+ if (options.onOversizedFile) walkOptions.onOversizedFile = options.onOversizedFile;
2523
+ if (options.priorMtimes) walkOptions.priorMtimes = options.priorMtimes;
2524
+ if (options.scopedPaths) walkOptions.scopedPaths = options.scopedPaths;
2433
2525
  }
2434
2526
 
2435
2527
  // kernel/sidecar/parse.ts
2436
2528
  import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
2437
- import { dirname as dirname4, resolve as resolve7 } from "path";
2529
+ import { dirname as dirname4, resolve as resolve8 } from "path";
2438
2530
  import { createRequire as createRequire3 } from "module";
2439
2531
  import { Ajv2020 as Ajv20204 } from "ajv/dist/2020.js";
2440
2532
  import yaml2 from "js-yaml";
@@ -2513,10 +2605,10 @@ function getSidecarValidator() {
2513
2605
  applyAjvFormats(ajv);
2514
2606
  const specRoot = resolveSpecRoot2();
2515
2607
  const annotationsSchema = JSON.parse(
2516
- readFileSync6(resolve7(specRoot, "schemas/annotations.schema.json"), "utf8")
2608
+ readFileSync6(resolve8(specRoot, "schemas/annotations.schema.json"), "utf8")
2517
2609
  );
2518
2610
  const sidecarSchema = JSON.parse(
2519
- readFileSync6(resolve7(specRoot, "schemas/sidecar.schema.json"), "utf8")
2611
+ readFileSync6(resolve8(specRoot, "schemas/sidecar.schema.json"), "utf8")
2520
2612
  );
2521
2613
  ajv.addSchema(annotationsSchema);
2522
2614
  cachedSidecarValidator = ajv.compile(sidecarSchema);
@@ -2587,7 +2679,7 @@ function safeIsFile(path) {
2587
2679
 
2588
2680
  // kernel/sidecar/store.ts
2589
2681
  import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
2590
- import { dirname as dirname6, resolve as resolve8 } from "path";
2682
+ import { dirname as dirname6, resolve as resolve9 } from "path";
2591
2683
  import { createRequire as createRequire4 } from "module";
2592
2684
  import { Ajv2020 as Ajv20205 } from "ajv/dist/2020.js";
2593
2685
  import yaml3 from "js-yaml";
@@ -2610,7 +2702,7 @@ import { dirname as dirname5 } from "path";
2610
2702
  // kernel/orchestrator/node-build.ts
2611
2703
  import { createHash } from "crypto";
2612
2704
  import { existsSync as existsSync9 } from "fs";
2613
- import { isAbsolute as isAbsolute2, resolve as resolvePath } from "path";
2705
+ import { isAbsolute as isAbsolute3, resolve as resolvePath } from "path";
2614
2706
  import "js-tiktoken/lite";
2615
2707
  import yaml4 from "js-yaml";
2616
2708
 
@@ -2773,7 +2865,7 @@ function resolveSidecarOverlay(relativePath, nodePathForIssue, roots, liveBodyHa
2773
2865
  };
2774
2866
  }
2775
2867
  function resolveAbsoluteMdPath(relativePath, roots) {
2776
- if (isAbsolute2(relativePath)) {
2868
+ if (isAbsolute3(relativePath)) {
2777
2869
  return existsSync9(relativePath) ? relativePath : null;
2778
2870
  }
2779
2871
  for (const root of roots) {
@@ -2862,34 +2954,46 @@ async function walkAndExtract(opts) {
2862
2954
  oversizedSeen.add(info.path);
2863
2955
  oversizedFiles.push(info);
2864
2956
  };
2957
+ const priorMtimes = buildPriorMtimes(opts);
2865
2958
  const walkOptions = {
2866
2959
  ...opts.ignoreFilter ? { ignoreFilter: opts.ignoreFilter } : {},
2867
2960
  onOversizedFile,
2868
- ...opts.maxFileSizeBytes !== void 0 ? { maxFileSizeBytes: opts.maxFileSizeBytes } : {}
2961
+ ...opts.maxFileSizeBytes !== void 0 ? { maxFileSizeBytes: opts.maxFileSizeBytes } : {},
2962
+ ...priorMtimes ? { priorMtimes } : {}
2869
2963
  };
2870
- let filesWalked = 0;
2964
+ let filesWalked;
2871
2965
  let index = 0;
2872
- const effectiveMaxNodes = opts.overrideMaxNodes ?? opts.recommendedNodeLimit;
2966
+ const { effectiveScanCeiling, effectiveMaxRenderNodes } = resolveEffectiveCaps(opts);
2873
2967
  let capReached = false;
2874
- const activeProviders = opts.providers.filter((provider) => {
2875
- if (!provider.gatedByActiveLens) return true;
2876
- if (opts.activeProvider === null) return true;
2877
- return provider.id === opts.activeProvider;
2878
- });
2968
+ const activeProviders = opts.providers.filter(
2969
+ (provider) => providerParticipates(provider, opts.activeProvider)
2970
+ );
2879
2971
  const advance = async (raw, provider) => {
2880
2972
  const advanced = await processRawNode(raw, provider, wctx, accum, claimedPaths, index + 1);
2881
2973
  if (advanced) index += 1;
2882
2974
  };
2883
- outer: for (const provider of activeProviders) {
2884
- for await (const raw of resolveProviderWalk(provider)(opts.roots, walkOptions)) {
2885
- filesWalked += 1;
2886
- if (claimedPaths.has(raw.path)) continue;
2887
- if (accum.nodes.length >= effectiveMaxNodes) {
2888
- capReached = true;
2889
- break outer;
2890
- }
2891
- await advance(raw, provider);
2892
- }
2975
+ if (opts.incrementalChangedPaths !== void 0) {
2976
+ filesWalked = await walkIncremental({
2977
+ changedPaths: opts.incrementalChangedPaths,
2978
+ activeProviders,
2979
+ walkOptions,
2980
+ wctx,
2981
+ accum,
2982
+ claimedPaths,
2983
+ advance
2984
+ });
2985
+ } else {
2986
+ const full = await walkFullTraversal({
2987
+ activeProviders,
2988
+ roots: opts.roots,
2989
+ walkOptions,
2990
+ accum,
2991
+ claimedPaths,
2992
+ effectiveScanCeiling,
2993
+ advance
2994
+ });
2995
+ filesWalked = full.filesWalked;
2996
+ capReached = full.capReached;
2893
2997
  }
2894
2998
  const orphanSidecars = discoverOrphanSidecars(opts.roots);
2895
2999
  return {
@@ -2900,9 +3004,9 @@ async function walkAndExtract(opts) {
2900
3004
  frontmatterIssues: accum.frontmatterIssues,
2901
3005
  filesWalked,
2902
3006
  oversizedFiles,
2903
- recommendedNodeLimit: opts.recommendedNodeLimit,
2904
- overrideMaxNodes: opts.overrideMaxNodes,
2905
- capReached,
3007
+ scanCeiling: effectiveScanCeiling,
3008
+ scanTruncated: capReached,
3009
+ maxRenderNodes: effectiveMaxRenderNodes,
2906
3010
  enrichments: [...accum.enrichmentBuffer.values()],
2907
3011
  extractorRuns: accum.extractorRuns,
2908
3012
  contributions: accum.contributionsBuffer,
@@ -2913,6 +3017,119 @@ async function walkAndExtract(opts) {
2913
3017
  signals: accum.signals
2914
3018
  };
2915
3019
  }
3020
+ async function walkFullTraversal(args) {
3021
+ let filesWalked = 0;
3022
+ let capReached = false;
3023
+ outer: for (const provider of args.activeProviders) {
3024
+ for await (const raw of resolveProviderWalk(provider)(args.roots, args.walkOptions)) {
3025
+ filesWalked += 1;
3026
+ if (args.claimedPaths.has(raw.path)) continue;
3027
+ if (args.accum.nodes.length >= args.effectiveScanCeiling) {
3028
+ capReached = true;
3029
+ break outer;
3030
+ }
3031
+ await args.advance(raw, provider);
3032
+ }
3033
+ }
3034
+ return { filesWalked, capReached };
3035
+ }
3036
+ async function walkIncremental(args) {
3037
+ const changed = expandSidecarPaths(args.changedPaths.changed, args.wctx.priorNodesByPath);
3038
+ const removed = expandSidecarPaths(args.changedPaths.removed, args.wctx.priorNodesByPath);
3039
+ let filesWalked = 0;
3040
+ const scopedAbs = [...changed].map((rel) => toAbsolute(rel, args.wctx.opts.roots));
3041
+ if (scopedAbs.length > 0) {
3042
+ const scopedWalkOptions = { ...args.walkOptions, scopedPaths: scopedAbs };
3043
+ for (const provider of args.activeProviders) {
3044
+ for await (const raw of resolveProviderWalk(provider)(args.wctx.opts.roots, scopedWalkOptions)) {
3045
+ filesWalked += 1;
3046
+ if (args.claimedPaths.has(raw.path)) continue;
3047
+ await args.advance(raw, provider);
3048
+ }
3049
+ }
3050
+ }
3051
+ filesWalked += await injectUnchangedPriorNodes(args, changed, removed);
3052
+ return filesWalked;
3053
+ }
3054
+ async function injectUnchangedPriorNodes(args, changed, removed) {
3055
+ const providerById = new Map(args.activeProviders.map((p) => [p.id, p]));
3056
+ const universalFallback = args.activeProviders.find((p) => !p.gatedByActiveLens) ?? args.activeProviders[0];
3057
+ let injected = 0;
3058
+ for (const priorNode of args.wctx.opts.prior?.nodes ?? []) {
3059
+ if (!shouldInjectPriorNode(priorNode, changed, removed, args.claimedPaths)) continue;
3060
+ const provider = providerById.get(priorNode.provider) ?? universalFallback;
3061
+ if (!provider) continue;
3062
+ const raw = buildUnchangedRawNode(priorNode, provider, args.wctx.opts.roots);
3063
+ await args.advance(raw, provider);
3064
+ injected += 1;
3065
+ }
3066
+ return injected;
3067
+ }
3068
+ function shouldInjectPriorNode(priorNode, changed, removed, claimedPaths) {
3069
+ const path = priorNode.path;
3070
+ if (changed.has(path) || removed.has(path) || claimedPaths.has(path)) return false;
3071
+ if (priorNode.virtual === true) return false;
3072
+ return true;
3073
+ }
3074
+ function buildUnchangedRawNode(priorNode, provider, roots) {
3075
+ const path = priorNode.path;
3076
+ return {
3077
+ path,
3078
+ body: "",
3079
+ frontmatterRaw: "",
3080
+ frontmatter: {},
3081
+ ...typeof priorNode.modifiedAtMs === "number" ? { modifiedAtMs: priorNode.modifiedAtMs } : {},
3082
+ unchanged: true,
3083
+ reread: async () => {
3084
+ const abs = toAbsolute(path, roots);
3085
+ for await (const re of resolveProviderWalk(provider)(roots, { scopedPaths: [abs] })) {
3086
+ return {
3087
+ body: re.body,
3088
+ frontmatterRaw: re.frontmatterRaw,
3089
+ frontmatter: re.frontmatter,
3090
+ ...re.parseIssues ? { parseIssues: re.parseIssues } : {}
3091
+ };
3092
+ }
3093
+ return { body: "", frontmatterRaw: "", frontmatter: {} };
3094
+ }
3095
+ };
3096
+ }
3097
+ function expandSidecarPaths(paths, priorNodesByPath) {
3098
+ const out = /* @__PURE__ */ new Set();
3099
+ for (const path of paths) {
3100
+ if (path.endsWith(".sm")) {
3101
+ const mdPath = `${path.slice(0, -".sm".length)}.md`;
3102
+ if (priorNodesByPath.has(mdPath)) out.add(mdPath);
3103
+ continue;
3104
+ }
3105
+ out.add(path);
3106
+ }
3107
+ return out;
3108
+ }
3109
+ function toAbsolute(relPath, roots) {
3110
+ const root = roots[0] ?? ".";
3111
+ const absRoot = isAbsolute4(root) ? root : resolve10(root);
3112
+ return resolve10(absRoot, relPath);
3113
+ }
3114
+ function resolveEffectiveCaps(opts) {
3115
+ return {
3116
+ effectiveScanCeiling: opts.overrideScanCeiling ?? opts.scanCeiling,
3117
+ effectiveMaxRenderNodes: opts.overrideMaxRenderNodes ?? opts.maxRenderNodes
3118
+ };
3119
+ }
3120
+ function buildPriorMtimes(opts) {
3121
+ if (!opts.enableCache || opts.prior === null || opts.tokenizerChanged) return void 0;
3122
+ const map = /* @__PURE__ */ new Map();
3123
+ for (const node of opts.prior.nodes) {
3124
+ if (typeof node.modifiedAtMs === "number") map.set(node.path, node.modifiedAtMs);
3125
+ }
3126
+ return map.size > 0 ? map : void 0;
3127
+ }
3128
+ function providerParticipates(provider, activeProvider) {
3129
+ if (!provider.gatedByActiveLens) return true;
3130
+ if (activeProvider === null) return true;
3131
+ return provider.id === activeProvider;
3132
+ }
2916
3133
  function createWalkAccumulators() {
2917
3134
  return {
2918
3135
  nodes: [],
@@ -2941,61 +3158,106 @@ function buildWalkContext(opts) {
2941
3158
  return { opts, priorNodesByPath, priorLinksByOriginating, priorFrontmatterIssuesByNode, shortIdToQualified };
2942
3159
  }
2943
3160
  async function processRawNode(raw, provider, wctx, accum, claimedPaths, nextIndex) {
2944
- const bodyHash = sha256(raw.body);
2945
- const frontmatterHash = sha256(canonicalFrontmatter(raw.frontmatter, raw.frontmatterRaw));
2946
3161
  if (Array.isArray(provider.roots) && provider.roots.length > 0) {
2947
3162
  if (!matchesAnyRoot(raw.path, provider.roots)) return false;
2948
3163
  }
3164
+ if (raw.unchanged === true) {
3165
+ const handled = await handleUnchangedRawNode(raw, provider, wctx, accum, claimedPaths, nextIndex);
3166
+ if (handled) return true;
3167
+ }
3168
+ const bodyHash = sha256(raw.body);
3169
+ const frontmatterHash = sha256(canonicalFrontmatter(raw.frontmatter, raw.frontmatterRaw));
2949
3170
  const kind = provider.classify(raw.path, raw.frontmatter);
2950
3171
  if (kind === null) {
2951
3172
  return false;
2952
3173
  }
2953
3174
  claimedPaths.add(raw.path);
2954
3175
  const priorNode = wctx.priorNodesByPath.get(raw.path);
2955
- const nodeHashCacheEligible = wctx.opts.enableCache && // Tokenizer-change invalidation: when the resolved encoder differs
2956
- // from the one that produced the prior snapshot's counts, no node is
2957
- // cache-eligible, every node rebuilds so `buildNode` re-tokenizes
2958
- // with the current encoder. See `tokenizerChanged` on the options.
2959
- !wctx.opts.tokenizerChanged && wctx.opts.prior !== null && priorNode !== void 0 && priorNode.bodyHash === bodyHash && priorNode.frontmatterHash === frontmatterHash;
3176
+ const nodeHashCacheEligible = isNodeHashCacheEligible(wctx, priorNode, bodyHash, frontmatterHash);
3177
+ await dispatchNode(
3178
+ { raw, provider, kind, bodyHash, frontmatterHash, nodeHashCacheEligible, priorNode },
3179
+ wctx,
3180
+ accum,
3181
+ nextIndex
3182
+ );
3183
+ return true;
3184
+ }
3185
+ function isNodeHashCacheEligible(wctx, priorNode, bodyHash, frontmatterHash) {
3186
+ return wctx.opts.enableCache && !wctx.opts.tokenizerChanged && wctx.opts.prior !== null && priorNode !== void 0 && priorNode.bodyHash === bodyHash && priorNode.frontmatterHash === frontmatterHash;
3187
+ }
3188
+ async function handleUnchangedRawNode(raw, provider, wctx, accum, claimedPaths, nextIndex) {
3189
+ const prior = wctx.priorNodesByPath.get(raw.path);
3190
+ if (prior) {
3191
+ claimedPaths.add(raw.path);
3192
+ await dispatchNode(
3193
+ {
3194
+ raw,
3195
+ provider,
3196
+ kind: prior.kind,
3197
+ bodyHash: prior.bodyHash,
3198
+ frontmatterHash: prior.frontmatterHash,
3199
+ nodeHashCacheEligible: true,
3200
+ priorNode: prior,
3201
+ ensureBody: () => rereadInto(raw)
3202
+ },
3203
+ wctx,
3204
+ accum,
3205
+ nextIndex
3206
+ );
3207
+ return true;
3208
+ }
3209
+ await rereadInto(raw);
3210
+ return false;
3211
+ }
3212
+ async function dispatchNode(args, wctx, accum, nextIndex) {
2960
3213
  const sidecarResolution = resolveSidecarOverlay(
2961
- raw.path,
2962
- raw.path,
3214
+ args.raw.path,
3215
+ args.raw.path,
2963
3216
  wctx.opts.roots,
2964
- bodyHash,
2965
- frontmatterHash
3217
+ args.bodyHash,
3218
+ args.frontmatterHash
2966
3219
  );
2967
3220
  const sidecarAnnotationsHash = sha256(
2968
3221
  canonicalSidecarAnnotations(sidecarResolution.overlay.annotations)
2969
3222
  );
2970
3223
  const cacheDecision = computeCacheDecision({
2971
3224
  extractors: wctx.opts.extractors,
2972
- kind,
3225
+ kind: args.kind,
2973
3226
  activeProvider: wctx.opts.activeProvider,
2974
- nodePath: raw.path,
2975
- bodyHash,
3227
+ nodePath: args.raw.path,
3228
+ bodyHash: args.bodyHash,
2976
3229
  sidecarAnnotationsHash,
2977
- nodeHashCacheEligible,
3230
+ nodeHashCacheEligible: args.nodeHashCacheEligible,
2978
3231
  priorExtractorRuns: wctx.opts.priorExtractorRuns
2979
3232
  });
2980
3233
  const ctx = {
2981
- raw,
2982
- provider,
2983
- kind,
2984
- bodyHash,
2985
- frontmatterHash,
3234
+ raw: args.raw,
3235
+ provider: args.provider,
3236
+ kind: args.kind,
3237
+ bodyHash: args.bodyHash,
3238
+ frontmatterHash: args.frontmatterHash,
2986
3239
  sidecarResolution,
2987
3240
  sidecarAnnotationsHash,
2988
- nodeHashCacheEligible,
3241
+ nodeHashCacheEligible: args.nodeHashCacheEligible,
2989
3242
  cacheDecision,
2990
- priorNode,
3243
+ priorNode: args.priorNode,
2991
3244
  index: nextIndex
2992
3245
  };
2993
- if (cacheDecision.fullCacheHit && priorNode) {
3246
+ if (cacheDecision.fullCacheHit && args.priorNode) {
2994
3247
  applyFullCacheHit(ctx, wctx, accum);
2995
3248
  } else {
3249
+ if (args.ensureBody) await args.ensureBody();
2996
3250
  await applyExtractPath(ctx, wctx, accum);
2997
3251
  }
2998
- return true;
3252
+ }
3253
+ async function rereadInto(raw) {
3254
+ if (!raw.reread) return;
3255
+ const re = await raw.reread();
3256
+ raw.body = re.body;
3257
+ raw.frontmatter = re.frontmatter;
3258
+ raw.frontmatterRaw = re.frontmatterRaw;
3259
+ raw.unchanged = false;
3260
+ if (re.parseIssues) raw.parseIssues = re.parseIssues;
2999
3261
  }
3000
3262
  function attachSidecar(node, resolution, sidecarRoots) {
3001
3263
  node.sidecar = resolution.overlay;
@@ -3208,9 +3470,18 @@ async function runScanInternal(_kernel, options) {
3208
3470
  providerFrontmatter: setup.providerFrontmatter,
3209
3471
  pluginStores: options.pluginStores,
3210
3472
  activeProvider: activeProviderId,
3211
- recommendedNodeLimit: options.recommendedNodeLimit ?? 256,
3212
- overrideMaxNodes: options.overrideMaxNodes ?? null,
3213
- ...options.maxFileSizeBytes !== void 0 ? { maxFileSizeBytes: options.maxFileSizeBytes } : {}
3473
+ scanCeiling: options.scanCeiling ?? 5e4,
3474
+ overrideScanCeiling: options.overrideScanCeiling ?? null,
3475
+ maxRenderNodes: options.maxRenderNodes ?? 256,
3476
+ overrideMaxRenderNodes: options.overrideMaxRenderNodes ?? null,
3477
+ ...options.maxFileSizeBytes !== void 0 ? { maxFileSizeBytes: options.maxFileSizeBytes } : {},
3478
+ // Watcher incremental fast path: only honoured when a prior exists,
3479
+ // cache reuse is on, and the tokenizer is unchanged (else a scoped
3480
+ // read would skip nodes whose token counts must be recomputed). The
3481
+ // walker enumerates from the prior snapshot + reads only the changed
3482
+ // files instead of traversing the corpus. Falls back to the full
3483
+ // traversal + mtime-gate when the gate does not hold.
3484
+ ...options.incrementalChangedPaths !== void 0 && prior !== null && setup.enableCache && !tokenizerChanged ? { incrementalChangedPaths: options.incrementalChangedPaths } : {}
3214
3485
  });
3215
3486
  const activeProvider = activeProviderId ? exts.providers.find((p) => p.id === activeProviderId) ?? null : null;
3216
3487
  const resolved = resolveSignals({
@@ -3408,8 +3679,9 @@ function buildScanReturn(walked, issues, renameOps, stats, options, setup, linkS
3408
3679
  providers: setup.exts.providers.map((a) => a.id),
3409
3680
  scannedBy: SCANNED_BY,
3410
3681
  tokenizer: setup.tokenizer,
3411
- recommendedNodeLimit: walked.recommendedNodeLimit,
3412
- overrideMaxNodes: walked.overrideMaxNodes,
3682
+ scanCeiling: walked.scanCeiling,
3683
+ scanTruncated: walked.scanTruncated,
3684
+ maxRenderNodes: walked.maxRenderNodes,
3413
3685
  oversizedFiles: walked.oversizedFiles,
3414
3686
  nodes: walked.nodes,
3415
3687
  links: walked.internalLinks,
@@ -3438,7 +3710,7 @@ function validateRoots(roots) {
3438
3710
  function resolveActiveProviderOption(optionValue, roots, providers) {
3439
3711
  if (optionValue !== void 0) return optionValue;
3440
3712
  for (const root of roots) {
3441
- const absRoot = isAbsolute3(root) ? root : resolve9(root);
3713
+ const absRoot = isAbsolute5(root) ? root : resolve11(root);
3442
3714
  if (!existsSync10(absRoot)) continue;
3443
3715
  const detected = detectProvidersFromFilesystem(absRoot, providers)[0] ?? null;
3444
3716
  if (detected !== null) return detected;
@@ -3447,10 +3719,10 @@ function resolveActiveProviderOption(optionValue, roots, providers) {
3447
3719
  }
3448
3720
 
3449
3721
  // kernel/scan/watcher.ts
3450
- import { resolve as resolve10, relative as relative4, sep as sep3 } from "path";
3722
+ import { resolve as resolve12, relative as relative4, sep as sep3 } from "path";
3451
3723
  import chokidar from "chokidar";
3452
3724
  function createChokidarWatcher(opts) {
3453
- const absRoots = opts.roots.map((r) => resolve10(opts.cwd, r));
3725
+ const absRoots = opts.roots.map((r) => resolve12(opts.cwd, r));
3454
3726
  const ignoreFilterOpt = opts.ignoreFilter;
3455
3727
  const getFilter = ignoreFilterOpt === void 0 ? void 0 : typeof ignoreFilterOpt === "function" ? ignoreFilterOpt : () => ignoreFilterOpt;
3456
3728
  const ignored = getFilter ? (path) => {
@@ -3851,4 +4123,4 @@ export {
3851
4123
  runScanWithRenames
3852
4124
  };
3853
4125
  //# sourceMappingURL=index.js.map
3854
- //# debugId=5077263a-bf07-58af-b37d-ea6ab8f9a27c
4126
+ //# debugId=ca2610ab-aae0-5366-8634-73a2d8fd3eda
@@ -289,14 +289,24 @@ CREATE TABLE scan_meta (
289
289
  stats_files_walked INTEGER NOT NULL,
290
290
  stats_files_skipped INTEGER NOT NULL,
291
291
  stats_duration_ms INTEGER NOT NULL,
292
- -- Node-cap envelope (see spec/cli-contract.md §Scan, `scan.maxNodes` setting
293
- -- and `--max-nodes` flag). `recommended_node_limit` is the effective default
294
- -- (from `scan.maxNodes`) that produced this scan; the UI raises a persistent
295
- -- "oversized graph" banner when `stats_files_walked >= recommended_node_limit`.
296
- -- `override_max_nodes` is the per-invocation override (when `--max-nodes <N>`
297
- -- was passed) or NULL when the value above came from the setting.
298
- recommended_node_limit INTEGER NOT NULL,
299
- override_max_nodes INTEGER,
292
+ -- Scan-ceiling vs render-cap envelope (see spec/cli-contract.md §Scan).
293
+ -- Two independent knobs:
294
+ -- - `scan_ceiling` is the effective WALK-INTAKE ceiling that produced
295
+ -- this scan (`scan.maxScan` setting, default 50000, or the
296
+ -- `--max-scan <N>` override). The walker walks, parses, analyzes, and
297
+ -- reference-validates the full corpus up to this number, so references
298
+ -- resolve across the whole project regardless of how many nodes the
299
+ -- map renders.
300
+ -- - `scan_truncated` is 1 when the walker reached `scan_ceiling` and
301
+ -- dropped files (in stable provider-walker order), 0 otherwise. The UI
302
+ -- raises a persistent banner pointing at the `.skillmapignore` editor.
303
+ -- - `max_render_nodes` is the effective MAP RENDER cap (`scan.maxNodes`
304
+ -- setting, default 256, or the `--max-nodes <N>` override). Pure
305
+ -- metadata: it does NOT bound the walk, only the graph projection the
306
+ -- UI draws onto the canvas.
307
+ scan_ceiling INTEGER NOT NULL,
308
+ scan_truncated INTEGER NOT NULL DEFAULT 0,
309
+ max_render_nodes INTEGER NOT NULL,
300
310
  -- File-size skip envelope (see spec/cli-contract.md §Scan, `scan.maxFileSizeBytes`
301
311
  -- setting, default 1 MiB). `files_oversized` is the count of files the walker
302
312
  -- skipped before reading because they exceeded the limit (= `stats.filesOversized`);