@skill-map/cli 0.61.4 → 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 (43) hide show
  1. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/portfolio/en/__PROVIDER__/agents/content-editor.md +1 -1
  2. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/portfolio/es/__PROVIDER__/agents/content-editor.md +1 -1
  3. package/dist/cli/tutorial/sm-tutorial/references/_manifest.json +2 -7
  4. package/dist/cli/tutorial/sm-tutorial/references/_manifest.yml +1 -2
  5. package/dist/cli/tutorial/sm-tutorial/references/part-connect-harness.md +6 -0
  6. package/dist/cli/tutorial/sm-tutorial/references/part-daily-loop.md +40 -34
  7. package/dist/cli.js +1346 -479
  8. package/dist/index.js +368 -96
  9. package/dist/kernel/index.d.ts +232 -25
  10. package/dist/kernel/index.js +368 -96
  11. package/dist/migrations/001_initial.sql +18 -8
  12. package/dist/ui/{chunk-4N3NRZEH.js → chunk-276RLZR4.js} +1 -1
  13. package/dist/ui/{chunk-MGWGV4VD.js → chunk-34ZZDYNQ.js} +1 -1
  14. package/dist/ui/chunk-56CBK7LB.js +1 -0
  15. package/dist/ui/{chunk-OVVTCPBJ.js → chunk-7ANZW2OI.js} +1 -1
  16. package/dist/ui/chunk-BJ6X6WBO.js +4 -0
  17. package/dist/ui/{chunk-5SSKJ7AM.js → chunk-BOVJVOLH.js} +1 -1
  18. package/dist/ui/chunk-C42H2UHU.js +3 -0
  19. package/dist/ui/{chunk-GKQA75EF.js → chunk-CJURGJTN.js} +1 -1
  20. package/dist/ui/chunk-CM4YB7L4.js +2 -0
  21. package/dist/ui/{chunk-Q4PXVDJA.js → chunk-CZSLV6YD.js} +1 -1
  22. package/dist/ui/{chunk-7X3DZNG4.js → chunk-DLYJHLJX.js} +2 -2
  23. package/dist/ui/chunk-ECKRC6XD.js +1843 -0
  24. package/dist/ui/{chunk-JTCIY3SL.js → chunk-FC22ZJQZ.js} +1 -1
  25. package/dist/ui/{chunk-FRUHVCND.js → chunk-FYATUDAH.js} +1 -1
  26. package/dist/ui/chunk-IYC5ZW4L.js +2 -0
  27. package/dist/ui/{chunk-MBBJJEUX.js → chunk-JZ2YF7EL.js} +1 -1
  28. package/dist/ui/{chunk-HQ6M2HXK.js → chunk-LPDD2DHE.js} +1 -1
  29. package/dist/ui/{chunk-I52OQIZQ.js → chunk-NC3HOVDG.js} +1 -1
  30. package/dist/ui/{chunk-N6MUHKWR.js → chunk-UTRZTB6V.js} +1 -1
  31. package/dist/ui/chunk-VHEFRMK3.js +1 -0
  32. package/dist/ui/chunk-Y2Z26SRI.js +1 -0
  33. package/dist/ui/index.html +1 -1
  34. package/dist/ui/main-RW5YGD6H.js +4 -0
  35. package/migrations/001_initial.sql +18 -8
  36. package/package.json +2 -2
  37. package/dist/ui/chunk-4VAXWCM2.js +0 -3
  38. package/dist/ui/chunk-6WSKVPDL.js +0 -1843
  39. package/dist/ui/chunk-7VUEZZFJ.js +0 -1
  40. package/dist/ui/chunk-AKKFFP7Y.js +0 -1
  41. package/dist/ui/chunk-L34EUS75.js +0 -2
  42. package/dist/ui/chunk-ZYPXVXYF.js +0 -4
  43. package/dist/ui/main-36BUYCEI.js +0 -4
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // cli/entry.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]="d6e5d903-0942-5718-94b3-53a96de15b21")}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]="d7ccbbda-dc56-582c-966a-4822e4bdcd83")}catch(e){}}();
4
4
  import { existsSync as existsSync33 } from "fs";
5
5
  import { Builtins, Cli as Cli2 } from "clipanion";
6
6
 
@@ -250,7 +250,7 @@ function bucketByKind(kind, instance, bag) {
250
250
  // package.json
251
251
  var package_default = {
252
252
  name: "@skill-map/cli",
253
- version: "0.61.4",
253
+ version: "0.62.0",
254
254
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
255
255
  license: "MIT",
256
256
  type: "module",
@@ -812,6 +812,15 @@ function extractCodeRegions(input) {
812
812
  }
813
813
  return out;
814
814
  }
815
+ var HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
816
+ var HTML_TAG_RE = /<\/?[a-zA-Z][a-zA-Z0-9-]*(?:\s+(?:"[^"]*"|'[^']*'|[^"'>])*)?\/?\s*>/g;
817
+ function stripHtml(input) {
818
+ if (!input) return input;
819
+ return input.replace(HTML_COMMENT_RE, (m) => blank(m)).replace(HTML_TAG_RE, (m) => blank(m));
820
+ }
821
+ function stripCodeAndHtml(input) {
822
+ return stripHtml(stripCodeBlocks(input));
823
+ }
815
824
  function stripFences(input) {
816
825
  const out = [];
817
826
  const lines = input.split("\n");
@@ -896,7 +905,7 @@ var atDirectiveExtractor = {
896
905
  extract(ctx) {
897
906
  const seenMentions = /* @__PURE__ */ new Set();
898
907
  const seenReferences = /* @__PURE__ */ new Set();
899
- const body = stripCodeBlocks(ctx.body);
908
+ const body = stripCodeAndHtml(ctx.body);
900
909
  const lineStarts = computeLineStarts(body);
901
910
  const sourceDir = pathPosix.dirname(ctx.node.path);
902
911
  for (const match of body.matchAll(AT_RE)) {
@@ -982,7 +991,7 @@ var slashCommandExtractor = {
982
991
  precondition: { provider: ["claude"] },
983
992
  extract(ctx) {
984
993
  const seen = /* @__PURE__ */ new Set();
985
- const body = stripCodeBlocks(ctx.body);
994
+ const body = stripCodeAndHtml(ctx.body);
986
995
  const lineStarts = computeLineStarts(body);
987
996
  for (const match of body.matchAll(SLASH_RE)) {
988
997
  const original = match[1];
@@ -1566,7 +1575,7 @@ var externalUrlCounterExtractor = {
1566
1575
  extract(ctx) {
1567
1576
  const seen = /* @__PURE__ */ new Set();
1568
1577
  const ignoredDomains = readIgnoredDomains(ctx.settings[SETTING_IGNORED_DOMAINS]);
1569
- const body = stripCodeBlocks(ctx.body);
1578
+ const body = stripCodeAndHtml(ctx.body);
1570
1579
  const lineStarts = computeLineStarts(body);
1571
1580
  for (const match of body.matchAll(URL_RE)) {
1572
1581
  const original = stripTrailingPunctuation(match[0]);
@@ -1638,7 +1647,7 @@ var markdownLinkExtractor = {
1638
1647
  scope: "body",
1639
1648
  extract(ctx) {
1640
1649
  const seen = /* @__PURE__ */ new Set();
1641
- const body = stripCodeBlocks(ctx.body);
1650
+ const body = stripCodeAndHtml(ctx.body);
1642
1651
  const lineStarts = computeLineStarts(body);
1643
1652
  const sourceDir = pathPosix3.dirname(ctx.node.path);
1644
1653
  for (const match of body.matchAll(LINK_RE)) {
@@ -5312,6 +5321,7 @@ var defaults_default = {
5312
5321
  tokenize: true,
5313
5322
  strict: false,
5314
5323
  maxFileSizeBytes: 1048576,
5324
+ maxScan: 5e4,
5315
5325
  maxNodes: 256,
5316
5326
  watch: {
5317
5327
  debounceMs: 300
@@ -6207,7 +6217,7 @@ import { existsSync as existsSync11 } from "fs";
6207
6217
  import { mkdirSync as mkdirSync4 } from "fs";
6208
6218
  import { dirname as dirname8, resolve as resolve11 } from "path";
6209
6219
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
6210
- import { CamelCasePlugin, Kysely, sql as sql3 } from "kysely";
6220
+ import { CamelCasePlugin, Kysely, sql as sql4 } from "kysely";
6211
6221
 
6212
6222
  // kernel/i18n/storage.texts.ts
6213
6223
  var STORAGE_TEXTS = {
@@ -6304,8 +6314,8 @@ var NodeSqliteConnection = class {
6304
6314
  constructor(db) {
6305
6315
  this.#db = db;
6306
6316
  }
6307
- exec(sql4) {
6308
- this.#db.exec(sql4);
6317
+ exec(sql5) {
6318
+ this.#db.exec(sql5);
6309
6319
  }
6310
6320
  async executeQuery(query) {
6311
6321
  const stmt = this.#db.prepare(query.sql);
@@ -6338,7 +6348,7 @@ var AsyncMutex = class {
6338
6348
  this.#locked = true;
6339
6349
  return;
6340
6350
  }
6341
- await new Promise((resolve40) => this.#waiters.push(resolve40));
6351
+ await new Promise((resolve43) => this.#waiters.push(resolve43));
6342
6352
  this.#locked = true;
6343
6353
  }
6344
6354
  unlock() {
@@ -6882,10 +6892,10 @@ function applyOneMigration(db, migration) {
6882
6892
  tx(MIGRATIONS_TEXTS.invalidVersion, { value: String(migration.version) })
6883
6893
  );
6884
6894
  }
6885
- const sql4 = readFileSync8(migration.filePath, "utf8");
6895
+ const sql5 = readFileSync8(migration.filePath, "utf8");
6886
6896
  try {
6887
6897
  db.exec("BEGIN");
6888
- db.exec(sql4);
6898
+ db.exec(sql5);
6889
6899
  db.prepare(
6890
6900
  `INSERT INTO config_schema_versions (scope, owner_id, version, description, applied_at)
6891
6901
  VALUES ('kernel', 'kernel', ?, ?, ?)`
@@ -6929,37 +6939,37 @@ import { join as join6 } from "path";
6929
6939
  function normalizePluginId(id) {
6930
6940
  return id.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
6931
6941
  }
6932
- function stripComments(sql4) {
6933
- return sql4.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/--[^\n\r]*/g, " ");
6942
+ function stripComments(sql5) {
6943
+ return sql5.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/--[^\n\r]*/g, " ");
6934
6944
  }
6935
- function detectCommentMarkerInLiteral(sql4) {
6945
+ function detectCommentMarkerInLiteral(sql5) {
6936
6946
  let i = 0;
6937
- while (i < sql4.length) {
6938
- const ch = sql4[i];
6947
+ while (i < sql5.length) {
6948
+ const ch = sql5[i];
6939
6949
  if (ch === "'") {
6940
- const end = scanCheckedLiteral(sql4, i + 1, "'", "string literal");
6950
+ const end = scanCheckedLiteral(sql5, i + 1, "'", "string literal");
6941
6951
  if (typeof end === "string") return end;
6942
6952
  i = end;
6943
6953
  } else if (ch === '"') {
6944
- const end = scanCheckedLiteral(sql4, i + 1, '"', "double-quoted identifier");
6954
+ const end = scanCheckedLiteral(sql5, i + 1, '"', "double-quoted identifier");
6945
6955
  if (typeof end === "string") return end;
6946
6956
  i = end;
6947
6957
  } else if (ch === "`") {
6948
- i = skipUntilCloser(sql4, i + 1, "`");
6958
+ i = skipUntilCloser(sql5, i + 1, "`");
6949
6959
  } else if (ch === "[") {
6950
- i = skipUntilCloser(sql4, i + 1, "]");
6960
+ i = skipUntilCloser(sql5, i + 1, "]");
6951
6961
  } else {
6952
6962
  i++;
6953
6963
  }
6954
6964
  }
6955
6965
  return null;
6956
6966
  }
6957
- function scanCheckedLiteral(sql4, start, closer, label) {
6967
+ function scanCheckedLiteral(sql5, start, closer, label) {
6958
6968
  const allowStack = closer === "'";
6959
6969
  let i = start;
6960
- while (i < sql4.length) {
6961
- const ch = sql4[i];
6962
- const next = sql4[i + 1];
6970
+ while (i < sql5.length) {
6971
+ const ch = sql5[i];
6972
+ const next = sql5[i + 1];
6963
6973
  if (allowStack && ch === closer && next === closer) {
6964
6974
  i += 2;
6965
6975
  continue;
@@ -6980,10 +6990,10 @@ function findCommentMarker(ch, next, label) {
6980
6990
  }
6981
6991
  return null;
6982
6992
  }
6983
- function skipUntilCloser(sql4, start, closer) {
6993
+ function skipUntilCloser(sql5, start, closer) {
6984
6994
  let i = start;
6985
- while (i < sql4.length) {
6986
- if (sql4[i] === closer) return i + 1;
6995
+ while (i < sql5.length) {
6996
+ if (sql5[i] === closer) return i + 1;
6987
6997
  i++;
6988
6998
  }
6989
6999
  return i;
@@ -7094,14 +7104,14 @@ function stripIdentifierWrapper(raw) {
7094
7104
  return raw;
7095
7105
  }
7096
7106
  var QUOTE_OPENERS = /* @__PURE__ */ new Set(["'", '"', "`", "["]);
7097
- function splitStatements(sql4) {
7107
+ function splitStatements(sql5) {
7098
7108
  const out = [];
7099
7109
  let current = "";
7100
7110
  let i = 0;
7101
- while (i < sql4.length) {
7102
- const ch = sql4[i];
7111
+ while (i < sql5.length) {
7112
+ const ch = sql5[i];
7103
7113
  if (QUOTE_OPENERS.has(ch)) {
7104
- const consumed = copyQuotedRegion(sql4, i, ch);
7114
+ const consumed = copyQuotedRegion(sql5, i, ch);
7105
7115
  current += consumed.text;
7106
7116
  i = consumed.next;
7107
7117
  continue;
@@ -7120,16 +7130,16 @@ function splitStatements(sql4) {
7120
7130
  if (tail.length > 0) out.push(tail);
7121
7131
  return out;
7122
7132
  }
7123
- function copyQuotedRegion(sql4, start, opener) {
7133
+ function copyQuotedRegion(sql5, start, opener) {
7124
7134
  const closer = opener === "[" ? "]" : opener;
7125
7135
  const allowStack = opener === "'";
7126
7136
  let text = opener;
7127
7137
  let i = start + 1;
7128
- while (i < sql4.length) {
7129
- const ch = sql4[i];
7138
+ while (i < sql5.length) {
7139
+ const ch = sql5[i];
7130
7140
  text += ch;
7131
7141
  if (ch === closer) {
7132
- if (allowStack && sql4[i + 1] === closer) {
7142
+ if (allowStack && sql5[i + 1] === closer) {
7133
7143
  text += closer;
7134
7144
  i += 2;
7135
7145
  continue;
@@ -7140,11 +7150,11 @@ function copyQuotedRegion(sql4, start, opener) {
7140
7150
  }
7141
7151
  return { text, next: i };
7142
7152
  }
7143
- function validatePluginMigrationSql(sql4, normalizedId) {
7144
- const literalIssue = detectCommentMarkerInLiteral(sql4);
7153
+ function validatePluginMigrationSql(sql5, normalizedId) {
7154
+ const literalIssue = detectCommentMarkerInLiteral(sql5);
7145
7155
  if (literalIssue) return { ok: false, violations: [literalIssue] };
7146
7156
  const prefix = `plugin_${normalizedId}_`;
7147
- const stripped = stripComments(sql4);
7157
+ const stripped = stripComments(sql5);
7148
7158
  const violations = [
7149
7159
  ...detectForbiddenKeywords(stripped),
7150
7160
  ...detectStatementViolations(stripped, prefix)
@@ -7305,9 +7315,9 @@ function applyPluginMigrations(db, plugin, options = {}, files = discoverPluginM
7305
7315
  function preflightValidateAll(pending, normalizedId, pluginId) {
7306
7316
  const sources = /* @__PURE__ */ new Map();
7307
7317
  for (const m of pending) {
7308
- const sql4 = readFileSync9(m.filePath, "utf8");
7309
- sources.set(m.filePath, sql4);
7310
- const result = validatePluginMigrationSql(sql4, normalizedId);
7318
+ const sql5 = readFileSync9(m.filePath, "utf8");
7319
+ sources.set(m.filePath, sql5);
7320
+ const result = validatePluginMigrationSql(sql5, normalizedId);
7311
7321
  if (!result.ok) {
7312
7322
  throw new Error(
7313
7323
  `Plugin ${pluginId}: migration ${formatMigrationName(m)} failed validation:
@@ -7317,8 +7327,8 @@ function preflightValidateAll(pending, normalizedId, pluginId) {
7317
7327
  }
7318
7328
  return sources;
7319
7329
  }
7320
- function applyOnePluginMigration(db, plugin, migration, sql4, normalizedId) {
7321
- const result = validatePluginMigrationSql(sql4, normalizedId);
7330
+ function applyOnePluginMigration(db, plugin, migration, sql5, normalizedId) {
7331
+ const result = validatePluginMigrationSql(sql5, normalizedId);
7322
7332
  if (!result.ok) {
7323
7333
  throw new Error(
7324
7334
  `Plugin ${plugin.id}: migration ${formatMigrationName(migration)} failed Layer-2 validation:
@@ -7327,7 +7337,7 @@ function applyOnePluginMigration(db, plugin, migration, sql4, normalizedId) {
7327
7337
  }
7328
7338
  try {
7329
7339
  db.exec("BEGIN");
7330
- db.exec(sql4);
7340
+ db.exec(sql5);
7331
7341
  db.prepare(
7332
7342
  `INSERT INTO config_schema_versions (scope, owner_id, version, description, applied_at)
7333
7343
  VALUES ('plugin', ?, ?, ?, ?)`
@@ -7386,6 +7396,9 @@ async function loadPluginOverrideMap(db) {
7386
7396
  return out;
7387
7397
  }
7388
7398
 
7399
+ // kernel/adapters/sqlite/scan-load.ts
7400
+ import { sql as sql2 } from "kysely";
7401
+
7389
7402
  // kernel/util/enum-parsers.ts
7390
7403
  var STABILITY_VALUES2 = Object.freeze([
7391
7404
  "experimental",
@@ -7482,40 +7495,11 @@ async function loadScanResult(db) {
7482
7495
  );
7483
7496
  }
7484
7497
  if (metaRow) {
7485
- const scannedBy = {
7486
- name: metaRow.scannedByName,
7487
- version: metaRow.scannedByVersion,
7488
- specVersion: metaRow.scannedBySpecVersion
7489
- };
7490
- const oversizedFiles = parseJsonArray(metaRow.oversizedFilesJson);
7491
- return {
7492
- schemaVersion: 1,
7493
- scannedAt: metaRow.scannedAt,
7494
- roots: parseJsonArray(metaRow.rootsJson),
7495
- providers: parseJsonArray(metaRow.providersJson),
7496
- scannedBy,
7497
- // Resolved encoder of the prior scan (see project-config.schema.json
7498
- // §tokenizer). NULL column → `undefined` domain field; the
7499
- // orchestrator's tokenizer-change check compares this against the
7500
- // freshly-resolved encoder and treats a missing prior value as a
7501
- // change (forcing a token recompute).
7502
- ...metaRow.tokenizer !== null ? { tokenizer: metaRow.tokenizer } : {},
7503
- recommendedNodeLimit: metaRow.recommendedNodeLimit,
7504
- overrideMaxNodes: metaRow.overrideMaxNodes,
7505
- oversizedFiles,
7506
- nodes,
7507
- links,
7508
- issues,
7509
- stats: {
7510
- filesWalked: metaRow.statsFilesWalked,
7511
- filesSkipped: metaRow.statsFilesSkipped,
7512
- filesOversized: metaRow.filesOversized,
7513
- nodesCount: nodes.length,
7514
- linksCount: links.length,
7515
- issuesCount: issues.length,
7516
- durationMs: metaRow.statsDurationMs
7517
- }
7518
- };
7498
+ return buildScanResultFromMeta(metaRow, nodes, links, issues, {
7499
+ nodesCount: nodes.length,
7500
+ linksCount: links.length,
7501
+ issuesCount: issues.length
7502
+ });
7519
7503
  }
7520
7504
  let scannedAt = 0;
7521
7505
  for (const row of nodeRows) {
@@ -7527,11 +7511,13 @@ async function loadScanResult(db) {
7527
7511
  scannedAt,
7528
7512
  roots: ["."],
7529
7513
  providers: [],
7530
- // Synthetic envelope, default to the design cap (256) so SPA reads
7531
- // the same shape across cold-boot and pre-cap-aware DBs. A real
7532
- // scan overwrites scan_meta with the live values on next run.
7533
- recommendedNodeLimit: 256,
7534
- overrideMaxNodes: null,
7514
+ // Synthetic envelope, default to the design knobs (corpus ceiling
7515
+ // 50000, render cap 256, not truncated) so the SPA reads the same
7516
+ // shape across cold-boot and never-scanned scopes. A real scan
7517
+ // overwrites scan_meta with the live values on next run.
7518
+ scanCeiling: 5e4,
7519
+ scanTruncated: false,
7520
+ maxRenderNodes: 256,
7535
7521
  oversizedFiles: [],
7536
7522
  nodes,
7537
7523
  links,
@@ -7547,6 +7533,175 @@ async function loadScanResult(db) {
7547
7533
  }
7548
7534
  };
7549
7535
  }
7536
+ function buildScanResultFromMeta(metaRow, nodes, links, issues, counts) {
7537
+ const scannedBy = {
7538
+ name: metaRow.scannedByName,
7539
+ version: metaRow.scannedByVersion,
7540
+ specVersion: metaRow.scannedBySpecVersion
7541
+ };
7542
+ const oversizedFiles = parseJsonArray(metaRow.oversizedFilesJson);
7543
+ return {
7544
+ schemaVersion: 1,
7545
+ scannedAt: metaRow.scannedAt,
7546
+ roots: parseJsonArray(metaRow.rootsJson),
7547
+ providers: parseJsonArray(metaRow.providersJson),
7548
+ scannedBy,
7549
+ // Resolved encoder of the prior scan (see project-config.schema.json
7550
+ // §tokenizer). A NULL column maps to an absent domain field; the
7551
+ // orchestrator's tokenizer-change check treats a missing prior value
7552
+ // as a change (forcing a token recompute).
7553
+ ...metaRow.tokenizer !== null ? { tokenizer: metaRow.tokenizer } : {},
7554
+ scanCeiling: metaRow.scanCeiling,
7555
+ scanTruncated: metaRow.scanTruncated === 1,
7556
+ maxRenderNodes: metaRow.maxRenderNodes,
7557
+ oversizedFiles,
7558
+ nodes,
7559
+ links,
7560
+ issues,
7561
+ stats: {
7562
+ filesWalked: metaRow.statsFilesWalked,
7563
+ filesSkipped: metaRow.statsFilesSkipped,
7564
+ filesOversized: metaRow.filesOversized,
7565
+ nodesCount: counts.nodesCount,
7566
+ linksCount: counts.linksCount,
7567
+ issuesCount: counts.issuesCount,
7568
+ durationMs: metaRow.statsDurationMs
7569
+ }
7570
+ };
7571
+ }
7572
+ async function loadScanMeta(db, counts) {
7573
+ const metaRow = await db.selectFrom("scan_meta").selectAll().executeTakeFirst();
7574
+ const c = {
7575
+ nodesCount: counts.nodes,
7576
+ linksCount: counts.links,
7577
+ issuesCount: counts.issues
7578
+ };
7579
+ if (metaRow) {
7580
+ return buildScanResultFromMeta(metaRow, [], [], [], c);
7581
+ }
7582
+ return {
7583
+ schemaVersion: 1,
7584
+ scannedAt: Date.now(),
7585
+ roots: ["."],
7586
+ providers: [],
7587
+ scanCeiling: 5e4,
7588
+ scanTruncated: false,
7589
+ maxRenderNodes: 256,
7590
+ oversizedFiles: [],
7591
+ nodes: [],
7592
+ links: [],
7593
+ issues: [],
7594
+ stats: {
7595
+ filesWalked: 0,
7596
+ filesSkipped: 0,
7597
+ filesOversized: 0,
7598
+ nodesCount: c.nodesCount,
7599
+ linksCount: c.linksCount,
7600
+ issuesCount: c.issuesCount,
7601
+ durationMs: 0
7602
+ }
7603
+ };
7604
+ }
7605
+ var DEFAULT_MAX_RENDER_NODES = 256;
7606
+ async function loadLiteNodes(db) {
7607
+ const rows = await db.selectFrom("scan_nodes").select([
7608
+ "path",
7609
+ "kind",
7610
+ "linksInCount",
7611
+ "linksOutCount",
7612
+ "tokensTotal",
7613
+ "modifiedAtMs",
7614
+ "sidecarStatus"
7615
+ ]).orderBy("path", "asc").execute();
7616
+ return rows.map((r) => ({
7617
+ path: r.path,
7618
+ kind: r.kind,
7619
+ linksInCount: r.linksInCount,
7620
+ linksOutCount: r.linksOutCount,
7621
+ tokensTotal: r.tokensTotal,
7622
+ modifiedAtMs: r.modifiedAtMs,
7623
+ sidecarStatus: r.sidecarStatus
7624
+ }));
7625
+ }
7626
+ async function loadIssueCountsByPath(db) {
7627
+ const rows = await db.selectFrom(
7628
+ sql2`(
7629
+ SELECT je.value AS value, si.severity AS severity, COUNT(*) AS c
7630
+ FROM scan_issues si, json_each(si.node_ids_json) je
7631
+ WHERE si.severity IN ('error', 'warn')
7632
+ GROUP BY je.value, si.severity
7633
+ )`.as("incidence")
7634
+ ).select(["value", "severity", "c"]).execute();
7635
+ const out = /* @__PURE__ */ new Map();
7636
+ for (const row of rows) {
7637
+ const bucket = out.get(row.value) ?? { error: 0, warn: 0 };
7638
+ if (row.severity === "error") bucket.error = Number(row.c);
7639
+ else if (row.severity === "warn") bucket.warn = Number(row.c);
7640
+ out.set(row.value, bucket);
7641
+ }
7642
+ return out;
7643
+ }
7644
+ async function loadEffectiveMaxRenderNodes(db) {
7645
+ const metaRow = await db.selectFrom("scan_meta").select(["maxRenderNodes"]).executeTakeFirst();
7646
+ return metaRow?.maxRenderNodes ?? DEFAULT_MAX_RENDER_NODES;
7647
+ }
7648
+ async function loadBranch(db, prefixes, limit) {
7649
+ const uniquePrefixes = [...new Set(prefixes)];
7650
+ const total = await countBranchNodes(db, uniquePrefixes);
7651
+ const nodeRows = await applyBranchScope(
7652
+ db.selectFrom("scan_nodes").selectAll(),
7653
+ uniquePrefixes
7654
+ ).orderBy("path", "asc").limit(limit).execute();
7655
+ const nodes = nodeRows.map(rowToNode);
7656
+ const pathSet = new Set(nodes.map((n) => n.path));
7657
+ if (pathSet.size === 0) {
7658
+ return { nodes, links: [], issues: [], total, paths: uniquePrefixes };
7659
+ }
7660
+ const paths = [...pathSet];
7661
+ const [linkRows, issueRows] = await Promise.all([
7662
+ db.selectFrom("scan_links").selectAll().where("sourcePath", "in", paths).where("targetPath", "in", paths).execute(),
7663
+ db.selectFrom("scan_issues").selectAll().where(
7664
+ ({ exists, selectFrom }) => exists(
7665
+ selectFrom(
7666
+ sql2`json_each(scan_issues.node_ids_json)`.as("je")
7667
+ ).select(sql2`1`.as("one")).where(sql2.ref("je.value"), "in", paths)
7668
+ )
7669
+ ).execute()
7670
+ ]);
7671
+ return {
7672
+ nodes,
7673
+ links: linkRows.map(rowToLink),
7674
+ issues: issueRows.map(rowToIssue),
7675
+ total,
7676
+ paths: uniquePrefixes
7677
+ };
7678
+ }
7679
+ async function countBranchNodes(db, prefixes) {
7680
+ const row = await applyBranchScope(
7681
+ db.selectFrom("scan_nodes"),
7682
+ prefixes
7683
+ ).select(({ fn }) => fn.countAll().as("c")).executeTakeFirst();
7684
+ return Number(row?.c ?? 0);
7685
+ }
7686
+ function applyBranchScope(query, prefixes) {
7687
+ if (prefixes.length === 0) return query;
7688
+ return query.where(
7689
+ ({ eb, or }) => or(
7690
+ prefixes.map(
7691
+ (prefix) => (
7692
+ // Per-prefix subtree predicate. `prefix || '/%'`: the `/%` lives
7693
+ // in the template, `prefix` binds separately, so no user input is
7694
+ // interpolated into the SQL. The two clauses are ORed so the
7695
+ // prefix matches the folder node itself plus every descendant.
7696
+ eb.or([
7697
+ eb("path", "=", prefix),
7698
+ eb("path", "like", sql2`${prefix} || '/%'`)
7699
+ ])
7700
+ )
7701
+ )
7702
+ )
7703
+ );
7704
+ }
7550
7705
  function rowToNode(row) {
7551
7706
  const bytes = {
7552
7707
  frontmatter: row.bytesFrontmatter,
@@ -7702,7 +7857,7 @@ function parseJsonArray(s) {
7702
7857
  }
7703
7858
 
7704
7859
  // kernel/adapters/sqlite/scan-persistence.ts
7705
- import { sql as sql2 } from "kysely";
7860
+ import { sql as sql3 } from "kysely";
7706
7861
 
7707
7862
  // kernel/adapters/sqlite/contributions.ts
7708
7863
  async function replaceAllScanContributions(trx, contributions, livePaths = /* @__PURE__ */ new Set(), registeredKeys = /* @__PURE__ */ new Set(), freshlyRunTuples = /* @__PURE__ */ new Set()) {
@@ -7767,9 +7922,9 @@ async function sweepPerTupleContributions(trx, contributions, freshlyRunTuples)
7767
7922
  const bufferKeys = buildContributionsBufferKeys(contributions);
7768
7923
  const tuplesByPluginExt = groupFreshlyRunTuplesByPluginExt(freshlyRunTuples);
7769
7924
  for (const [pe, nodes] of tuplesByPluginExt) {
7770
- const sep8 = pe.indexOf("\0");
7771
- if (sep8 < 0) continue;
7772
- await deleteStaleTupleRows(trx, pe.slice(0, sep8), pe.slice(sep8 + 1), [...nodes], bufferKeys);
7925
+ const sep9 = pe.indexOf("\0");
7926
+ if (sep9 < 0) continue;
7927
+ await deleteStaleTupleRows(trx, pe.slice(0, sep9), pe.slice(sep9 + 1), [...nodes], bufferKeys);
7773
7928
  }
7774
7929
  }
7775
7930
  function buildContributionsBufferKeys(contributions) {
@@ -7909,9 +8064,9 @@ function schemaFingerprint(files) {
7909
8064
  const migrations = files ?? discoverMigrations();
7910
8065
  const hash = createHash("sha256");
7911
8066
  for (const m of migrations) {
7912
- const sql4 = existsSync10(m.filePath) ? readFileSync10(m.filePath, "utf8") : "";
7913
- hash.update(`${m.version}\0${m.description}\0${Buffer.byteLength(sql4)}\0`);
7914
- hash.update(sql4);
8067
+ const sql5 = existsSync10(m.filePath) ? readFileSync10(m.filePath, "utf8") : "";
8068
+ hash.update(`${m.version}\0${m.description}\0${Buffer.byteLength(sql5)}\0`);
8069
+ hash.update(sql5);
7915
8070
  }
7916
8071
  const digest = hash.digest("hex");
7917
8072
  if (files === void 0) memoized = digest;
@@ -8017,7 +8172,7 @@ async function persistScanResult(db, result, inputs = {}) {
8017
8172
  await upsertEnrichmentLayer(trx, result, renameOps, enrichments);
8018
8173
  await flagStaleProbabilisticEnrichments(trx, result, enrichments);
8019
8174
  });
8020
- await sql2`PRAGMA wal_checkpoint(TRUNCATE)`.execute(db);
8175
+ await sql3`PRAGMA wal_checkpoint(TRUNCATE)`.execute(db);
8021
8176
  return { renames };
8022
8177
  }
8023
8178
  function resolvePersistInputs(inputs) {
@@ -8072,36 +8227,36 @@ function collectKnownOrphanPaths(issues) {
8072
8227
  }
8073
8228
  return out;
8074
8229
  }
8230
+ var MAX_SQL_VARS = 2e4;
8231
+ async function chunkedInsert(trx, table, rows) {
8232
+ if (rows.length === 0) return;
8233
+ const columns = Object.keys(rows[0]).length || 1;
8234
+ const batchSize = Math.max(1, Math.floor(MAX_SQL_VARS / columns));
8235
+ for (let start = 0; start < rows.length; start += batchSize) {
8236
+ await trx.insertInto(table).values(rows.slice(start, start + batchSize)).execute();
8237
+ }
8238
+ }
8075
8239
  async function replaceAllScanZone(trx, result, scannedAt, extractorRuns) {
8076
8240
  await trx.deleteFrom("scan_issues").execute();
8077
8241
  await trx.deleteFrom("scan_links").execute();
8078
8242
  await trx.deleteFrom("scan_nodes").execute();
8079
8243
  await trx.deleteFrom("scan_meta").execute();
8080
8244
  await trx.deleteFrom("scan_extractor_runs").execute();
8081
- if (result.nodes.length > 0) {
8082
- await trx.insertInto("scan_nodes").values(result.nodes.map((n) => nodeToRow(n, scannedAt))).execute();
8083
- }
8084
- if (result.links.length > 0) {
8085
- await trx.insertInto("scan_links").values(result.links.map(linkToRow)).execute();
8086
- }
8087
- if (result.issues.length > 0) {
8088
- await trx.insertInto("scan_issues").values(result.issues.map(issueToRow)).execute();
8089
- }
8245
+ await chunkedInsert(trx, "scan_nodes", result.nodes.map((n) => nodeToRow(n, scannedAt)));
8246
+ await chunkedInsert(trx, "scan_links", result.links.map(linkToRow));
8247
+ await chunkedInsert(trx, "scan_issues", result.issues.map(issueToRow));
8090
8248
  await trx.insertInto("scan_meta").values(metaToRow(result)).execute();
8091
- if (extractorRuns.length > 0) {
8092
- await trx.insertInto("scan_extractor_runs").values(extractorRuns.map(extractorRunToRow)).execute();
8093
- }
8249
+ await chunkedInsert(trx, "scan_extractor_runs", extractorRuns.map(extractorRunToRow));
8094
8250
  }
8095
8251
  async function upsertEnrichmentLayer(trx, result, renameOps, enrichments) {
8096
8252
  const enrichmentLivePaths = new Set(result.nodes.map((n) => n.path));
8097
8253
  for (const op of renameOps) {
8098
8254
  await trx.updateTable("node_enrichments").set({ nodePath: op.to }).where("nodePath", "=", op.from).execute();
8099
8255
  }
8100
- if (enrichmentLivePaths.size > 0) {
8101
- const liveList = [...enrichmentLivePaths];
8102
- await trx.deleteFrom("node_enrichments").where("nodePath", "not in", liveList).execute();
8103
- } else {
8104
- await trx.deleteFrom("node_enrichments").execute();
8256
+ const existingEnrichmentPaths = await trx.selectFrom("node_enrichments").select("nodePath").distinct().execute();
8257
+ const deadEnrichmentPaths = existingEnrichmentPaths.map((r) => r.nodePath).filter((p) => !enrichmentLivePaths.has(p));
8258
+ for (let start = 0; start < deadEnrichmentPaths.length; start += MAX_SQL_VARS) {
8259
+ await trx.deleteFrom("node_enrichments").where("nodePath", "in", deadEnrichmentPaths.slice(start, start + MAX_SQL_VARS)).execute();
8105
8260
  }
8106
8261
  for (const enrichment of enrichments) {
8107
8262
  const row = enrichmentToRow(enrichment);
@@ -8286,8 +8441,9 @@ function projectOversizedColumns(result) {
8286
8441
  }
8287
8442
  function projectNodeLimitColumns(result) {
8288
8443
  return {
8289
- recommendedNodeLimit: result.recommendedNodeLimit ?? 256,
8290
- overrideMaxNodes: result.overrideMaxNodes ?? null
8444
+ scanCeiling: result.scanCeiling ?? 5e4,
8445
+ scanTruncated: result.scanTruncated ? 1 : 0,
8446
+ maxRenderNodes: result.maxRenderNodes ?? 256
8291
8447
  };
8292
8448
  }
8293
8449
  function extractorRunToRow(record) {
@@ -8462,11 +8618,16 @@ var SqliteStorageAdapter = class {
8462
8618
  this.scans = {
8463
8619
  persist: (result, opts) => persistScansThroughNonTx(this.db, result, opts),
8464
8620
  load: () => loadScanResult(this.db),
8621
+ loadMeta: async () => loadScanMeta(this.db, await countRows(this.db)),
8465
8622
  loadExtractorRuns: () => loadExtractorRuns(this.db),
8466
8623
  loadNodeEnrichments: () => loadNodeEnrichments(this.db),
8467
8624
  countRows: () => countRows(this.db),
8468
8625
  findNodes: (filter) => findNodes(this.db, filter),
8469
- findNode: (path2) => findNode(this.db, path2)
8626
+ findNode: (path2) => findNode(this.db, path2),
8627
+ listLiteNodes: () => loadLiteNodes(this.db),
8628
+ issueCountsByPath: () => loadIssueCountsByPath(this.db),
8629
+ effectiveMaxRenderNodes: () => loadEffectiveMaxRenderNodes(this.db),
8630
+ loadBranch: (prefixes, limit) => loadBranch(this.db, prefixes, limit)
8470
8631
  };
8471
8632
  this.contributions = {
8472
8633
  listForNode: (nodePath) => loadContributionsForNode(this.db, nodePath),
@@ -8601,8 +8762,8 @@ async function findNodes(db, filter) {
8601
8762
  query = query.where(
8602
8763
  ({ exists, selectFrom, ref }) => exists(
8603
8764
  selectFrom(
8604
- sql3`json_each(scan_issues.node_ids_json)`.as("je")
8605
- ).innerJoin("scan_issues", (j) => j.onTrue()).select(sql3`1`.as("one")).whereRef(sql3.ref("je.value"), "=", ref("scan_nodes.path"))
8765
+ sql4`json_each(scan_issues.node_ids_json)`.as("je")
8766
+ ).innerJoin("scan_issues", (j) => j.onTrue()).select(sql4`1`.as("one")).whereRef(sql4.ref("je.value"), "=", ref("scan_nodes.path"))
8606
8767
  )
8607
8768
  );
8608
8769
  }
@@ -8674,8 +8835,8 @@ function applyIssueFilters(query, filter) {
8674
8835
  q = q.where(
8675
8836
  ({ exists, selectFrom }) => exists(
8676
8837
  selectFrom(
8677
- sql3`json_each(scan_issues.node_ids_json)`.as("je")
8678
- ).select(sql3`1`.as("one")).where(sql3.ref("je.value"), "=", target)
8838
+ sql4`json_each(scan_issues.node_ids_json)`.as("je")
8839
+ ).select(sql4`1`.as("one")).where(sql4.ref("je.value"), "=", target)
8679
8840
  )
8680
8841
  );
8681
8842
  }
@@ -8686,14 +8847,14 @@ function applyIssueFilters(query, filter) {
8686
8847
  }
8687
8848
  function applyNodePathsFilter(query, nodePaths) {
8688
8849
  if (nodePaths.length === 0) {
8689
- return query.where(sql3`0`, "=", 1);
8850
+ return query.where(sql4`0`, "=", 1);
8690
8851
  }
8691
8852
  const targets = [...nodePaths];
8692
8853
  return query.where(
8693
8854
  ({ exists, selectFrom }) => exists(
8694
8855
  selectFrom(
8695
- sql3`json_each(scan_issues.node_ids_json)`.as("je")
8696
- ).select(sql3`1`.as("one")).where(sql3.ref("je.value"), "in", targets)
8856
+ sql4`json_each(scan_issues.node_ids_json)`.as("je")
8857
+ ).select(sql4`1`.as("one")).where(sql4.ref("je.value"), "in", targets)
8697
8858
  )
8698
8859
  );
8699
8860
  }
@@ -10632,7 +10793,7 @@ async function buildEnabledResolver(ctx) {
10632
10793
 
10633
10794
  // kernel/scan/walk-content.ts
10634
10795
  import { readFile, readdir, lstat } from "fs/promises";
10635
- import { join as join9, relative as relative2, sep as sep3 } from "path";
10796
+ import { isAbsolute as isAbsolute4, join as join9, relative as relative2, resolve as resolve18, sep as sep3 } from "path";
10636
10797
 
10637
10798
  // kernel/scan/ignore.ts
10638
10799
  import { existsSync as existsSync14, readFileSync as readFileSync14 } from "fs";
@@ -10812,32 +10973,115 @@ async function* walkContent(roots, options) {
10812
10973
  const filter = options.ignoreFilter ?? buildIgnoreFilter();
10813
10974
  const extensions = options.extensions;
10814
10975
  const sizeLimit = buildSizeLimit(options);
10976
+ if (options.scopedPaths !== void 0) {
10977
+ yield* walkScoped(roots, options.scopedPaths, extensions, sizeLimit, parser);
10978
+ return;
10979
+ }
10815
10980
  for (const root of roots) {
10816
10981
  for await (const entry of walkRoot(root, root, filter, extensions, sizeLimit)) {
10817
10982
  const relPath = relative2(root, entry.full).split(sep3).join("/");
10818
- let raw;
10819
- try {
10820
- raw = await readFile(entry.full, "utf8");
10821
- } catch {
10822
- continue;
10823
- }
10824
- const parsed = parser.parse(raw, relPath);
10825
- yield {
10826
- path: relPath,
10827
- body: parsed.body,
10828
- frontmatterRaw: parsed.frontmatterRaw,
10829
- frontmatter: parsed.frontmatter,
10830
- // File mtime from the TOCTOU `lstat` (zero extra syscalls).
10831
- // Threaded onto the persisted `Node` as `modifiedAtMs`.
10832
- modifiedAtMs: entry.modifiedAtMs,
10833
- // Audit L1: forward parser diagnostics (e.g. malformed YAML)
10834
- // through the IRawNode surface so the orchestrator can
10835
- // convert them into warn-level kernel `Issue` rows. Omitted
10836
- // when the parser reported no issues (happy path).
10837
- ...parsed.issues && parsed.issues.length > 0 ? { parseIssues: parsed.issues } : {}
10838
- };
10983
+ const rec = await traversedEntryToNode(entry, relPath, options.priorMtimes, parser);
10984
+ if (rec !== null) yield rec;
10985
+ }
10986
+ }
10987
+ }
10988
+ async function traversedEntryToNode(entry, relPath, priorMtimes, parser) {
10989
+ const priorMtime = priorMtimes?.get(relPath);
10990
+ if (priorMtime !== void 0 && priorMtime === entry.modifiedAtMs) {
10991
+ return buildUnchangedRecord(entry.full, relPath, entry.modifiedAtMs, parser);
10992
+ }
10993
+ const parsed = await readAndParse(entry.full, relPath, parser);
10994
+ if (parsed === null) return null;
10995
+ return {
10996
+ path: relPath,
10997
+ body: parsed.body,
10998
+ frontmatterRaw: parsed.frontmatterRaw,
10999
+ frontmatter: parsed.frontmatter,
11000
+ // File mtime from the TOCTOU `lstat` (zero extra syscalls).
11001
+ // Threaded onto the persisted `Node` as `modifiedAtMs`.
11002
+ modifiedAtMs: entry.modifiedAtMs,
11003
+ // Audit L1: forward parser diagnostics (e.g. malformed YAML)
11004
+ // through the IRawNode surface so the orchestrator can
11005
+ // convert them into warn-level kernel `Issue` rows. Omitted
11006
+ // when the parser reported no issues (happy path).
11007
+ ...parsed.parseIssues ? { parseIssues: parsed.parseIssues } : {}
11008
+ };
11009
+ }
11010
+ function buildUnchangedRecord(full, relPath, modifiedAtMs, parser) {
11011
+ return {
11012
+ path: relPath,
11013
+ body: "",
11014
+ frontmatterRaw: "",
11015
+ frontmatter: {},
11016
+ modifiedAtMs,
11017
+ unchanged: true,
11018
+ reread: async () => {
11019
+ const re = await readAndParse(full, relPath, parser);
11020
+ return re ?? { body: "", frontmatterRaw: "", frontmatter: {} };
10839
11021
  }
11022
+ };
11023
+ }
11024
+ async function* walkScoped(roots, scopedPaths, extensions, sizeLimit, parser) {
11025
+ const absRoots = roots.map((r) => isAbsolute4(r) ? r : resolve18(r));
11026
+ for (const scoped of scopedPaths) {
11027
+ const rec = await scopedPathToNode(scoped, absRoots, extensions, sizeLimit, parser);
11028
+ if (rec !== null) yield rec;
11029
+ }
11030
+ }
11031
+ async function scopedPathToNode(scoped, absRoots, extensions, sizeLimit, parser) {
11032
+ const full = isAbsolute4(scoped) ? scoped : resolve18(scoped);
11033
+ const relPath = relativeFromRoots(full, absRoots);
11034
+ if (relPath === null) return null;
11035
+ if (!hasMatchingExtension(full, extensions)) return null;
11036
+ const s = await statRegularFile(full, relPath, sizeLimit);
11037
+ if (s === null) return null;
11038
+ const parsed = await readAndParse(full, relPath, parser);
11039
+ if (parsed === null) return null;
11040
+ return {
11041
+ path: relPath,
11042
+ body: parsed.body,
11043
+ frontmatterRaw: parsed.frontmatterRaw,
11044
+ frontmatter: parsed.frontmatter,
11045
+ modifiedAtMs: Math.round(s.mtimeMs),
11046
+ ...parsed.parseIssues ? { parseIssues: parsed.parseIssues } : {}
11047
+ };
11048
+ }
11049
+ async function statRegularFile(full, relPath, sizeLimit) {
11050
+ let s;
11051
+ try {
11052
+ s = await lstat(full);
11053
+ } catch {
11054
+ return null;
11055
+ }
11056
+ if (!s.isFile()) return null;
11057
+ if (sizeLimit.maxFileSizeBytes !== void 0 && s.size > sizeLimit.maxFileSizeBytes) {
11058
+ sizeLimit.onOversizedFile?.({ path: relPath, bytes: s.size });
11059
+ return null;
11060
+ }
11061
+ return s;
11062
+ }
11063
+ function relativeFromRoots(full, absRoots) {
11064
+ for (const root of absRoots) {
11065
+ const rel = relative2(root, full);
11066
+ if (rel === "" || rel.startsWith("..") || isAbsolute4(rel)) continue;
11067
+ return rel.split(sep3).join("/");
11068
+ }
11069
+ return null;
11070
+ }
11071
+ async function readAndParse(full, relPath, parser) {
11072
+ let raw;
11073
+ try {
11074
+ raw = await readFile(full, "utf8");
11075
+ } catch {
11076
+ return null;
10840
11077
  }
11078
+ const parsed = parser.parse(raw, relPath);
11079
+ return {
11080
+ body: parsed.body,
11081
+ frontmatterRaw: parsed.frontmatterRaw,
11082
+ frontmatter: parsed.frontmatter,
11083
+ ...parsed.issues && parsed.issues.length > 0 ? { parseIssues: parsed.issues } : {}
11084
+ };
10841
11085
  }
10842
11086
  function buildSizeLimit(options) {
10843
11087
  const sizeLimit = {};
@@ -10894,18 +11138,24 @@ function resolveProviderWalk(provider) {
10894
11138
  return walk3;
10895
11139
  }
10896
11140
  const read = provider.read ?? DEFAULT_READ_CONFIG;
10897
- return (roots, options) => {
10898
- const walkOptions = {
10899
- extensions: read.extensions,
10900
- parser: read.parser
10901
- };
10902
- if (options?.ignoreFilter) walkOptions.ignoreFilter = options.ignoreFilter;
10903
- if (options?.maxFileSizeBytes !== void 0) {
10904
- walkOptions.maxFileSizeBytes = options.maxFileSizeBytes;
10905
- }
10906
- if (options?.onOversizedFile) walkOptions.onOversizedFile = options.onOversizedFile;
10907
- return walkContent(roots, walkOptions);
11141
+ return (roots, options) => walkContent(roots, buildWalkContentOptions(read, options));
11142
+ }
11143
+ function buildWalkContentOptions(read, options) {
11144
+ const walkOptions = {
11145
+ extensions: read.extensions,
11146
+ parser: read.parser
10908
11147
  };
11148
+ if (options) copyOptionalWalkOptions(walkOptions, options);
11149
+ return walkOptions;
11150
+ }
11151
+ function copyOptionalWalkOptions(walkOptions, options) {
11152
+ if (options.ignoreFilter) walkOptions.ignoreFilter = options.ignoreFilter;
11153
+ if (options.maxFileSizeBytes !== void 0) {
11154
+ walkOptions.maxFileSizeBytes = options.maxFileSizeBytes;
11155
+ }
11156
+ if (options.onOversizedFile) walkOptions.onOversizedFile = options.onOversizedFile;
11157
+ if (options.priorMtimes) walkOptions.priorMtimes = options.priorMtimes;
11158
+ if (options.scopedPaths) walkOptions.scopedPaths = options.scopedPaths;
10909
11159
  }
10910
11160
 
10911
11161
  // kernel/extensions/collect-view-contributions.ts
@@ -10995,7 +11245,7 @@ function isExtensionInstance(v) {
10995
11245
  }
10996
11246
 
10997
11247
  // core/runtime/plugin-runtime/warnings.ts
10998
- import { resolve as resolve18 } from "path";
11248
+ import { resolve as resolve19 } from "path";
10999
11249
 
11000
11250
  // kernel/util/text.ts
11001
11251
  function truncateHead(s, max) {
@@ -11045,7 +11295,7 @@ function resolveRuntimeContext(opts) {
11045
11295
  return opts.runtimeContext ?? defaultRuntimeContext();
11046
11296
  }
11047
11297
  function resolveSearchPaths(opts, ctx) {
11048
- if (opts.pluginDir) return [resolve18(opts.pluginDir)];
11298
+ if (opts.pluginDir) return [resolve19(opts.pluginDir)];
11049
11299
  return [defaultProjectPluginsDir(ctx)];
11050
11300
  }
11051
11301
 
@@ -11596,11 +11846,11 @@ function resolveActiveProvider(cwd, providers = []) {
11596
11846
  }
11597
11847
 
11598
11848
  // cli/util/path-display.ts
11599
- import { isAbsolute as isAbsolute4, relative as pathRelative } from "path";
11849
+ import { isAbsolute as isAbsolute5, relative as pathRelative } from "path";
11600
11850
  function relativeIfBelow(path, cwd) {
11601
- if (!isAbsolute4(path)) return path;
11851
+ if (!isAbsolute5(path)) return path;
11602
11852
  const rel = pathRelative(cwd, path);
11603
- if (rel === "" || rel.startsWith("..") || isAbsolute4(rel)) return path;
11853
+ if (rel === "" || rel.startsWith("..") || isAbsolute5(rel)) return path;
11604
11854
  return rel;
11605
11855
  }
11606
11856
 
@@ -12324,7 +12574,7 @@ var CONFIG_COMMANDS = [
12324
12574
 
12325
12575
  // cli/commands/conformance.ts
12326
12576
  import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
12327
- import { dirname as dirname12, resolve as resolve21 } from "path";
12577
+ import { dirname as dirname12, resolve as resolve22 } from "path";
12328
12578
  import { fileURLToPath as fileURLToPath4 } from "url";
12329
12579
  import { Command as Command5, Option as Option5 } from "clipanion";
12330
12580
 
@@ -12332,7 +12582,7 @@ import { Command as Command5, Option as Option5 } from "clipanion";
12332
12582
  import { spawnSync as spawnSync2 } from "child_process";
12333
12583
  import { cpSync, existsSync as existsSync17, mkdtempSync, readdirSync as readdirSync5, readFileSync as readFileSync15, rmSync, statSync as statSync3 } from "fs";
12334
12584
  import { tmpdir } from "os";
12335
- import { isAbsolute as isAbsolute5, join as join11, relative as relative3, resolve as resolve19 } from "path";
12585
+ import { isAbsolute as isAbsolute6, join as join11, relative as relative3, resolve as resolve20 } from "path";
12336
12586
 
12337
12587
  // conformance/i18n/runner.texts.ts
12338
12588
  var CONFORMANCE_RUNNER_TEXTS = {
@@ -12494,14 +12744,14 @@ function replaceFixture(scope, fixturesRoot, fixture) {
12494
12744
  cpSync(src, scope, { recursive: true });
12495
12745
  }
12496
12746
  function assertContained2(root, rel, label) {
12497
- if (isAbsolute5(rel)) {
12747
+ if (isAbsolute6(rel)) {
12498
12748
  throw new Error(
12499
12749
  tx(CONFORMANCE_RUNNER_TEXTS.pathMustBeRelative, { label, path: rel, anchor: root })
12500
12750
  );
12501
12751
  }
12502
- const abs = resolve19(root, rel);
12752
+ const abs = resolve20(root, rel);
12503
12753
  const r = relative3(root, abs);
12504
- if (r.startsWith("..") || isAbsolute5(r)) {
12754
+ if (r.startsWith("..") || isAbsolute6(r)) {
12505
12755
  throw new Error(
12506
12756
  tx(CONFORMANCE_RUNNER_TEXTS.pathEscapesAnchor, { label, path: rel, anchor: root })
12507
12757
  );
@@ -12526,7 +12776,7 @@ function evaluateAssertion(a, ctx) {
12526
12776
  } catch (err) {
12527
12777
  return { ok: false, type: a.type, reason: formatErrorMessage(err) };
12528
12778
  }
12529
- const abs = resolve19(ctx.scope, a.path);
12779
+ const abs = resolve20(ctx.scope, a.path);
12530
12780
  return existsSync17(abs) ? { ok: true, type: a.type } : {
12531
12781
  ok: false,
12532
12782
  type: a.type,
@@ -12541,7 +12791,7 @@ function evaluateAssertion(a, ctx) {
12541
12791
  return { ok: false, type: a.type, reason: formatErrorMessage(err) };
12542
12792
  }
12543
12793
  const fixturePath = join11(ctx.fixturesRoot, a.fixture);
12544
- const targetPath = resolve19(ctx.scope, a.path);
12794
+ const targetPath = resolve20(ctx.scope, a.path);
12545
12795
  if (!existsSync17(targetPath)) {
12546
12796
  return {
12547
12797
  ok: false,
@@ -12724,7 +12974,7 @@ var CONFORMANCE_TEXTS = {
12724
12974
 
12725
12975
  // cli/util/conformance-scopes.ts
12726
12976
  import { existsSync as existsSync18, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
12727
- import { dirname as dirname11, resolve as resolve20 } from "path";
12977
+ import { dirname as dirname11, resolve as resolve21 } from "path";
12728
12978
  import { createRequire as createRequire6 } from "module";
12729
12979
  import { fileURLToPath as fileURLToPath3 } from "url";
12730
12980
  function resolveSpecRoot4() {
@@ -12742,7 +12992,7 @@ function resolveCliWorkspaceRoot() {
12742
12992
  const here = dirname11(fileURLToPath3(import.meta.url));
12743
12993
  let cursor = here;
12744
12994
  for (let depth = 0; depth < 6; depth += 1) {
12745
- const candidate = resolve20(cursor, "plugins");
12995
+ const candidate = resolve21(cursor, "plugins");
12746
12996
  if (existsSync18(candidate) && statSync4(candidate).isDirectory()) {
12747
12997
  return cursor;
12748
12998
  }
@@ -12762,12 +13012,12 @@ function collectProviderScopes(specRoot) {
12762
13012
  } catch {
12763
13013
  return out;
12764
13014
  }
12765
- const pluginsRoot = resolve20(workspaceRoot, "plugins");
13015
+ const pluginsRoot = resolve21(workspaceRoot, "plugins");
12766
13016
  if (!existsSync18(pluginsRoot)) return out;
12767
13017
  for (const pluginEntry of readdirSync6(pluginsRoot)) {
12768
- const pluginDir = resolve20(pluginsRoot, pluginEntry);
13018
+ const pluginDir = resolve21(pluginsRoot, pluginEntry);
12769
13019
  if (!isDir(pluginDir)) continue;
12770
- const providersRoot = resolve20(pluginDir, "providers");
13020
+ const providersRoot = resolve21(pluginDir, "providers");
12771
13021
  if (!isDir(providersRoot)) continue;
12772
13022
  collectPluginProviderScopes(providersRoot, specRoot, out);
12773
13023
  }
@@ -12782,12 +13032,12 @@ function isDir(path) {
12782
13032
  }
12783
13033
  function collectPluginProviderScopes(providersRoot, specRoot, out) {
12784
13034
  for (const entry of readdirSync6(providersRoot)) {
12785
- const providerDir = resolve20(providersRoot, entry);
13035
+ const providerDir = resolve21(providersRoot, entry);
12786
13036
  if (!isDir(providerDir)) continue;
12787
- const conformanceDir = resolve20(providerDir, "conformance");
13037
+ const conformanceDir = resolve21(providerDir, "conformance");
12788
13038
  if (!existsSync18(conformanceDir)) continue;
12789
- const casesDir = resolve20(conformanceDir, "cases");
12790
- const fixturesDir = resolve20(conformanceDir, "fixtures");
13039
+ const casesDir = resolve21(conformanceDir, "cases");
13040
+ const fixturesDir = resolve21(conformanceDir, "fixtures");
12791
13041
  if (!existsSync18(casesDir) || !existsSync18(fixturesDir)) continue;
12792
13042
  out.push({
12793
13043
  id: `provider:${entry}`,
@@ -12804,8 +13054,8 @@ function specScope(specRoot) {
12804
13054
  id: "spec",
12805
13055
  kind: "spec",
12806
13056
  label: "spec",
12807
- casesDir: resolve20(specRoot, "conformance", "cases"),
12808
- fixturesDir: resolve20(specRoot, "conformance", "fixtures"),
13057
+ casesDir: resolve21(specRoot, "conformance", "cases"),
13058
+ fixturesDir: resolve21(specRoot, "conformance", "fixtures"),
12809
13059
  specRoot
12810
13060
  };
12811
13061
  }
@@ -12827,7 +13077,7 @@ function selectConformanceScopes(scope) {
12827
13077
  }
12828
13078
  function listCaseFiles(scope) {
12829
13079
  if (!existsSync18(scope.casesDir)) return [];
12830
- return readdirSync6(scope.casesDir).filter((entry) => entry.endsWith(".json")).sort().map((entry) => resolve20(scope.casesDir, entry));
13080
+ return readdirSync6(scope.casesDir).filter((entry) => entry.endsWith(".json")).sort().map((entry) => resolve21(scope.casesDir, entry));
12831
13081
  }
12832
13082
 
12833
13083
  // cli/commands/conformance.ts
@@ -12844,13 +13094,13 @@ function resolveBinary() {
12844
13094
  const here = dirname12(fileURLToPath4(import.meta.url));
12845
13095
  let cursor = here;
12846
13096
  for (let depth = 0; depth < 6; depth += 1) {
12847
- const candidate = resolve21(cursor, "bin", "sm.js");
13097
+ const candidate = resolve22(cursor, "bin", "sm.js");
12848
13098
  if (existsSync19(candidate)) return candidate;
12849
13099
  const parent = dirname12(cursor);
12850
13100
  if (parent === cursor) break;
12851
13101
  cursor = parent;
12852
13102
  }
12853
- return resolve21(here, "..", "..", "bin", "sm.js");
13103
+ return resolve22(here, "..", "..", "bin", "sm.js");
12854
13104
  }
12855
13105
  var ConformanceRunCommand = class extends SmCommand {
12856
13106
  static paths = [["conformance", "run"]];
@@ -13103,7 +13353,7 @@ function writeStreamSnippet(stream, header, text) {
13103
13353
  var CONFORMANCE_COMMANDS = [ConformanceRunCommand];
13104
13354
 
13105
13355
  // cli/commands/db/backup.ts
13106
- import { dirname as dirname13, join as join12, resolve as resolve22 } from "path";
13356
+ import { dirname as dirname13, join as join12, resolve as resolve23 } from "path";
13107
13357
  import { Command as Command6, Option as Option6 } from "clipanion";
13108
13358
 
13109
13359
  // cli/i18n/db.texts.ts
@@ -13216,7 +13466,7 @@ var DbBackupCommand = class extends SmCommand {
13216
13466
  const exit = requireDbOrExit(path, this.context.stderr);
13217
13467
  if (exit !== null) return exit;
13218
13468
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
13219
- const outPath = this.out ? resolve22(this.out) : join12(dirname13(path), "backups", `${ts}.db`);
13469
+ const outPath = this.out ? resolve23(this.out) : join12(dirname13(path), "backups", `${ts}.db`);
13220
13470
  await withSqlite({ databasePath: path, autoMigrate: false }, async (storage) => {
13221
13471
  storage.migrations.writeBackup(outPath);
13222
13472
  });
@@ -13233,7 +13483,7 @@ var DbBackupCommand = class extends SmCommand {
13233
13483
 
13234
13484
  // cli/commands/db/restore.ts
13235
13485
  import { chmod, copyFile, mkdir, rm } from "fs/promises";
13236
- import { dirname as dirname14, resolve as resolve23 } from "path";
13486
+ import { dirname as dirname14, resolve as resolve24 } from "path";
13237
13487
  import { Command as Command7, Option as Option7 } from "clipanion";
13238
13488
 
13239
13489
  // cli/util/fs.ts
@@ -13283,7 +13533,7 @@ var DbRestoreCommand = class extends SmCommand {
13283
13533
  });
13284
13534
  async run() {
13285
13535
  const target = resolveDbPath({ db: this.db, ...defaultRuntimeContext() });
13286
- const sourcePath = resolve23(this.source);
13536
+ const sourcePath = resolve24(this.source);
13287
13537
  const stderrAnsi = this.ansiFor("stderr");
13288
13538
  const sourceStat = await statOrNull(sourcePath);
13289
13539
  if (!sourceStat) {
@@ -13524,7 +13774,7 @@ var DbShellCommand = class extends SmCommand {
13524
13774
 
13525
13775
  // cli/commands/db/browser.ts
13526
13776
  import { spawn, spawnSync as spawnSync4 } from "child_process";
13527
- import { resolve as resolve24 } from "path";
13777
+ import { resolve as resolve25 } from "path";
13528
13778
  import { Command as Command10, Option as Option9 } from "clipanion";
13529
13779
  var DbBrowserCommand = class extends SmCommand {
13530
13780
  static paths = [["db", "browser"]];
@@ -13557,7 +13807,7 @@ var DbBrowserCommand = class extends SmCommand {
13557
13807
  });
13558
13808
  positional = Option9.String({ required: false });
13559
13809
  async run() {
13560
- const path = this.positional ? resolve24(this.positional) : resolveDbPath({ db: this.db, ...defaultRuntimeContext() });
13810
+ const path = this.positional ? resolve25(this.positional) : resolveDbPath({ db: this.db, ...defaultRuntimeContext() });
13561
13811
  if (!assertDbExists(path, this.context.stderr)) {
13562
13812
  this.printer.error(DB_TEXTS.browserRunScanFirstHint);
13563
13813
  return ExitCode.NotFound;
@@ -13647,8 +13897,8 @@ function listSchemaObjects(db, tables) {
13647
13897
  return db.prepare(`${baseQuery} ORDER BY rootpage`).all();
13648
13898
  }
13649
13899
  const placeholders = tables.map(() => "?").join(",");
13650
- const sql4 = `${baseQuery} AND (name IN (${placeholders}) OR tbl_name IN (${placeholders})) ORDER BY rootpage`;
13651
- return db.prepare(sql4).all(...tables, ...tables);
13900
+ const sql5 = `${baseQuery} AND (name IN (${placeholders}) OR tbl_name IN (${placeholders})) ORDER BY rootpage`;
13901
+ return db.prepare(sql5).all(...tables, ...tables);
13652
13902
  }
13653
13903
  function writeTableData(db, out, tableName) {
13654
13904
  const quoted = `"${tableName.replace(/"/g, '""')}"`;
@@ -14475,7 +14725,7 @@ var GraphCommand = class extends SmCommand {
14475
14725
  // cli/commands/help.ts
14476
14726
  import { readFileSync as readFileSync17 } from "fs";
14477
14727
  import { createRequire as createRequire7 } from "module";
14478
- import { resolve as resolve25 } from "path";
14728
+ import { resolve as resolve26 } from "path";
14479
14729
  import { Command as Command15, Option as Option14 } from "clipanion";
14480
14730
 
14481
14731
  // cli/i18n/help.texts.ts
@@ -14789,7 +15039,7 @@ function resolveSpecVersion() {
14789
15039
  try {
14790
15040
  const req = createRequire7(import.meta.url);
14791
15041
  const indexPath = req.resolve("@skill-map/spec/index.json");
14792
- const pkgPath = resolve25(indexPath, "..", "package.json");
15042
+ const pkgPath = resolve26(indexPath, "..", "package.json");
14793
15043
  const pkg = JSON.parse(readFileSync17(pkgPath, "utf8"));
14794
15044
  return pkg.version;
14795
15045
  } catch {
@@ -15097,7 +15347,7 @@ function registeredVerbPaths(cli2) {
15097
15347
 
15098
15348
  // cli/commands/hooks.ts
15099
15349
  import { chmod as chmod2, mkdir as mkdir3, readFile as readFile2, stat as stat2, writeFile } from "fs/promises";
15100
- import { dirname as dirname16, resolve as resolve26 } from "path";
15350
+ import { dirname as dirname16, resolve as resolve27 } from "path";
15101
15351
  import { Command as Command16, Option as Option15 } from "clipanion";
15102
15352
 
15103
15353
  // cli/i18n/hooks.texts.ts
@@ -15200,8 +15450,8 @@ var HooksInstallCommand = class extends SmCommand {
15200
15450
  );
15201
15451
  return ExitCode.NotFound;
15202
15452
  }
15203
- const hooksDir = resolve26(repoRoot, ".git", "hooks");
15204
- const hookPath = resolve26(hooksDir, "pre-commit");
15453
+ const hooksDir = resolve27(repoRoot, ".git", "hooks");
15454
+ const hookPath = resolve27(hooksDir, "pre-commit");
15205
15455
  const existing = await pathExists(hookPath) ? await readFile2(hookPath, "utf8") : null;
15206
15456
  const planned2 = computePlannedHookContent(existing);
15207
15457
  if (planned2.kind === "already-installed") {
@@ -15259,7 +15509,7 @@ var HooksInstallCommand = class extends SmCommand {
15259
15509
  async function findGitRepoRoot(cwd) {
15260
15510
  let current = cwd;
15261
15511
  while (true) {
15262
- if (await pathExists(resolve26(current, ".git"))) return current;
15512
+ if (await pathExists(resolve27(current, ".git"))) return current;
15263
15513
  const parent = dirname16(current);
15264
15514
  if (parent === current) return null;
15265
15515
  current = parent;
@@ -15270,8 +15520,8 @@ function computePlannedHookContent(existing) {
15270
15520
  if (existing.includes(SKILL_MAP_MARKER)) {
15271
15521
  return { kind: "already-installed", content: existing };
15272
15522
  }
15273
- const sep8 = existing.endsWith("\n") ? "" : "\n";
15274
- return { kind: "chained", content: existing + sep8 + "\n" + SKILL_MAP_BLOCK };
15523
+ const sep9 = existing.endsWith("\n") ? "" : "\n";
15524
+ return { kind: "chained", content: existing + sep9 + "\n" + SKILL_MAP_BLOCK };
15275
15525
  }
15276
15526
  async function ensureExecutableBit(path) {
15277
15527
  const mode = (await stat2(path)).mode;
@@ -15286,7 +15536,7 @@ import { Command as Command17, Option as Option16 } from "clipanion";
15286
15536
 
15287
15537
  // kernel/orchestrator/index.ts
15288
15538
  import { existsSync as existsSync23, statSync as statSync6 } from "fs";
15289
- import { isAbsolute as isAbsolute7, resolve as resolve28 } from "path";
15539
+ import { isAbsolute as isAbsolute9, resolve as resolve30 } from "path";
15290
15540
  import { Tiktoken as Tiktoken2 } from "js-tiktoken/lite";
15291
15541
 
15292
15542
  // kernel/i18n/orchestrator.texts.ts
@@ -16232,7 +16482,7 @@ function collectBrokenLinks(links, nodes, ctx) {
16232
16482
  return broken;
16233
16483
  }
16234
16484
  function applyResolution(link, indexes, ctx) {
16235
- const resolution = resolve27(link, indexes, ctx);
16485
+ const resolution = resolve28(link, indexes, ctx);
16236
16486
  if (resolution === "none") return;
16237
16487
  link.resolvedTarget = resolution;
16238
16488
  }
@@ -16247,7 +16497,7 @@ function buildIndexes(nodes, ctx) {
16247
16497
  }
16248
16498
  return { byPath: byPath3, byName, nodeByPath };
16249
16499
  }
16250
- function resolve27(link, indexes, ctx) {
16500
+ function resolve28(link, indexes, ctx) {
16251
16501
  if (indexes.byPath.has(link.target)) return link.target;
16252
16502
  return resolveByName(link, indexes, ctx);
16253
16503
  }
@@ -16635,6 +16885,9 @@ function detectRenamesAndOrphans(prior, current, issues, silenced) {
16635
16885
  return ops;
16636
16886
  }
16637
16887
 
16888
+ // kernel/orchestrator/walk.ts
16889
+ import { isAbsolute as isAbsolute8, resolve as resolve29 } from "path";
16890
+
16638
16891
  // kernel/sidecar/drift.ts
16639
16892
  function computeDriftStatus(args2) {
16640
16893
  const bodyDrift = args2.storedBodyHash !== args2.liveBodyHash;
@@ -16689,7 +16942,7 @@ function safeIsFile(path) {
16689
16942
  // kernel/orchestrator/node-build.ts
16690
16943
  import { createHash as createHash2 } from "crypto";
16691
16944
  import { existsSync as existsSync22 } from "fs";
16692
- import { isAbsolute as isAbsolute6, resolve as resolvePath } from "path";
16945
+ import { isAbsolute as isAbsolute7, resolve as resolvePath } from "path";
16693
16946
  import "js-tiktoken/lite";
16694
16947
  import yaml4 from "js-yaml";
16695
16948
 
@@ -16852,7 +17105,7 @@ function resolveSidecarOverlay(relativePath2, nodePathForIssue, roots, liveBodyH
16852
17105
  };
16853
17106
  }
16854
17107
  function resolveAbsoluteMdPath(relativePath2, roots) {
16855
- if (isAbsolute6(relativePath2)) {
17108
+ if (isAbsolute7(relativePath2)) {
16856
17109
  return existsSync22(relativePath2) ? relativePath2 : null;
16857
17110
  }
16858
17111
  for (const root of roots) {
@@ -16925,34 +17178,46 @@ async function walkAndExtract(opts) {
16925
17178
  oversizedSeen.add(info.path);
16926
17179
  oversizedFiles.push(info);
16927
17180
  };
17181
+ const priorMtimes = buildPriorMtimes(opts);
16928
17182
  const walkOptions = {
16929
17183
  ...opts.ignoreFilter ? { ignoreFilter: opts.ignoreFilter } : {},
16930
17184
  onOversizedFile,
16931
- ...opts.maxFileSizeBytes !== void 0 ? { maxFileSizeBytes: opts.maxFileSizeBytes } : {}
17185
+ ...opts.maxFileSizeBytes !== void 0 ? { maxFileSizeBytes: opts.maxFileSizeBytes } : {},
17186
+ ...priorMtimes ? { priorMtimes } : {}
16932
17187
  };
16933
- let filesWalked = 0;
17188
+ let filesWalked;
16934
17189
  let index = 0;
16935
- const effectiveMaxNodes = opts.overrideMaxNodes ?? opts.recommendedNodeLimit;
17190
+ const { effectiveScanCeiling, effectiveMaxRenderNodes } = resolveEffectiveCaps(opts);
16936
17191
  let capReached = false;
16937
- const activeProviders = opts.providers.filter((provider) => {
16938
- if (!provider.gatedByActiveLens) return true;
16939
- if (opts.activeProvider === null) return true;
16940
- return provider.id === opts.activeProvider;
16941
- });
17192
+ const activeProviders = opts.providers.filter(
17193
+ (provider) => providerParticipates(provider, opts.activeProvider)
17194
+ );
16942
17195
  const advance = async (raw, provider) => {
16943
17196
  const advanced = await processRawNode(raw, provider, wctx, accum, claimedPaths, index + 1);
16944
17197
  if (advanced) index += 1;
16945
17198
  };
16946
- outer: for (const provider of activeProviders) {
16947
- for await (const raw of resolveProviderWalk(provider)(opts.roots, walkOptions)) {
16948
- filesWalked += 1;
16949
- if (claimedPaths.has(raw.path)) continue;
16950
- if (accum.nodes.length >= effectiveMaxNodes) {
16951
- capReached = true;
16952
- break outer;
16953
- }
16954
- await advance(raw, provider);
16955
- }
17199
+ if (opts.incrementalChangedPaths !== void 0) {
17200
+ filesWalked = await walkIncremental({
17201
+ changedPaths: opts.incrementalChangedPaths,
17202
+ activeProviders,
17203
+ walkOptions,
17204
+ wctx,
17205
+ accum,
17206
+ claimedPaths,
17207
+ advance
17208
+ });
17209
+ } else {
17210
+ const full = await walkFullTraversal({
17211
+ activeProviders,
17212
+ roots: opts.roots,
17213
+ walkOptions,
17214
+ accum,
17215
+ claimedPaths,
17216
+ effectiveScanCeiling,
17217
+ advance
17218
+ });
17219
+ filesWalked = full.filesWalked;
17220
+ capReached = full.capReached;
16956
17221
  }
16957
17222
  const orphanSidecars = discoverOrphanSidecars(opts.roots);
16958
17223
  return {
@@ -16963,9 +17228,9 @@ async function walkAndExtract(opts) {
16963
17228
  frontmatterIssues: accum.frontmatterIssues,
16964
17229
  filesWalked,
16965
17230
  oversizedFiles,
16966
- recommendedNodeLimit: opts.recommendedNodeLimit,
16967
- overrideMaxNodes: opts.overrideMaxNodes,
16968
- capReached,
17231
+ scanCeiling: effectiveScanCeiling,
17232
+ scanTruncated: capReached,
17233
+ maxRenderNodes: effectiveMaxRenderNodes,
16969
17234
  enrichments: [...accum.enrichmentBuffer.values()],
16970
17235
  extractorRuns: accum.extractorRuns,
16971
17236
  contributions: accum.contributionsBuffer,
@@ -16976,6 +17241,119 @@ async function walkAndExtract(opts) {
16976
17241
  signals: accum.signals
16977
17242
  };
16978
17243
  }
17244
+ async function walkFullTraversal(args2) {
17245
+ let filesWalked = 0;
17246
+ let capReached = false;
17247
+ outer: for (const provider of args2.activeProviders) {
17248
+ for await (const raw of resolveProviderWalk(provider)(args2.roots, args2.walkOptions)) {
17249
+ filesWalked += 1;
17250
+ if (args2.claimedPaths.has(raw.path)) continue;
17251
+ if (args2.accum.nodes.length >= args2.effectiveScanCeiling) {
17252
+ capReached = true;
17253
+ break outer;
17254
+ }
17255
+ await args2.advance(raw, provider);
17256
+ }
17257
+ }
17258
+ return { filesWalked, capReached };
17259
+ }
17260
+ async function walkIncremental(args2) {
17261
+ const changed = expandSidecarPaths(args2.changedPaths.changed, args2.wctx.priorNodesByPath);
17262
+ const removed = expandSidecarPaths(args2.changedPaths.removed, args2.wctx.priorNodesByPath);
17263
+ let filesWalked = 0;
17264
+ const scopedAbs = [...changed].map((rel) => toAbsolute(rel, args2.wctx.opts.roots));
17265
+ if (scopedAbs.length > 0) {
17266
+ const scopedWalkOptions = { ...args2.walkOptions, scopedPaths: scopedAbs };
17267
+ for (const provider of args2.activeProviders) {
17268
+ for await (const raw of resolveProviderWalk(provider)(args2.wctx.opts.roots, scopedWalkOptions)) {
17269
+ filesWalked += 1;
17270
+ if (args2.claimedPaths.has(raw.path)) continue;
17271
+ await args2.advance(raw, provider);
17272
+ }
17273
+ }
17274
+ }
17275
+ filesWalked += await injectUnchangedPriorNodes(args2, changed, removed);
17276
+ return filesWalked;
17277
+ }
17278
+ async function injectUnchangedPriorNodes(args2, changed, removed) {
17279
+ const providerById = new Map(args2.activeProviders.map((p) => [p.id, p]));
17280
+ const universalFallback = args2.activeProviders.find((p) => !p.gatedByActiveLens) ?? args2.activeProviders[0];
17281
+ let injected = 0;
17282
+ for (const priorNode of args2.wctx.opts.prior?.nodes ?? []) {
17283
+ if (!shouldInjectPriorNode(priorNode, changed, removed, args2.claimedPaths)) continue;
17284
+ const provider = providerById.get(priorNode.provider) ?? universalFallback;
17285
+ if (!provider) continue;
17286
+ const raw = buildUnchangedRawNode(priorNode, provider, args2.wctx.opts.roots);
17287
+ await args2.advance(raw, provider);
17288
+ injected += 1;
17289
+ }
17290
+ return injected;
17291
+ }
17292
+ function shouldInjectPriorNode(priorNode, changed, removed, claimedPaths) {
17293
+ const path = priorNode.path;
17294
+ if (changed.has(path) || removed.has(path) || claimedPaths.has(path)) return false;
17295
+ if (priorNode.virtual === true) return false;
17296
+ return true;
17297
+ }
17298
+ function buildUnchangedRawNode(priorNode, provider, roots) {
17299
+ const path = priorNode.path;
17300
+ return {
17301
+ path,
17302
+ body: "",
17303
+ frontmatterRaw: "",
17304
+ frontmatter: {},
17305
+ ...typeof priorNode.modifiedAtMs === "number" ? { modifiedAtMs: priorNode.modifiedAtMs } : {},
17306
+ unchanged: true,
17307
+ reread: async () => {
17308
+ const abs = toAbsolute(path, roots);
17309
+ for await (const re of resolveProviderWalk(provider)(roots, { scopedPaths: [abs] })) {
17310
+ return {
17311
+ body: re.body,
17312
+ frontmatterRaw: re.frontmatterRaw,
17313
+ frontmatter: re.frontmatter,
17314
+ ...re.parseIssues ? { parseIssues: re.parseIssues } : {}
17315
+ };
17316
+ }
17317
+ return { body: "", frontmatterRaw: "", frontmatter: {} };
17318
+ }
17319
+ };
17320
+ }
17321
+ function expandSidecarPaths(paths, priorNodesByPath) {
17322
+ const out = /* @__PURE__ */ new Set();
17323
+ for (const path of paths) {
17324
+ if (path.endsWith(".sm")) {
17325
+ const mdPath = `${path.slice(0, -".sm".length)}.md`;
17326
+ if (priorNodesByPath.has(mdPath)) out.add(mdPath);
17327
+ continue;
17328
+ }
17329
+ out.add(path);
17330
+ }
17331
+ return out;
17332
+ }
17333
+ function toAbsolute(relPath, roots) {
17334
+ const root = roots[0] ?? ".";
17335
+ const absRoot = isAbsolute8(root) ? root : resolve29(root);
17336
+ return resolve29(absRoot, relPath);
17337
+ }
17338
+ function resolveEffectiveCaps(opts) {
17339
+ return {
17340
+ effectiveScanCeiling: opts.overrideScanCeiling ?? opts.scanCeiling,
17341
+ effectiveMaxRenderNodes: opts.overrideMaxRenderNodes ?? opts.maxRenderNodes
17342
+ };
17343
+ }
17344
+ function buildPriorMtimes(opts) {
17345
+ if (!opts.enableCache || opts.prior === null || opts.tokenizerChanged) return void 0;
17346
+ const map = /* @__PURE__ */ new Map();
17347
+ for (const node of opts.prior.nodes) {
17348
+ if (typeof node.modifiedAtMs === "number") map.set(node.path, node.modifiedAtMs);
17349
+ }
17350
+ return map.size > 0 ? map : void 0;
17351
+ }
17352
+ function providerParticipates(provider, activeProvider) {
17353
+ if (!provider.gatedByActiveLens) return true;
17354
+ if (activeProvider === null) return true;
17355
+ return provider.id === activeProvider;
17356
+ }
16979
17357
  function createWalkAccumulators() {
16980
17358
  return {
16981
17359
  nodes: [],
@@ -17004,61 +17382,106 @@ function buildWalkContext(opts) {
17004
17382
  return { opts, priorNodesByPath, priorLinksByOriginating, priorFrontmatterIssuesByNode, shortIdToQualified };
17005
17383
  }
17006
17384
  async function processRawNode(raw, provider, wctx, accum, claimedPaths, nextIndex) {
17007
- const bodyHash = sha256(raw.body);
17008
- const frontmatterHash = sha256(canonicalFrontmatter(raw.frontmatter, raw.frontmatterRaw));
17009
17385
  if (Array.isArray(provider.roots) && provider.roots.length > 0) {
17010
17386
  if (!matchesAnyRoot(raw.path, provider.roots)) return false;
17011
17387
  }
17388
+ if (raw.unchanged === true) {
17389
+ const handled = await handleUnchangedRawNode(raw, provider, wctx, accum, claimedPaths, nextIndex);
17390
+ if (handled) return true;
17391
+ }
17392
+ const bodyHash = sha256(raw.body);
17393
+ const frontmatterHash = sha256(canonicalFrontmatter(raw.frontmatter, raw.frontmatterRaw));
17012
17394
  const kind = provider.classify(raw.path, raw.frontmatter);
17013
17395
  if (kind === null) {
17014
17396
  return false;
17015
17397
  }
17016
17398
  claimedPaths.add(raw.path);
17017
17399
  const priorNode = wctx.priorNodesByPath.get(raw.path);
17018
- const nodeHashCacheEligible = wctx.opts.enableCache && // Tokenizer-change invalidation: when the resolved encoder differs
17019
- // from the one that produced the prior snapshot's counts, no node is
17020
- // cache-eligible, every node rebuilds so `buildNode` re-tokenizes
17021
- // with the current encoder. See `tokenizerChanged` on the options.
17022
- !wctx.opts.tokenizerChanged && wctx.opts.prior !== null && priorNode !== void 0 && priorNode.bodyHash === bodyHash && priorNode.frontmatterHash === frontmatterHash;
17400
+ const nodeHashCacheEligible = isNodeHashCacheEligible(wctx, priorNode, bodyHash, frontmatterHash);
17401
+ await dispatchNode(
17402
+ { raw, provider, kind, bodyHash, frontmatterHash, nodeHashCacheEligible, priorNode },
17403
+ wctx,
17404
+ accum,
17405
+ nextIndex
17406
+ );
17407
+ return true;
17408
+ }
17409
+ function isNodeHashCacheEligible(wctx, priorNode, bodyHash, frontmatterHash) {
17410
+ return wctx.opts.enableCache && !wctx.opts.tokenizerChanged && wctx.opts.prior !== null && priorNode !== void 0 && priorNode.bodyHash === bodyHash && priorNode.frontmatterHash === frontmatterHash;
17411
+ }
17412
+ async function handleUnchangedRawNode(raw, provider, wctx, accum, claimedPaths, nextIndex) {
17413
+ const prior = wctx.priorNodesByPath.get(raw.path);
17414
+ if (prior) {
17415
+ claimedPaths.add(raw.path);
17416
+ await dispatchNode(
17417
+ {
17418
+ raw,
17419
+ provider,
17420
+ kind: prior.kind,
17421
+ bodyHash: prior.bodyHash,
17422
+ frontmatterHash: prior.frontmatterHash,
17423
+ nodeHashCacheEligible: true,
17424
+ priorNode: prior,
17425
+ ensureBody: () => rereadInto(raw)
17426
+ },
17427
+ wctx,
17428
+ accum,
17429
+ nextIndex
17430
+ );
17431
+ return true;
17432
+ }
17433
+ await rereadInto(raw);
17434
+ return false;
17435
+ }
17436
+ async function dispatchNode(args2, wctx, accum, nextIndex) {
17023
17437
  const sidecarResolution = resolveSidecarOverlay(
17024
- raw.path,
17025
- raw.path,
17438
+ args2.raw.path,
17439
+ args2.raw.path,
17026
17440
  wctx.opts.roots,
17027
- bodyHash,
17028
- frontmatterHash
17441
+ args2.bodyHash,
17442
+ args2.frontmatterHash
17029
17443
  );
17030
17444
  const sidecarAnnotationsHash = sha256(
17031
17445
  canonicalSidecarAnnotations(sidecarResolution.overlay.annotations)
17032
17446
  );
17033
17447
  const cacheDecision = computeCacheDecision({
17034
17448
  extractors: wctx.opts.extractors,
17035
- kind,
17449
+ kind: args2.kind,
17036
17450
  activeProvider: wctx.opts.activeProvider,
17037
- nodePath: raw.path,
17038
- bodyHash,
17451
+ nodePath: args2.raw.path,
17452
+ bodyHash: args2.bodyHash,
17039
17453
  sidecarAnnotationsHash,
17040
- nodeHashCacheEligible,
17454
+ nodeHashCacheEligible: args2.nodeHashCacheEligible,
17041
17455
  priorExtractorRuns: wctx.opts.priorExtractorRuns
17042
17456
  });
17043
17457
  const ctx = {
17044
- raw,
17045
- provider,
17046
- kind,
17047
- bodyHash,
17048
- frontmatterHash,
17458
+ raw: args2.raw,
17459
+ provider: args2.provider,
17460
+ kind: args2.kind,
17461
+ bodyHash: args2.bodyHash,
17462
+ frontmatterHash: args2.frontmatterHash,
17049
17463
  sidecarResolution,
17050
17464
  sidecarAnnotationsHash,
17051
- nodeHashCacheEligible,
17465
+ nodeHashCacheEligible: args2.nodeHashCacheEligible,
17052
17466
  cacheDecision,
17053
- priorNode,
17467
+ priorNode: args2.priorNode,
17054
17468
  index: nextIndex
17055
17469
  };
17056
- if (cacheDecision.fullCacheHit && priorNode) {
17470
+ if (cacheDecision.fullCacheHit && args2.priorNode) {
17057
17471
  applyFullCacheHit(ctx, wctx, accum);
17058
17472
  } else {
17473
+ if (args2.ensureBody) await args2.ensureBody();
17059
17474
  await applyExtractPath(ctx, wctx, accum);
17060
17475
  }
17061
- return true;
17476
+ }
17477
+ async function rereadInto(raw) {
17478
+ if (!raw.reread) return;
17479
+ const re = await raw.reread();
17480
+ raw.body = re.body;
17481
+ raw.frontmatter = re.frontmatter;
17482
+ raw.frontmatterRaw = re.frontmatterRaw;
17483
+ raw.unchanged = false;
17484
+ if (re.parseIssues) raw.parseIssues = re.parseIssues;
17062
17485
  }
17063
17486
  function attachSidecar(node, resolution, sidecarRoots) {
17064
17487
  node.sidecar = resolution.overlay;
@@ -17271,9 +17694,18 @@ async function runScanInternal(_kernel, options) {
17271
17694
  providerFrontmatter: setup.providerFrontmatter,
17272
17695
  pluginStores: options.pluginStores,
17273
17696
  activeProvider: activeProviderId,
17274
- recommendedNodeLimit: options.recommendedNodeLimit ?? 256,
17275
- overrideMaxNodes: options.overrideMaxNodes ?? null,
17276
- ...options.maxFileSizeBytes !== void 0 ? { maxFileSizeBytes: options.maxFileSizeBytes } : {}
17697
+ scanCeiling: options.scanCeiling ?? 5e4,
17698
+ overrideScanCeiling: options.overrideScanCeiling ?? null,
17699
+ maxRenderNodes: options.maxRenderNodes ?? 256,
17700
+ overrideMaxRenderNodes: options.overrideMaxRenderNodes ?? null,
17701
+ ...options.maxFileSizeBytes !== void 0 ? { maxFileSizeBytes: options.maxFileSizeBytes } : {},
17702
+ // Watcher incremental fast path: only honoured when a prior exists,
17703
+ // cache reuse is on, and the tokenizer is unchanged (else a scoped
17704
+ // read would skip nodes whose token counts must be recomputed). The
17705
+ // walker enumerates from the prior snapshot + reads only the changed
17706
+ // files instead of traversing the corpus. Falls back to the full
17707
+ // traversal + mtime-gate when the gate does not hold.
17708
+ ...options.incrementalChangedPaths !== void 0 && prior !== null && setup.enableCache && !tokenizerChanged ? { incrementalChangedPaths: options.incrementalChangedPaths } : {}
17277
17709
  });
17278
17710
  const activeProvider = activeProviderId ? exts.providers.find((p) => p.id === activeProviderId) ?? null : null;
17279
17711
  const resolved = resolveSignals({
@@ -17471,8 +17903,9 @@ function buildScanReturn(walked, issues, renameOps, stats, options, setup, linkS
17471
17903
  providers: setup.exts.providers.map((a) => a.id),
17472
17904
  scannedBy: SCANNED_BY,
17473
17905
  tokenizer: setup.tokenizer,
17474
- recommendedNodeLimit: walked.recommendedNodeLimit,
17475
- overrideMaxNodes: walked.overrideMaxNodes,
17906
+ scanCeiling: walked.scanCeiling,
17907
+ scanTruncated: walked.scanTruncated,
17908
+ maxRenderNodes: walked.maxRenderNodes,
17476
17909
  oversizedFiles: walked.oversizedFiles,
17477
17910
  nodes: walked.nodes,
17478
17911
  links: walked.internalLinks,
@@ -17501,7 +17934,7 @@ function validateRoots(roots) {
17501
17934
  function resolveActiveProviderOption(optionValue, roots, providers) {
17502
17935
  if (optionValue !== void 0) return optionValue;
17503
17936
  for (const root of roots) {
17504
- const absRoot = isAbsolute7(root) ? root : resolve28(root);
17937
+ const absRoot = isAbsolute9(root) ? root : resolve30(root);
17505
17938
  if (!existsSync23(absRoot)) continue;
17506
17939
  const detected = detectProvidersFromFilesystem(absRoot, providers)[0] ?? null;
17507
17940
  if (detected !== null) return detected;
@@ -17510,10 +17943,10 @@ function resolveActiveProviderOption(optionValue, roots, providers) {
17510
17943
  }
17511
17944
 
17512
17945
  // kernel/scan/watcher.ts
17513
- import { resolve as resolve29, relative as relative5, sep as sep5 } from "path";
17946
+ import { resolve as resolve31, relative as relative5, sep as sep5 } from "path";
17514
17947
  import chokidar from "chokidar";
17515
17948
  function createChokidarWatcher(opts) {
17516
- const absRoots = opts.roots.map((r) => resolve29(opts.cwd, r));
17949
+ const absRoots = opts.roots.map((r) => resolve31(opts.cwd, r));
17517
17950
  const ignoreFilterOpt = opts.ignoreFilter;
17518
17951
  const getFilter = ignoreFilterOpt === void 0 ? void 0 : typeof ignoreFilterOpt === "function" ? ignoreFilterOpt : () => ignoreFilterOpt;
17519
17952
  const ignored = getFilter ? (path) => {
@@ -18036,7 +18469,7 @@ function resolveScanRoots(inputs) {
18036
18469
  // core/runtime/reference-paths-walker.ts
18037
18470
  import { readdirSync as readdirSync8, statSync as statSync7 } from "fs";
18038
18471
  import { homedir as osHomedir2 } from "os";
18039
- import { isAbsolute as isAbsolute8, join as join14, resolve as resolve30 } from "path";
18472
+ import { isAbsolute as isAbsolute10, join as join14, resolve as resolve32 } from "path";
18040
18473
  var REFERENCE_WALK_MAX_FILES = 5e4;
18041
18474
  var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
18042
18475
  "node_modules",
@@ -18044,10 +18477,10 @@ var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
18044
18477
  SKILL_MAP_DIR
18045
18478
  ]);
18046
18479
  function resolveScanPath(raw, cwd) {
18047
- if (raw.startsWith("~/")) return resolve30(join14(osHomedir2(), raw.slice(2)));
18048
- if (raw === "~") return resolve30(osHomedir2());
18049
- if (isAbsolute8(raw)) return resolve30(raw);
18050
- return resolve30(cwd, raw);
18480
+ if (raw.startsWith("~/")) return resolve32(join14(osHomedir2(), raw.slice(2)));
18481
+ if (raw === "~") return resolve32(osHomedir2());
18482
+ if (isAbsolute10(raw)) return resolve32(raw);
18483
+ return resolve32(cwd, raw);
18051
18484
  }
18052
18485
  function walkReferencePaths(rawRoots, cwd) {
18053
18486
  const paths = /* @__PURE__ */ new Set();
@@ -18096,7 +18529,7 @@ function safeStat(path) {
18096
18529
 
18097
18530
  // core/runtime/active-provider-bootstrap.ts
18098
18531
  import { createInterface as createInterface3 } from "readline";
18099
- import { isAbsolute as isAbsolute9, join as join15 } from "path";
18532
+ import { isAbsolute as isAbsolute11, join as join15 } from "path";
18100
18533
  async function bootstrapActiveProvider(opts) {
18101
18534
  const fromCwd = resolveActiveProvider(opts.cwd, opts.providers);
18102
18535
  if (fromCwd.source === "config") {
@@ -18155,7 +18588,7 @@ function aggregateDetected(cwd, effectiveRoots, cwdDetected, providers) {
18155
18588
  out.push(id);
18156
18589
  }
18157
18590
  for (const root of effectiveRoots) {
18158
- const absRoot = isAbsolute9(root) ? root : join15(cwd, root);
18591
+ const absRoot = isAbsolute11(root) ? root : join15(cwd, root);
18159
18592
  const r = resolveActiveProvider(absRoot, providers);
18160
18593
  for (const id of r.detected) {
18161
18594
  if (seen.has(id)) continue;
@@ -18413,6 +18846,7 @@ async function runScanForCommand(opts) {
18413
18846
  referenceablePaths,
18414
18847
  ctx.cwd,
18415
18848
  activeProvider,
18849
+ cfg.scan.maxScan,
18416
18850
  cfg.scan.maxNodes,
18417
18851
  cfg.scan.maxFileSizeBytes,
18418
18852
  cfg.tokenizer
@@ -18531,7 +18965,7 @@ function makePriorLoader(noBuiltIns, strict) {
18531
18965
  return loaded;
18532
18966
  };
18533
18967
  }
18534
- function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, extensions, referenceablePaths, scanCwd, activeProvider, recommendedNodeLimit, maxFileSizeBytes, tokenizer) {
18968
+ function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, extensions, referenceablePaths, scanCwd, activeProvider, scanCeiling, maxRenderNodes, maxFileSizeBytes, tokenizer) {
18535
18969
  return async (prior, priorExtractorRuns) => {
18536
18970
  if (opts.changed && prior === null) {
18537
18971
  opts.stderr.write(SCAN_RUNNER_TEXTS.changedNoPriorWarning);
@@ -18546,7 +18980,8 @@ function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, exte
18546
18980
  cwd: scanCwd,
18547
18981
  prior,
18548
18982
  activeProvider,
18549
- recommendedNodeLimit,
18983
+ scanCeiling,
18984
+ maxRenderNodes,
18550
18985
  maxFileSizeBytes,
18551
18986
  tokenizer,
18552
18987
  ...priorExtractorRuns ? { priorExtractorRuns } : {}
@@ -18564,8 +18999,10 @@ function buildRunScanOptions(args2) {
18564
18999
  strict: args2.strict,
18565
19000
  emitter: buildRunScanEmitter(opts),
18566
19001
  activeProvider: args2.activeProvider,
18567
- recommendedNodeLimit: args2.recommendedNodeLimit,
18568
- overrideMaxNodes: opts.maxNodes ?? null,
19002
+ scanCeiling: args2.scanCeiling,
19003
+ overrideScanCeiling: opts.maxScan ?? null,
19004
+ maxRenderNodes: args2.maxRenderNodes,
19005
+ overrideMaxRenderNodes: opts.maxNodes ?? null,
18569
19006
  maxFileSizeBytes: args2.maxFileSizeBytes
18570
19007
  };
18571
19008
  if (args2.extensions) runOptions.extensions = args2.extensions;
@@ -19510,7 +19947,7 @@ import { Command as Command19, Option as Option18 } from "clipanion";
19510
19947
 
19511
19948
  // kernel/jobs/orphan-files.ts
19512
19949
  import { readdirSync as readdirSync9, statSync as statSync8 } from "fs";
19513
- import { join as join17, resolve as resolve31 } from "path";
19950
+ import { join as join17, resolve as resolve33 } from "path";
19514
19951
  function findOrphanJobFiles(jobsDir, referencedPaths) {
19515
19952
  let entries;
19516
19953
  try {
@@ -19528,7 +19965,7 @@ function findOrphanJobFiles(jobsDir, referencedPaths) {
19528
19965
  if (!entry.isFile()) continue;
19529
19966
  const name = entry.name;
19530
19967
  if (!name.endsWith(".md")) continue;
19531
- const abs = resolve31(join17(jobsDir, name));
19968
+ const abs = resolve33(join17(jobsDir, name));
19532
19969
  if (!referencedPaths.has(abs)) orphans.push(abs);
19533
19970
  }
19534
19971
  orphans.sort();
@@ -20755,9 +21192,9 @@ function sortPluginsForPresentation(plugins) {
20755
21192
  }
20756
21193
 
20757
21194
  // cli/commands/plugins/shared.ts
20758
- import { resolve as resolve32 } from "path";
21195
+ import { resolve as resolve34 } from "path";
20759
21196
  function resolveSearchPaths2(opts, cwd) {
20760
- if (opts.pluginDir) return [resolve32(opts.pluginDir)];
21197
+ if (opts.pluginDir) return [resolve34(opts.pluginDir)];
20761
21198
  return [defaultProjectPluginsDir({ cwd })];
20762
21199
  }
20763
21200
  async function buildResolver() {
@@ -22071,7 +22508,7 @@ function resolveBareToggle(id, catalogue) {
22071
22508
 
22072
22509
  // cli/commands/plugins/create.ts
22073
22510
  import { existsSync as existsSync25, mkdirSync as mkdirSync5, writeFileSync } from "fs";
22074
- import { dirname as dirname17, join as join18, resolve as resolve33 } from "path";
22511
+ import { dirname as dirname17, join as join18, resolve as resolve35 } from "path";
22075
22512
  import { Command as Command26, Option as Option25 } from "clipanion";
22076
22513
 
22077
22514
  // cli/commands/plugins/scaffold/action.ts
@@ -22521,7 +22958,7 @@ var PluginsCreateCommand = class extends SmCommand {
22521
22958
  const kind = this.kind;
22522
22959
  const ctx = defaultRuntimeContext();
22523
22960
  const baseDir = defaultProjectPluginsDir(ctx);
22524
- const targetDir = this.at ? resolve33(this.at) : join18(baseDir, this.pluginId);
22961
+ const targetDir = this.at ? resolve35(this.at) : join18(baseDir, this.pluginId);
22525
22962
  if (existsSync25(targetDir) && !this.force) {
22526
22963
  this.printer.error(
22527
22964
  tx(PLUGINS_TEXTS.createRefuseOverwrite, {
@@ -23037,7 +23474,7 @@ var PLUGIN_COMMANDS = [
23037
23474
 
23038
23475
  // cli/commands/refresh.ts
23039
23476
  import { readFile as readFile4 } from "fs/promises";
23040
- import { resolve as resolve34 } from "path";
23477
+ import { resolve as resolve36 } from "path";
23041
23478
  import { Command as Command30, Option as Option28 } from "clipanion";
23042
23479
 
23043
23480
  // cli/i18n/refresh.texts.ts
@@ -23345,7 +23782,7 @@ var RefreshCommand = class extends SmCommand {
23345
23782
  let body;
23346
23783
  try {
23347
23784
  assertContained(cwd, node.path);
23348
- const raw = await readFile4(resolve34(cwd, node.path), "utf8");
23785
+ const raw = await readFile4(resolve36(cwd, node.path), "utf8");
23349
23786
  body = stripFrontmatterFence(raw);
23350
23787
  } catch (err) {
23351
23788
  if (!this.json) {
@@ -23418,7 +23855,7 @@ var IntentionalFailCommand = class extends SmCommand {
23418
23855
  setTimeout(() => {
23419
23856
  throw new Error(INTENTIONAL_FAIL_TEXTS.errorMessage);
23420
23857
  }, 0);
23421
- await new Promise((resolve40) => setTimeout(resolve40, 5e3));
23858
+ await new Promise((resolve43) => setTimeout(resolve43, 5e3));
23422
23859
  return ExitCode.Issues;
23423
23860
  }
23424
23861
  };
@@ -23508,15 +23945,16 @@ var SCAN_TEXTS = {
23508
23945
  countInfoNoun: "info",
23509
23946
  countNoIssues: "0 issues",
23510
23947
  /**
23511
- * Cap-hit notice, printed when the walker stopped accepting nodes
23512
- * because `--max-nodes` (or the `scan.maxNodes` setting) was reached.
23513
- * `{{glyph}}` is the yellow warning glyph, `{{limit}}` the effective
23514
- * cap, `{{source}}` either `--max-nodes` or `scan.maxNodes`. The hint
23515
- * names both escape routes the user has: trimming `.skillmapignore`
23516
- * (preferred) or raising the cap with `--max-nodes <N>`.
23948
+ * Truncation notice, printed when the walker stopped accepting files
23949
+ * because the walk ceiling `--max-scan` (or the `scan.maxScan`
23950
+ * setting) was reached and extra files were dropped. `{{glyph}}` is
23951
+ * the yellow warning glyph, `{{limit}}` the effective ceiling,
23952
+ * `{{source}}` either `--max-scan` or `scan.maxScan`. The hint names
23953
+ * both escape routes the user has: editing `.skillmapignore`
23954
+ * (preferred) or raising the ceiling with `--max-scan <N>`.
23517
23955
  */
23518
- scanCappedNotice: "{{glyph}} Scan capped at {{limit}} nodes ({{source}}).\n {{hint}}\n",
23519
- scanCappedNoticeHint: "Trim .skillmapignore to exclude noisy paths (preferred), or re-run with --max-nodes <N> to raise the cap. Past the recommended limit the graph is hard to read and analyzer signal drops.",
23956
+ scanCappedNotice: "{{glyph}} Scan truncated at {{limit}} files ({{source}}); extra files were dropped.\n {{hint}}\n",
23957
+ scanCappedNoticeHint: "Edit .skillmapignore to exclude noisy paths (preferred), or re-run with --max-scan <N> to raise the ceiling. Files past the ceiling are not parsed, analyzed, or reference-validated.",
23520
23958
  /**
23521
23959
  * File-size skip notice, printed (WARN, stderr) when the walker
23522
23960
  * skipped one or more files for exceeding `scan.maxFileSizeBytes`.
@@ -23531,6 +23969,12 @@ var SCAN_TEXTS = {
23531
23969
  scanSkippedFileNounSingular: "file",
23532
23970
  scanSkippedFileNounPlural: "files",
23533
23971
  scanSkippedFilesNoticeHint: "Raise scan.maxFileSizeBytes to include these, or add them to .skillmapignore to skip them on purpose.",
23972
+ /**
23973
+ * Validation message for an invalid `--max-scan` value. Surfaced as a
23974
+ * §3.1b two-line block.
23975
+ */
23976
+ maxScanInvalid: "{{glyph}} --max-scan must be an integer >= 1 (got `{{value}}`).\n {{hint}}\n",
23977
+ maxScanInvalidHint: "Pass a positive integer, e.g. --max-scan 50000.",
23534
23978
  /**
23535
23979
  * Validation message for an invalid `--max-nodes` value. Surfaced as a
23536
23980
  * §3.1b two-line block.
@@ -23579,7 +24023,7 @@ var SCAN_TEXTS = {
23579
24023
  import { Command as Command31, Option as Option29 } from "clipanion";
23580
24024
 
23581
24025
  // core/watcher/runtime.ts
23582
- import { dirname as dirname18 } from "path";
24026
+ import { dirname as dirname18, isAbsolute as isAbsolute12, relative as relative7, resolve as resolve37, sep as sep6 } from "path";
23583
24027
 
23584
24028
  // core/runtime/fresh-resolver.ts
23585
24029
  async function buildFreshResolver(deps) {
@@ -23612,6 +24056,39 @@ async function rebuildWatcherDbOnDrift(dbPath, events) {
23612
24056
  events.onDriftReset?.({ dbVersion: drift.dbVersion, currentVersion: drift.currentVersion });
23613
24057
  }
23614
24058
  }
24059
+ function applyPriorStateToRunOptions(runOptions, priorState, changedPaths) {
24060
+ if (!priorState) return;
24061
+ runOptions.priorSnapshot = priorState.snapshot;
24062
+ runOptions.enableCache = true;
24063
+ runOptions.priorExtractorRuns = priorState.extractorRuns;
24064
+ if (changedPaths) {
24065
+ runOptions.incrementalChangedPaths = {
24066
+ changed: changedPaths.changed,
24067
+ removed: changedPaths.removed
24068
+ };
24069
+ }
24070
+ }
24071
+ function toIncrementalPaths(events, roots, cwd) {
24072
+ const absRoots = roots.map((r) => isAbsolute12(r) ? r : resolve37(cwd, r));
24073
+ const changed = /* @__PURE__ */ new Set();
24074
+ const removed = /* @__PURE__ */ new Set();
24075
+ for (const ev of events) {
24076
+ const rel = relativeFromRoots2(ev.absolutePath, absRoots);
24077
+ if (rel === null) continue;
24078
+ if (ev.kind === "unlink") removed.add(rel);
24079
+ else changed.add(rel);
24080
+ }
24081
+ if (changed.size === 0 && removed.size === 0) return null;
24082
+ return { changed, removed };
24083
+ }
24084
+ function relativeFromRoots2(absolute, absRoots) {
24085
+ for (const root of absRoots) {
24086
+ const rel = relative7(root, absolute);
24087
+ if (rel === "" || rel.startsWith("..") || isAbsolute12(rel)) continue;
24088
+ return rel.split(sep6).join("/");
24089
+ }
24090
+ return null;
24091
+ }
23615
24092
  function createWatcherRuntime(opts) {
23616
24093
  const events = opts.events ?? {};
23617
24094
  const cwd = opts.runtimeContext.cwd;
@@ -23665,7 +24142,11 @@ function createWatcherRuntime(opts) {
23665
24142
  for (const warn of pluginRuntime.warnings) {
23666
24143
  events.onPluginWarning?.(warn);
23667
24144
  }
23668
- const runOnePass = async () => {
24145
+ const notifyBatchStart = () => {
24146
+ events.onBatchStart?.();
24147
+ };
24148
+ const runOnePass = async (changedPaths) => {
24149
+ notifyBatchStart();
23669
24150
  const resolveEnabledOverride = await buildFreshResolver({
23670
24151
  databasePath: opts.dbPath,
23671
24152
  effectiveConfig: () => cfg,
@@ -23711,8 +24192,10 @@ function createWatcherRuntime(opts) {
23711
24192
  ignoreFilter,
23712
24193
  strict,
23713
24194
  emitter,
23714
- recommendedNodeLimit: cfg.scan.maxNodes,
23715
- overrideMaxNodes: opts.maxNodesOverride ?? null,
24195
+ scanCeiling: cfg.scan.maxScan,
24196
+ overrideScanCeiling: opts.maxScanOverride ?? null,
24197
+ maxRenderNodes: cfg.scan.maxNodes,
24198
+ overrideMaxRenderNodes: opts.maxNodesOverride ?? null,
23716
24199
  maxFileSizeBytes: cfg.scan.maxFileSizeBytes
23717
24200
  };
23718
24201
  if (cfg.scan.referencePaths.length > 0) {
@@ -23723,11 +24206,7 @@ function createWatcherRuntime(opts) {
23723
24206
  }
23724
24207
  }
23725
24208
  if (composed) runOptions.extensions = composed;
23726
- if (priorState) {
23727
- runOptions.priorSnapshot = priorState.snapshot;
23728
- runOptions.enableCache = true;
23729
- runOptions.priorExtractorRuns = priorState.extractorRuns;
23730
- }
24209
+ applyPriorStateToRunOptions(runOptions, priorState, changedPaths);
23731
24210
  const ran = await runScanWithRenames(kernel, runOptions);
23732
24211
  const {
23733
24212
  result,
@@ -23754,11 +24233,11 @@ function createWatcherRuntime(opts) {
23754
24233
  );
23755
24234
  return result;
23756
24235
  };
23757
- handleBatch = async () => {
24236
+ handleBatch = async (changedPaths) => {
23758
24237
  if (stopped) return;
23759
24238
  batchCount++;
23760
24239
  try {
23761
- const result = await runOnePass();
24240
+ const result = await runOnePass(changedPaths);
23762
24241
  consecutiveFailures = 0;
23763
24242
  events.onBatch?.({ kind: "ok", result });
23764
24243
  } catch (err) {
@@ -23805,8 +24284,11 @@ function createWatcherRuntime(opts) {
23805
24284
  // `.skill-map/settings.json` edit, and chokidar's `ignored`
23806
24285
  // predicate must read the current value on every event.
23807
24286
  ignoreFilter: () => ignoreFilter,
23808
- onBatch: async () => {
23809
- if (handleBatch) await handleBatch();
24287
+ onBatch: async ({ events: batchEvents }) => {
24288
+ if (!handleBatch) return;
24289
+ const changedPaths = toIncrementalPaths(batchEvents, opts.roots, cwd);
24290
+ if (changedPaths) await handleBatch(changedPaths);
24291
+ else await handleBatch();
23810
24292
  },
23811
24293
  onError: (err) => {
23812
24294
  events.onWatcherError?.(err.message);
@@ -23938,6 +24420,11 @@ var WATCH_TEXTS = {
23938
24420
  */
23939
24421
  maxConsecutiveFailuresInvalid: "{{glyph}} sm watch: --max-consecutive-failures must be a non-negative integer (got {{raw}}).\n {{hint}}\n",
23940
24422
  maxConsecutiveFailuresInvalidHint: "Pass an integer >= 0 (0 disables the circuit-breaker; the default is 5).",
24423
+ /**
24424
+ * §3.1b two-line block. Validation rejection for `--max-scan`.
24425
+ */
24426
+ maxScanInvalid: "{{glyph}} sm watch: --max-scan must be an integer >= 1 (got {{raw}}).\n {{hint}}\n",
24427
+ maxScanInvalidHint: "Pass a positive integer, e.g. --max-scan 50000.",
23941
24428
  /**
23942
24429
  * §3.1b two-line block. Validation rejection for `--max-nodes`.
23943
24430
  */
@@ -24033,6 +24520,7 @@ async function runWatchLoop(opts) {
24033
24520
  circuitBreaker: { maxConsecutiveFailures: breakerLimit },
24034
24521
  killSwitches: readConformanceKillSwitches(),
24035
24522
  ...opts.maxBatches !== void 0 ? { maxBatches: opts.maxBatches } : {},
24523
+ ...opts.maxScan !== void 0 ? { maxScanOverride: opts.maxScan } : {},
24036
24524
  ...opts.maxNodes !== void 0 ? { maxNodesOverride: opts.maxNodes } : {},
24037
24525
  events: {
24038
24526
  onBatch: (outcome) => {
@@ -24157,9 +24645,13 @@ var WatchCommand = class extends SmCommand {
24157
24645
  required: false,
24158
24646
  description: "Shut down with exit 2 after N consecutive batch failures (default 5; 0 disables the breaker)."
24159
24647
  });
24648
+ maxScan = Option29.String("--max-scan", {
24649
+ required: false,
24650
+ description: "Per-batch override of scan.maxScan (default 50000), the WALK-INTAKE ceiling. The scan walks, parses, analyzes, and reference-validates the full corpus up to this number. Bidirectional: raises OR lowers the ceiling. When a batch hits it, additional files are dropped in stable order and the UI surfaces the persistent truncation banner. Validation: integer >= 1."
24651
+ });
24160
24652
  maxNodes = Option29.String("--max-nodes", {
24161
24653
  required: false,
24162
- description: "Per-batch override of scan.maxNodes (default 256). Bidirectional: raises OR lowers the recommended cap on classified nodes. When a batch hits the cap, additional files are dropped and the UI surfaces the persistent oversized banner. Validation: integer >= 1."
24654
+ description: "Per-batch override of scan.maxNodes (default 256), the MAP RENDER cap (pure metadata): it does NOT bound the scan, only the graph projection. Bidirectional: raises OR lowers the render cap. Validation: integer >= 1."
24163
24655
  });
24164
24656
  // Long-running verb, the watcher prints its own "stopped" line on
24165
24657
  // SIGINT / SIGTERM. Adding `done in <…>` after that would be noise.
@@ -24168,6 +24660,8 @@ var WatchCommand = class extends SmCommand {
24168
24660
  const roots = this.roots.length > 0 ? this.roots : ["."];
24169
24661
  const breaker = parseBreakerLimit(this.maxConsecutiveFailures, this.context.stderr, this.noColor);
24170
24662
  if (breaker === null) return ExitCode.Error;
24663
+ const maxScan = parseMaxScanLimit(this.maxScan, this.context.stderr, this.noColor);
24664
+ if (maxScan === null) return ExitCode.Error;
24171
24665
  const maxNodes = parseMaxNodesLimit(this.maxNodes, this.context.stderr, this.noColor);
24172
24666
  if (maxNodes === null) return ExitCode.Error;
24173
24667
  const watchOpts = {
@@ -24182,6 +24676,7 @@ var WatchCommand = class extends SmCommand {
24182
24676
  printer: this.printer
24183
24677
  };
24184
24678
  if (breaker !== void 0) watchOpts.maxConsecutiveFailures = breaker;
24679
+ if (maxScan !== void 0) watchOpts.maxScan = maxScan;
24185
24680
  if (maxNodes !== void 0) watchOpts.maxNodes = maxNodes;
24186
24681
  return runWatchLoop(watchOpts);
24187
24682
  }
@@ -24203,6 +24698,23 @@ function parseBreakerLimit(raw, stderr, noColor) {
24203
24698
  }
24204
24699
  return parsed;
24205
24700
  }
24701
+ function parseMaxScanLimit(raw, stderr, noColor) {
24702
+ if (raw === void 0) return void 0;
24703
+ const n = Number(raw);
24704
+ if (!Number.isInteger(n) || n < 1) {
24705
+ const stderrTty = stderr;
24706
+ const ansi = ansiFor({ isTTY: stderrTty.isTTY === true, noColorFlag: noColor });
24707
+ stderr.write(
24708
+ tx(WATCH_TEXTS.maxScanInvalid, {
24709
+ glyph: ansi.red("\u2715"),
24710
+ raw,
24711
+ hint: ansi.dim(WATCH_TEXTS.maxScanInvalidHint)
24712
+ })
24713
+ );
24714
+ return null;
24715
+ }
24716
+ return n;
24717
+ }
24206
24718
  function parseMaxNodesLimit(raw, stderr, noColor) {
24207
24719
  if (raw === void 0) return void 0;
24208
24720
  const n = Number(raw);
@@ -24287,16 +24799,20 @@ var ScanCommand = class extends SmCommand {
24287
24799
  yes = Option30.Boolean("--yes", false, {
24288
24800
  description: "Non-interactive mode. For ambiguous activeProvider auto-detect, multiple provider markers (.claude/, .codex/, AGENTS.md, .cursor/) under the scan tree exit non-zero instead of prompting; set the lens manually via `sm config set activeProvider <id>` and re-run. Also auto-confirms the pre-1.0 schema-drift rebuild (when the DB was written by a different skill-map major.minor it is deleted and regenerated) instead of prompting."
24289
24801
  });
24802
+ maxScan = Option30.String("--max-scan", {
24803
+ required: false,
24804
+ description: "Per-invocation override of `scan.maxScan` (default 50000). The WALK-INTAKE ceiling: the scan walks, parses, analyzes, and reference-validates the full corpus up to this number. Bidirectional: raises OR lowers the ceiling. When the walker hits it, additional files are dropped in stable order and the scan is marked truncated in scan_meta (the UI raises a persistent banner pointing at the .skillmapignore editor in Settings \u2192 Project). Validation: integer >= 1."
24805
+ });
24290
24806
  maxNodes = Option30.String("--max-nodes", {
24291
24807
  required: false,
24292
- description: "Per-invocation override of `scan.maxNodes` (default 256). Bidirectional: raises OR lowers the recommended cap on classified nodes. When the walker hits the cap, additional files are dropped and the scan is marked oversized in scan_meta (the UI raises a persistent banner pointing at the .skillmapignore editor in Settings \u2192 Project). Validation: integer >= 1."
24808
+ description: "Per-invocation override of `scan.maxNodes` (default 256). The MAP RENDER cap (pure metadata): it does NOT bound the scan, only how many nodes the graph view projects onto the canvas. Bidirectional: raises OR lowers the render cap. Validation: integer >= 1."
24293
24809
  });
24294
24810
  // Each branch in the orchestrator maps to one validation gate
24295
24811
  // (--watch alias / --changed mutex / -g mutex / dispatch).
24296
24812
  // Splitting per branch scatters the gate from the value it gates.
24297
24813
  async run() {
24298
- const parsedMaxNodes = this.parseMaxNodesFlag();
24299
- if (parsedMaxNodes.kind === "error") return parsedMaxNodes.exit;
24814
+ const caps = this.parseCapFlags();
24815
+ if (caps.kind === "error") return caps.exit;
24300
24816
  if (this.watch) return this.runWatchAlias();
24301
24817
  if (this.changed && this.noBuiltIns) {
24302
24818
  const ansi = this.ansiFor("stderr");
@@ -24333,7 +24849,7 @@ var ScanCommand = class extends SmCommand {
24333
24849
  colorEnabled,
24334
24850
  yes: this.yes,
24335
24851
  style,
24336
- ...parsedMaxNodes.value !== void 0 ? { maxNodes: parsedMaxNodes.value } : {}
24852
+ ...capOverrides(caps)
24337
24853
  });
24338
24854
  if (outcome.kind === "ok") {
24339
24855
  setScanExtensions(buildScanExtensionSet(outcome.executedExtensionIds));
@@ -24348,21 +24864,35 @@ var ScanCommand = class extends SmCommand {
24348
24864
  return this.renderFailure(outcome);
24349
24865
  }
24350
24866
  /**
24351
- * Parse `--max-nodes <N>`. Returns either the integer value (or
24352
- * `undefined` when the flag was omitted) or an error sentinel after
24353
- * printing the validation block. Invalid (non-integer, < 1) exits 2
24354
- * per spec/cli-contract.md §Node cap.
24867
+ * Parse both cap flags in one pass: `--max-scan <N>` (the WALK-INTAKE
24868
+ * ceiling) and `--max-nodes <N>` (the MAP RENDER cap). Returns both
24869
+ * resolved values (each `undefined` when its flag was omitted) or an
24870
+ * error sentinel after printing the §3.1b validation block for the
24871
+ * first offending flag. Invalid (non-integer, < 1) exits 2 per
24872
+ * spec/cli-contract.md §Scan.
24873
+ */
24874
+ parseCapFlags() {
24875
+ const scan = this.parseIntegerFlag(this.maxScan, SCAN_TEXTS.maxScanInvalid, SCAN_TEXTS.maxScanInvalidHint);
24876
+ if (scan.kind === "error") return scan;
24877
+ const nodes = this.parseIntegerFlag(this.maxNodes, SCAN_TEXTS.maxNodesInvalid, SCAN_TEXTS.maxNodesInvalidHint);
24878
+ if (nodes.kind === "error") return nodes;
24879
+ return { kind: "ok", maxScan: scan.value, maxNodes: nodes.value };
24880
+ }
24881
+ /**
24882
+ * Shared integer-flag parser for `--max-scan` / `--max-nodes`. Both
24883
+ * accept the same shape (integer >= 1) and render the same §3.1b
24884
+ * validation block; only the template + hint differ.
24355
24885
  */
24356
- parseMaxNodesFlag() {
24357
- if (this.maxNodes === void 0) return { kind: "ok", value: void 0 };
24358
- const n = Number(this.maxNodes);
24886
+ parseIntegerFlag(raw, invalidTemplate, invalidHint) {
24887
+ if (raw === void 0) return { kind: "ok", value: void 0 };
24888
+ const n = Number(raw);
24359
24889
  if (!Number.isInteger(n) || n < 1) {
24360
24890
  const ansi = this.ansiFor("stderr");
24361
24891
  this.printer.info(
24362
- tx(SCAN_TEXTS.maxNodesInvalid, {
24892
+ tx(invalidTemplate, {
24363
24893
  glyph: ansi.red("\u2715"),
24364
- value: this.maxNodes,
24365
- hint: ansi.dim(SCAN_TEXTS.maxNodesInvalidHint)
24894
+ value: raw,
24895
+ hint: ansi.dim(invalidHint)
24366
24896
  })
24367
24897
  );
24368
24898
  return { kind: "error", exit: ExitCode.Error };
@@ -24388,7 +24918,7 @@ var ScanCommand = class extends SmCommand {
24388
24918
  }
24389
24919
  this.emitElapsed = false;
24390
24920
  const roots = this.roots.length > 0 ? this.roots : ["."];
24391
- const parsedMaxNodes = this.parseMaxNodesFlag();
24921
+ const caps = this.parseCapFlags();
24392
24922
  return runWatchLoop({
24393
24923
  roots,
24394
24924
  json: this.json,
@@ -24399,7 +24929,7 @@ var ScanCommand = class extends SmCommand {
24399
24929
  noPlugins: this.noPlugins,
24400
24930
  context: this.context,
24401
24931
  printer: this.printer,
24402
- ...parsedMaxNodes.kind === "ok" && parsedMaxNodes.value !== void 0 ? { maxNodes: parsedMaxNodes.value } : {}
24932
+ ...caps.kind === "ok" ? capOverrides(caps) : {}
24403
24933
  });
24404
24934
  }
24405
24935
  /**
@@ -24515,24 +25045,24 @@ var ScanCommand = class extends SmCommand {
24515
25045
  );
24516
25046
  }
24517
25047
  /**
24518
- * Surface the §Node cap notice when the walker actually stopped
24519
- * accepting files because of the cap. Derivation: `filesWalked >
24520
- * effectiveLimit` means the walker incremented past the cap at least
24521
- * once (i.e. classified the (limit+1)-th raw before breaking). When
24522
- * the project has EXACTLY the cap many files the loop ends naturally
24523
- * without ever firing the break, so the notice stays silent.
25048
+ * Surface the §Scan truncation notice when the walker actually
25049
+ * stopped accepting files because the walk ceiling (`scan.maxScan` or
25050
+ * the `--max-scan` override) was reached and extra files were dropped.
25051
+ * Fires on `result.scanTruncated`, the kernel sets it when the walker
25052
+ * hit the ceiling; a project with at most the ceiling many files ends
25053
+ * the loop naturally with `scanTruncated: false`, so the notice stays
25054
+ * silent.
24524
25055
  */
24525
25056
  maybePrintCapNotice(result, ansi) {
24526
- const recommended = result.recommendedNodeLimit;
24527
- if (recommended === void 0) return;
24528
- const override = result.overrideMaxNodes ?? null;
24529
- const effectiveLimit = override ?? recommended;
24530
- if (result.stats.filesWalked <= effectiveLimit) return;
25057
+ if (result.scanTruncated !== true) return;
25058
+ const ceiling = result.scanCeiling;
25059
+ if (ceiling === void 0) return;
25060
+ const source = this.maxScan !== void 0 ? "--max-scan" : "scan.maxScan";
24531
25061
  this.printer.info(
24532
25062
  tx(SCAN_TEXTS.scanCappedNotice, {
24533
25063
  glyph: ansi.yellow("\u26A0"),
24534
- limit: String(effectiveLimit),
24535
- source: override !== null ? "--max-nodes" : "scan.maxNodes",
25064
+ limit: String(ceiling),
25065
+ source,
24536
25066
  hint: ansi.dim(SCAN_TEXTS.scanCappedNoticeHint)
24537
25067
  })
24538
25068
  );
@@ -24625,6 +25155,12 @@ function formatScanCounts(opts) {
24625
25155
  function countNoun(count3, singular, plural) {
24626
25156
  return count3 === 1 ? singular : plural;
24627
25157
  }
25158
+ function capOverrides(caps) {
25159
+ const out = {};
25160
+ if (caps.maxScan !== void 0) out.maxScan = caps.maxScan;
25161
+ if (caps.maxNodes !== void 0) out.maxNodes = caps.maxNodes;
25162
+ return out;
25163
+ }
24628
25164
 
24629
25165
  // cli/commands/scan-compare.ts
24630
25166
  import { access, readFile as readFile5 } from "fs/promises";
@@ -24877,11 +25413,11 @@ import { existsSync as existsSync31 } from "fs";
24877
25413
  import { Command as Command34, Option as Option32 } from "clipanion";
24878
25414
 
24879
25415
  // kernel/util/dev-mode.ts
24880
- import { sep as sep6 } from "path";
25416
+ import { sep as sep7 } from "path";
24881
25417
  import { fileURLToPath as fileURLToPath5 } from "url";
24882
25418
  var SELF_PATH = fileURLToPath5(import.meta.url);
24883
- var IS_DEV_BUILD = isDevBuildFromPath(SELF_PATH, sep6);
24884
- function isDevBuildFromPath(filePath, separator = sep6) {
25419
+ var IS_DEV_BUILD = isDevBuildFromPath(SELF_PATH, sep7);
25420
+ function isDevBuildFromPath(filePath, separator = sep7) {
24885
25421
  return !filePath.includes(`${separator}node_modules${separator}`);
24886
25422
  }
24887
25423
  function isDevBuild() {
@@ -24907,7 +25443,7 @@ import { WebSocketServer } from "ws";
24907
25443
  // server/app.ts
24908
25444
  import { Hono } from "hono";
24909
25445
  import { bodyLimit } from "hono/body-limit";
24910
- import { HTTPException as HTTPException17 } from "hono/http-exception";
25446
+ import { HTTPException as HTTPException18 } from "hono/http-exception";
24911
25447
 
24912
25448
  // core/config/service.ts
24913
25449
  var ConfigService = class {
@@ -24987,6 +25523,12 @@ var SERVER_TEXTS = {
24987
25523
  // Pagination caps on /api/nodes.
24988
25524
  paginationLimitTooLarge: "limit={{value}} exceeds the maximum of {{max}}.",
24989
25525
  paginationInvalidInteger: "{{name}}={{value}} is not a non-negative integer.",
25526
+ // /api/branch, the `limit` query param must be a positive integer
25527
+ // (>= 1). Non-integer / zero / negative rejects with 400 bad-query
25528
+ // before the branch projection runs. A value above the scan's
25529
+ // effective maxRenderNodes is silently clamped down (never an error),
25530
+ // so this message only ever fires on a malformed / < 1 input.
25531
+ branchInvalidLimit: "limit={{value}} is not a positive integer (>= 1).",
24990
25532
  // Required-query-param miss (used by `parseRequiredString`). The
24991
25533
  // route names the offending parameter so the operator gets a useful
24992
25534
  // 400 instead of a generic "missing input".
@@ -25393,20 +25935,148 @@ function registerAnnotationsRoute(app, deps) {
25393
25935
  });
25394
25936
  }
25395
25937
 
25396
- // server/routes/contributions.ts
25397
- import { HTTPException as HTTPException3 } from "hono/http-exception";
25398
-
25399
- // server/util/parse-query.ts
25938
+ // server/routes/branch.ts
25400
25939
  import { HTTPException as HTTPException2 } from "hono/http-exception";
25401
- function parseCsv(value) {
25402
- if (value === void 0) return [];
25403
- return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
25404
- }
25940
+
25941
+ // server/envelope.ts
25942
+ var REST_ENVELOPE_SCHEMA_VERSION = "1";
25943
+ function buildListEnvelope(opts) {
25944
+ const counts = {
25945
+ total: opts.total,
25946
+ returned: opts.items.length
25947
+ };
25948
+ if (opts.page) counts.page = opts.page;
25949
+ return {
25950
+ schemaVersion: REST_ENVELOPE_SCHEMA_VERSION,
25951
+ kind: opts.kind,
25952
+ items: opts.items,
25953
+ filters: opts.filters,
25954
+ counts,
25955
+ kindRegistry: opts.kindRegistry,
25956
+ providerRegistry: opts.providerRegistry,
25957
+ contributionsRegistry: opts.contributionsRegistry
25958
+ };
25959
+ }
25960
+ function buildValueEnvelope(kind, value, kindRegistry, providerRegistry, contributionsRegistry) {
25961
+ return {
25962
+ schemaVersion: REST_ENVELOPE_SCHEMA_VERSION,
25963
+ kind,
25964
+ value,
25965
+ kindRegistry,
25966
+ providerRegistry,
25967
+ contributionsRegistry
25968
+ };
25969
+ }
25970
+
25971
+ // server/routes/branch.ts
25972
+ var DEFAULT_MAX_RENDER_NODES2 = 256;
25973
+ function registerBranchRoute(app, deps) {
25974
+ app.get("/api/branch", async (c) => {
25975
+ const prefixes = c.req.queries("path")?.filter((p) => p.length > 0) ?? [];
25976
+ const limitOverride = parseLimit(c.req.query("limit"));
25977
+ const loaded = await tryWithSqlite(
25978
+ { databasePath: deps.options.dbPath, autoBackup: false },
25979
+ async (adapter) => {
25980
+ const maxRenderNodes = await adapter.scans.effectiveMaxRenderNodes();
25981
+ const cap = limitOverride === void 0 ? maxRenderNodes : Math.min(limitOverride, maxRenderNodes);
25982
+ const [branch, favSet] = await Promise.all([
25983
+ adapter.scans.loadBranch(prefixes, cap),
25984
+ adapter.favorites.listPaths()
25985
+ ]);
25986
+ const paths = branch.nodes.map((n) => n.path);
25987
+ const [tagRows, contribRows] = await Promise.all([
25988
+ adapter.tags.listForPaths(paths),
25989
+ adapter.contributions.listForPaths(paths)
25990
+ ]);
25991
+ return { branch, favSet, tagRows, contribRows, cap };
25992
+ }
25993
+ );
25994
+ return c.json(buildBranchResponse(prefixes, loaded));
25995
+ });
25996
+ }
25997
+ function buildBranchResponse(prefixes, loaded) {
25998
+ if (loaded === null) {
25999
+ return {
26000
+ schemaVersion: REST_ENVELOPE_SCHEMA_VERSION,
26001
+ kind: "branch",
26002
+ branch: { paths: [...new Set(prefixes)], total: 0, rendered: 0, truncated: false, cap: DEFAULT_MAX_RENDER_NODES2 },
26003
+ nodes: [],
26004
+ links: [],
26005
+ issues: []
26006
+ };
26007
+ }
26008
+ const { branch, favSet, tagRows, contribRows, cap } = loaded;
26009
+ const tagsByPath = groupTagsByPath(tagRows);
26010
+ const contribByPath = groupContribsByPath(contribRows);
26011
+ const nodes = branch.nodes.map((n) => ({
26012
+ ...n,
26013
+ isFavorite: favSet.has(n.path),
26014
+ tags: tagsByPath.get(n.path) ?? [],
26015
+ contributions: contribByPath.get(n.path) ?? []
26016
+ }));
26017
+ const rendered = nodes.length;
26018
+ return {
26019
+ schemaVersion: REST_ENVELOPE_SCHEMA_VERSION,
26020
+ kind: "branch",
26021
+ branch: {
26022
+ // Echo the de-duped prefixes the storage layer actually scoped on.
26023
+ paths: branch.paths,
26024
+ total: branch.total,
26025
+ rendered,
26026
+ truncated: branch.total > cap,
26027
+ cap
26028
+ },
26029
+ nodes,
26030
+ links: branch.links,
26031
+ issues: branch.issues
26032
+ };
26033
+ }
26034
+ function parseLimit(raw) {
26035
+ if (raw === void 0 || raw.length === 0) return void 0;
26036
+ const trimmed = raw.trim();
26037
+ const parsed = Number.parseInt(trimmed, 10);
26038
+ if (!Number.isInteger(parsed) || parsed < 1 || String(parsed) !== trimmed) {
26039
+ throw new HTTPException2(400, {
26040
+ message: tx(SERVER_TEXTS.branchInvalidLimit, { value: raw })
26041
+ });
26042
+ }
26043
+ return parsed;
26044
+ }
26045
+ function groupTagsByPath(rows) {
26046
+ const buckets = /* @__PURE__ */ new Map();
26047
+ for (const r of rows) {
26048
+ const set = buckets.get(r.nodePath);
26049
+ if (set) set.add(r.tag);
26050
+ else buckets.set(r.nodePath, /* @__PURE__ */ new Set([r.tag]));
26051
+ }
26052
+ const out = /* @__PURE__ */ new Map();
26053
+ for (const [path, set] of buckets) out.set(path, [...set].sort());
26054
+ return out;
26055
+ }
26056
+ function groupContribsByPath(rows) {
26057
+ const out = /* @__PURE__ */ new Map();
26058
+ for (const r of rows) {
26059
+ const list = out.get(r.nodePath);
26060
+ if (list) list.push(r);
26061
+ else out.set(r.nodePath, [r]);
26062
+ }
26063
+ return out;
26064
+ }
26065
+
26066
+ // server/routes/contributions.ts
26067
+ import { HTTPException as HTTPException4 } from "hono/http-exception";
26068
+
26069
+ // server/util/parse-query.ts
26070
+ import { HTTPException as HTTPException3 } from "hono/http-exception";
26071
+ function parseCsv(value) {
26072
+ if (value === void 0) return [];
26073
+ return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
26074
+ }
25405
26075
  function parsePagination(query, defaults) {
25406
26076
  const offset = parseNonNegativeInt(query.offset, "offset", 0);
25407
26077
  const limit = parseNonNegativeInt(query.limit, "limit", defaults.limit);
25408
26078
  if (limit > defaults.max) {
25409
- throw new HTTPException2(400, {
26079
+ throw new HTTPException3(400, {
25410
26080
  message: tx(SERVER_TEXTS.paginationLimitTooLarge, {
25411
26081
  value: limit,
25412
26082
  max: defaults.max
@@ -25420,7 +26090,7 @@ function parseBooleanFlag(value) {
25420
26090
  }
25421
26091
  function parseRequiredString(value, name) {
25422
26092
  if (typeof value !== "string" || value.length === 0) {
25423
- throw new HTTPException2(400, {
26093
+ throw new HTTPException3(400, {
25424
26094
  message: tx(SERVER_TEXTS.queryRequiredString, { name })
25425
26095
  });
25426
26096
  }
@@ -25431,7 +26101,7 @@ function parseNonNegativeInt(raw, name, fallback) {
25431
26101
  const trimmed = raw.trim();
25432
26102
  const parsed = Number.parseInt(trimmed, 10);
25433
26103
  if (!Number.isInteger(parsed) || parsed < 0 || String(parsed) !== trimmed) {
25434
- throw new HTTPException2(400, {
26104
+ throw new HTTPException3(400, {
25435
26105
  message: tx(SERVER_TEXTS.paginationInvalidInteger, { name, value: raw })
25436
26106
  });
25437
26107
  }
@@ -25442,7 +26112,7 @@ function parseNonNegativeInt(raw, name, fallback) {
25442
26112
  var QUALIFIED_ID_SEGMENT = /^[A-Za-z0-9._-]+$/;
25443
26113
  function parseQualifiedIdSegment(value, name) {
25444
26114
  if (!QUALIFIED_ID_SEGMENT.test(value)) {
25445
- throw new HTTPException3(400, {
26115
+ throw new HTTPException4(400, {
25446
26116
  message: tx(SERVER_TEXTS.qualifiedIdMalformed, {
25447
26117
  name,
25448
26118
  value: sanitizeForTerminal(value)
@@ -25472,7 +26142,7 @@ function registerContributionsRoutes(app, deps) {
25472
26142
  (e) => e.pluginId === pluginId && e.extensionId === extensionId && e.contributionId === contributionId
25473
26143
  );
25474
26144
  if (!catalogEntry) {
25475
- throw new HTTPException3(404, {
26145
+ throw new HTTPException4(404, {
25476
26146
  message: tx(SERVER_TEXTS.contributionUnknown, {
25477
26147
  pluginId: sanitizeForTerminal(pluginId),
25478
26148
  extensionId: sanitizeForTerminal(extensionId),
@@ -25503,46 +26173,14 @@ function registerContributionsRoutes(app, deps) {
25503
26173
  }
25504
26174
 
25505
26175
  // server/routes/config.ts
25506
- import { HTTPException as HTTPException4 } from "hono/http-exception";
25507
-
25508
- // server/envelope.ts
25509
- var REST_ENVELOPE_SCHEMA_VERSION = "1";
25510
- function buildListEnvelope(opts) {
25511
- const counts = {
25512
- total: opts.total,
25513
- returned: opts.items.length
25514
- };
25515
- if (opts.page) counts.page = opts.page;
25516
- return {
25517
- schemaVersion: REST_ENVELOPE_SCHEMA_VERSION,
25518
- kind: opts.kind,
25519
- items: opts.items,
25520
- filters: opts.filters,
25521
- counts,
25522
- kindRegistry: opts.kindRegistry,
25523
- providerRegistry: opts.providerRegistry,
25524
- contributionsRegistry: opts.contributionsRegistry
25525
- };
25526
- }
25527
- function buildValueEnvelope(kind, value, kindRegistry, providerRegistry, contributionsRegistry) {
25528
- return {
25529
- schemaVersion: REST_ENVELOPE_SCHEMA_VERSION,
25530
- kind,
25531
- value,
25532
- kindRegistry,
25533
- providerRegistry,
25534
- contributionsRegistry
25535
- };
25536
- }
25537
-
25538
- // server/routes/config.ts
26176
+ import { HTTPException as HTTPException5 } from "hono/http-exception";
25539
26177
  function registerConfigRoute(app, deps) {
25540
26178
  app.get("/api/config", (c) => {
25541
26179
  let loaded;
25542
26180
  try {
25543
26181
  loaded = deps.configService.get();
25544
26182
  } catch (err) {
25545
- throw new HTTPException4(500, { message: formatErrorMessage(err) });
26183
+ throw new HTTPException5(500, { message: formatErrorMessage(err) });
25546
26184
  }
25547
26185
  for (const warn of loaded.warnings) {
25548
26186
  log.warn(sanitizeForTerminal(warn));
@@ -25560,7 +26198,7 @@ function registerConfigRoute(app, deps) {
25560
26198
  }
25561
26199
 
25562
26200
  // server/routes/favorites.ts
25563
- import { HTTPException as HTTPException5 } from "hono/http-exception";
26201
+ import { HTTPException as HTTPException6 } from "hono/http-exception";
25564
26202
 
25565
26203
  // server/path-codec.ts
25566
26204
  var PathCodecError = class extends Error {
@@ -25600,7 +26238,7 @@ function registerFavoritesRoutes(app, deps) {
25600
26238
  }
25601
26239
  );
25602
26240
  if (!result || !result.found) {
25603
- throw new HTTPException5(404, {
26241
+ throw new HTTPException6(404, {
25604
26242
  message: tx(SERVER_TEXTS.nodeNotFound, { path: sanitizeForTerminal(nodePath) })
25605
26243
  });
25606
26244
  }
@@ -25620,14 +26258,57 @@ function decodePath(pathB64) {
25620
26258
  return decodeNodePath(pathB64);
25621
26259
  } catch (err) {
25622
26260
  if (err instanceof PathCodecError) {
25623
- throw new HTTPException5(404, { message: SERVER_TEXTS.pathB64Malformed });
26261
+ throw new HTTPException6(404, { message: SERVER_TEXTS.pathB64Malformed });
25624
26262
  }
25625
26263
  throw err;
25626
26264
  }
25627
26265
  }
25628
26266
 
26267
+ // server/routes/folders.ts
26268
+ function registerFoldersRoute(app, deps) {
26269
+ app.get("/api/folders", async (c) => {
26270
+ const loaded = await tryWithSqlite(
26271
+ { databasePath: deps.options.dbPath, autoBackup: false },
26272
+ async (adapter) => {
26273
+ const [liteNodes2, issueCounts2] = await Promise.all([
26274
+ adapter.scans.listLiteNodes(),
26275
+ adapter.scans.issueCountsByPath()
26276
+ ]);
26277
+ return { liteNodes: liteNodes2, issueCounts: issueCounts2 };
26278
+ }
26279
+ );
26280
+ const liteNodes = loaded?.liteNodes ?? [];
26281
+ const issueCounts = loaded?.issueCounts ?? /* @__PURE__ */ new Map();
26282
+ const items = liteNodes.map((n) => {
26283
+ const counts = issueCounts.get(n.path);
26284
+ return {
26285
+ path: n.path,
26286
+ kind: n.kind,
26287
+ linksInCount: n.linksInCount,
26288
+ linksOutCount: n.linksOutCount,
26289
+ tokensTotal: n.tokensTotal,
26290
+ modifiedAtMs: n.modifiedAtMs,
26291
+ errorCount: counts?.error ?? 0,
26292
+ warnCount: counts?.warn ?? 0,
26293
+ sidecarStatus: n.sidecarStatus
26294
+ };
26295
+ });
26296
+ return c.json(
26297
+ buildListEnvelope({
26298
+ kind: "folders",
26299
+ items,
26300
+ filters: {},
26301
+ total: items.length,
26302
+ kindRegistry: deps.kindRegistry,
26303
+ providerRegistry: deps.providerRegistry,
26304
+ contributionsRegistry: deps.contributionsRegistry
26305
+ })
26306
+ );
26307
+ });
26308
+ }
26309
+
25629
26310
  // server/routes/graph.ts
25630
- import { HTTPException as HTTPException6 } from "hono/http-exception";
26311
+ import { HTTPException as HTTPException7 } from "hono/http-exception";
25631
26312
  var DEFAULT_FORMAT2 = "ascii";
25632
26313
  var FORMAT_ID_PATTERN = /^[a-z0-9-]+$/;
25633
26314
  var FORMAT_ID_MAX = 32;
@@ -25635,7 +26316,7 @@ function registerGraphRoute(app, deps) {
25635
26316
  app.get("/api/graph", async (c) => {
25636
26317
  const format = c.req.query("format") ?? DEFAULT_FORMAT2;
25637
26318
  if (format.length > FORMAT_ID_MAX || !FORMAT_ID_PATTERN.test(format)) {
25638
- throw new HTTPException6(400, {
26319
+ throw new HTTPException7(400, {
25639
26320
  // Sanitize defensively, the regex above already rejects ANSI
25640
26321
  // and control bytes, but the message interpolates user input
25641
26322
  // and the BFF mirrors error envelopes into the server log.
@@ -25651,7 +26332,7 @@ function registerGraphRoute(app, deps) {
25651
26332
  const formatter = formatters.find((f) => f.formatId === format);
25652
26333
  if (!formatter) {
25653
26334
  const available = formatters.map((f) => f.formatId).sort().join(", ");
25654
- throw new HTTPException6(400, {
26335
+ throw new HTTPException7(400, {
25655
26336
  message: tx(SERVER_TEXTS.graphUnknownFormat, {
25656
26337
  format,
25657
26338
  available: available || "(none)"
@@ -25827,18 +26508,18 @@ function registerLinksRoute(app, deps) {
25827
26508
  }
25828
26509
 
25829
26510
  // server/routes/nodes.ts
25830
- import { HTTPException as HTTPException7 } from "hono/http-exception";
26511
+ import { HTTPException as HTTPException8 } from "hono/http-exception";
25831
26512
 
25832
26513
  // server/node-body.ts
25833
26514
  import { constants as fsConstants2 } from "fs";
25834
26515
  import { open } from "fs/promises";
25835
- import { isAbsolute as isAbsolute10, resolve as resolvePath2, relative as relativePath, sep as sep7 } from "path";
26516
+ import { isAbsolute as isAbsolute13, resolve as resolvePath2, relative as relativePath, sep as sep8 } from "path";
25836
26517
  async function readNodeBody(cwd, relPath) {
25837
- if (isAbsolute10(relPath)) return null;
26518
+ if (isAbsolute13(relPath)) return null;
25838
26519
  const absRoot = resolvePath2(cwd);
25839
26520
  const absFile = resolvePath2(absRoot, relPath);
25840
26521
  const rel = relativePath(absRoot, absFile);
25841
- if (rel.startsWith("..") || rel.startsWith(sep7) || rel.length === 0) {
26522
+ if (rel.startsWith("..") || rel.startsWith(sep8) || rel.length === 0) {
25842
26523
  return null;
25843
26524
  }
25844
26525
  let raw;
@@ -25938,7 +26619,7 @@ function registerNodesRoutes(app, deps) {
25938
26619
  nodePath = decodeNodePath(pathB64);
25939
26620
  } catch (err) {
25940
26621
  if (err instanceof PathCodecError) {
25941
- throw new HTTPException7(404, { message: SERVER_TEXTS.pathB64Malformed });
26622
+ throw new HTTPException8(404, { message: SERVER_TEXTS.pathB64Malformed });
25942
26623
  }
25943
26624
  throw err;
25944
26625
  }
@@ -25970,7 +26651,7 @@ function registerNodesRoutes(app, deps) {
25970
26651
  const contributions = result?.contributions ?? [];
25971
26652
  const tags = result?.tags ?? [];
25972
26653
  if (!bundle) {
25973
- throw new HTTPException7(404, {
26654
+ throw new HTTPException8(404, {
25974
26655
  message: tx(SERVER_TEXTS.nodeNotFound, { path: sanitizeForTerminal(nodePath) })
25975
26656
  });
25976
26657
  }
@@ -26022,7 +26703,7 @@ function registerNodesRoutes(app, deps) {
26022
26703
  const contribByPath = contributionsOmitted ? /* @__PURE__ */ new Map() : await groupContributionsByPath(
26023
26704
  await adapter.contributions.listForPaths(pagePaths)
26024
26705
  );
26025
- const tagByPath = groupTagsByPath(
26706
+ const tagByPath = groupTagsByPath2(
26026
26707
  await adapter.tags.listForPaths(pagePaths)
26027
26708
  );
26028
26709
  return { contributionsByPath: contribByPath, tagsByPath: tagByPath };
@@ -26058,7 +26739,7 @@ function registerNodesRoutes(app, deps) {
26058
26739
  function parseIncludes(raw) {
26059
26740
  return new Set(parseCsv(raw));
26060
26741
  }
26061
- function groupTagsByPath(rows) {
26742
+ function groupTagsByPath2(rows) {
26062
26743
  const buckets = /* @__PURE__ */ new Map();
26063
26744
  for (const r of rows) {
26064
26745
  const set = buckets.get(r.nodePath);
@@ -26080,11 +26761,11 @@ async function groupContributionsByPath(rows) {
26080
26761
  }
26081
26762
 
26082
26763
  // server/routes/plugins.ts
26083
- import { HTTPException as HTTPException9 } from "hono/http-exception";
26764
+ import { HTTPException as HTTPException10 } from "hono/http-exception";
26084
26765
 
26085
26766
  // server/util/parse-body.ts
26086
26767
  import { Ajv2020 as Ajv20207 } from "ajv/dist/2020.js";
26087
- import { HTTPException as HTTPException8 } from "hono/http-exception";
26768
+ import { HTTPException as HTTPException9 } from "hono/http-exception";
26088
26769
  function makeBodyValidator(schema, messages) {
26089
26770
  const ajv = new Ajv20207({ strict: false, allErrors: false });
26090
26771
  const validate = ajv.compile(schema);
@@ -26093,16 +26774,16 @@ function makeBodyValidator(schema, messages) {
26093
26774
  try {
26094
26775
  raw = await req.json();
26095
26776
  } catch {
26096
- throw new HTTPException8(400, { message: messages.notJson });
26777
+ throw new HTTPException9(400, { message: messages.notJson });
26097
26778
  }
26098
26779
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
26099
- throw new HTTPException8(400, { message: messages.notObject });
26780
+ throw new HTTPException9(400, { message: messages.notObject });
26100
26781
  }
26101
26782
  if (validate(raw)) {
26102
26783
  return raw;
26103
26784
  }
26104
26785
  const message = resolveErrorMessage(validate.errors, messages);
26105
- throw new HTTPException8(400, { message });
26786
+ throw new HTTPException9(400, { message });
26106
26787
  };
26107
26788
  }
26108
26789
  function resolveErrorMessage(errors, messages) {
@@ -26285,18 +26966,18 @@ function registerPluginsRoute(app, deps) {
26285
26966
  app.patch("/api/plugins/:id", async (c) => {
26286
26967
  const id = c.req.param("id");
26287
26968
  if (id.includes("/")) {
26288
- throw new HTTPException9(400, {
26969
+ throw new HTTPException10(400, {
26289
26970
  message: tx(SERVER_TEXTS.pluginsCascadeRouteQualifiedRejected, { id })
26290
26971
  });
26291
26972
  }
26292
26973
  const handle = findHandle(id, deps);
26293
26974
  if (!handle) {
26294
- throw new HTTPException9(404, {
26975
+ throw new HTTPException10(404, {
26295
26976
  message: tx(SERVER_TEXTS.pluginsUnknown, { id })
26296
26977
  });
26297
26978
  }
26298
26979
  if (isPluginLocked(id)) {
26299
- throw new HTTPException9(403, {
26980
+ throw new HTTPException10(403, {
26300
26981
  message: tx(SERVER_TEXTS.pluginsLocked, { id })
26301
26982
  });
26302
26983
  }
@@ -26310,18 +26991,18 @@ function registerPluginsRoute(app, deps) {
26310
26991
  const extensionId = c.req.param("extensionId");
26311
26992
  const handle = findHandle(pluginId, deps);
26312
26993
  if (!handle) {
26313
- throw new HTTPException9(404, {
26994
+ throw new HTTPException10(404, {
26314
26995
  message: tx(SERVER_TEXTS.pluginsUnknown, { id: pluginId })
26315
26996
  });
26316
26997
  }
26317
26998
  if (!hasExtension(handle, extensionId)) {
26318
- throw new HTTPException9(404, {
26999
+ throw new HTTPException10(404, {
26319
27000
  message: tx(SERVER_TEXTS.pluginsExtensionUnknown, { pluginId, extensionId })
26320
27001
  });
26321
27002
  }
26322
27003
  const qualified = qualifiedExtensionId(pluginId, extensionId);
26323
27004
  if (isPluginLocked(qualified) || isPluginLocked(pluginId)) {
26324
- throw new HTTPException9(403, {
27005
+ throw new HTTPException10(403, {
26325
27006
  message: tx(SERVER_TEXTS.pluginsExtensionLocked, { pluginId, extensionId })
26326
27007
  });
26327
27008
  }
@@ -26679,7 +27360,7 @@ function persistBulkSettings(deps, changes) {
26679
27360
  try {
26680
27361
  persistSettingsPatch(pluginId, extensionId, declarations, change.settings, cwd);
26681
27362
  } catch (err) {
26682
- throw new HTTPException9(500, {
27363
+ throw new HTTPException10(500, {
26683
27364
  message: tx(SERVER_TEXTS.pluginsSettingsPersistFailed, {
26684
27365
  id: change.id,
26685
27366
  message: err instanceof Error ? err.message : String(err)
@@ -26727,7 +27408,7 @@ function hasExtension(handle, extensionId) {
26727
27408
  }
26728
27409
 
26729
27410
  // server/routes/preferences.ts
26730
- import { HTTPException as HTTPException10 } from "hono/http-exception";
27411
+ import { HTTPException as HTTPException11 } from "hono/http-exception";
26731
27412
  function registerPreferencesRoute(app, _deps) {
26732
27413
  app.get("/api/preferences", (c) => {
26733
27414
  return c.json(buildEnvelope());
@@ -26759,7 +27440,7 @@ function applyPatch(body) {
26759
27440
  applyTelemetryPatch(body.telemetry);
26760
27441
  }
26761
27442
  } catch (err) {
26762
- throw new HTTPException10(400, {
27443
+ throw new HTTPException11(400, {
26763
27444
  message: tx(SERVER_TEXTS.preferencesPersistFailed, {
26764
27445
  message: formatErrorMessage(err)
26765
27446
  })
@@ -26818,14 +27499,14 @@ var parsePatchBody2 = makeBodyValidator(PATCH_BODY_SCHEMA, {
26818
27499
  });
26819
27500
 
26820
27501
  // server/routes/project-ignore.ts
26821
- import { HTTPException as HTTPException11 } from "hono/http-exception";
27502
+ import { HTTPException as HTTPException12 } from "hono/http-exception";
26822
27503
 
26823
27504
  // server/util/skillmapignore-io.ts
26824
27505
  import { existsSync as existsSync27, readFileSync as readFileSync18, writeFileSync as writeFileSync2 } from "fs";
26825
- import { resolve as resolve35 } from "path";
27506
+ import { resolve as resolve38 } from "path";
26826
27507
  var IGNORE_FILENAME2 = ".skillmapignore";
26827
27508
  function readPatterns(cwd) {
26828
- const path = resolve35(cwd, IGNORE_FILENAME2);
27509
+ const path = resolve38(cwd, IGNORE_FILENAME2);
26829
27510
  if (!existsSync27(path)) return [];
26830
27511
  let raw;
26831
27512
  try {
@@ -26836,7 +27517,7 @@ function readPatterns(cwd) {
26836
27517
  return raw.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
26837
27518
  }
26838
27519
  function writePatterns(cwd, nextPatterns) {
26839
- const path = resolve35(cwd, IGNORE_FILENAME2);
27520
+ const path = resolve38(cwd, IGNORE_FILENAME2);
26840
27521
  const prior = existsSync27(path) ? safeRead(path) : "";
26841
27522
  const content = buildContent(prior, nextPatterns);
26842
27523
  writeFileSync2(path, content, "utf8");
@@ -26905,12 +27586,12 @@ async function applyPatch2(deps, body) {
26905
27586
  for (const raw of body.patterns) {
26906
27587
  const t = raw.trim();
26907
27588
  if (t.length === 0) {
26908
- throw new HTTPException11(400, {
27589
+ throw new HTTPException12(400, {
26909
27590
  message: SERVER_TEXTS.projectIgnorePatternEmpty
26910
27591
  });
26911
27592
  }
26912
27593
  if (seen.has(t)) {
26913
- throw new HTTPException11(400, {
27594
+ throw new HTTPException12(400, {
26914
27595
  message: tx(SERVER_TEXTS.projectIgnorePatternDuplicate, { pattern: t })
26915
27596
  });
26916
27597
  }
@@ -26921,7 +27602,7 @@ async function applyPatch2(deps, body) {
26921
27602
  try {
26922
27603
  writePatterns(cwd, trimmed);
26923
27604
  } catch (err) {
26924
- throw new HTTPException11(400, {
27605
+ throw new HTTPException12(400, {
26925
27606
  message: tx(SERVER_TEXTS.projectIgnorePersistFailed, {
26926
27607
  message: formatErrorMessage(err)
26927
27608
  })
@@ -26999,7 +27680,7 @@ var parsePatchBody3 = makeBodyValidator(PATCH_BODY_SCHEMA2, {
26999
27680
 
27000
27681
  // server/routes/project-preferences.ts
27001
27682
  import { statSync as statSync9 } from "fs";
27002
- import { HTTPException as HTTPException12 } from "hono/http-exception";
27683
+ import { HTTPException as HTTPException13 } from "hono/http-exception";
27003
27684
  function registerProjectPreferencesRoute(app, deps) {
27004
27685
  app.get("/api/project-preferences", (c) => {
27005
27686
  return c.json(buildEnvelope3(deps));
@@ -27037,7 +27718,7 @@ function applyScanWrites(body, cwd) {
27037
27718
  if (writes.length === 0) return { attempted: false, mutated: false };
27038
27719
  const missingPaths = collectMissingPaths(writes, cwd);
27039
27720
  if (missingPaths.length > 0) {
27040
- throw new HTTPException12(400, {
27721
+ throw new HTTPException13(400, {
27041
27722
  message: tx(SERVER_TEXTS.projectPrefsPathNotFound, {
27042
27723
  paths: missingPaths.join(", ")
27043
27724
  })
@@ -27046,7 +27727,7 @@ function applyScanWrites(body, cwd) {
27046
27727
  const exposures = writes.map((w) => projectPathExposure({ key: w.key, value: w.value, cwd })).filter((e) => e.expandsSurface);
27047
27728
  if (exposures.length > 0 && body.confirm !== true) {
27048
27729
  const exposed = exposures.flatMap((e) => e.exposedPaths);
27049
- throw new HTTPException12(412, {
27730
+ throw new HTTPException13(412, {
27050
27731
  message: tx(SERVER_TEXTS.projectPrefsConfirmRequired, {
27051
27732
  paths: exposed.join(", ")
27052
27733
  })
@@ -27064,7 +27745,7 @@ function writeSidecarWritersPolicy(value, cwd) {
27064
27745
  try {
27065
27746
  writeConfigValue("allowSidecarWriters", value, { target: "project", cwd });
27066
27747
  } catch (err) {
27067
- throw new HTTPException12(400, {
27748
+ throw new HTTPException13(400, {
27068
27749
  message: tx(SERVER_TEXTS.projectPrefsPersistFailed, {
27069
27750
  key: "allowSidecarWriters",
27070
27751
  message: formatErrorMessage(err)
@@ -27115,7 +27796,7 @@ function runWrite(w, cwd) {
27115
27796
  try {
27116
27797
  writeConfigValue(w.key, w.value, { target: "project-local", cwd });
27117
27798
  } catch (err) {
27118
- throw new HTTPException12(400, {
27799
+ throw new HTTPException13(400, {
27119
27800
  message: tx(SERVER_TEXTS.projectPrefsPersistFailed, {
27120
27801
  key: w.key,
27121
27802
  message: formatErrorMessage(err)
@@ -27217,7 +27898,7 @@ var parsePatchBody4 = makeBodyValidator(PATCH_BODY_SCHEMA3, {
27217
27898
 
27218
27899
  // server/routes/active-provider.ts
27219
27900
  import { existsSync as existsSync28 } from "fs";
27220
- import { HTTPException as HTTPException13 } from "hono/http-exception";
27901
+ import { HTTPException as HTTPException14 } from "hono/http-exception";
27221
27902
  function registerActiveProviderRoute(app, deps) {
27222
27903
  app.get("/api/active-provider", async (c) => {
27223
27904
  return c.json(await buildEnvelope4(deps));
@@ -27258,7 +27939,7 @@ function applyLensSwitch(deps, newValue) {
27258
27939
  try {
27259
27940
  writeConfigValue("activeProvider", newValue, { target: "project", cwd });
27260
27941
  } catch (err) {
27261
- throw new HTTPException13(400, {
27942
+ throw new HTTPException14(400, {
27262
27943
  message: tx(SERVER_TEXTS.activeProviderPersistFailed, {
27263
27944
  message: formatErrorMessage(err)
27264
27945
  })
@@ -27294,11 +27975,11 @@ var parsePatchBody5 = makeBodyValidator(PATCH_BODY_SCHEMA4, {
27294
27975
  });
27295
27976
 
27296
27977
  // server/routes/actions.ts
27297
- import { HTTPException as HTTPException15 } from "hono/http-exception";
27298
- import { resolve as resolve36 } from "path";
27978
+ import { HTTPException as HTTPException16 } from "hono/http-exception";
27979
+ import { resolve as resolve39 } from "path";
27299
27980
 
27300
27981
  // server/routes/node-loader.ts
27301
- import { HTTPException as HTTPException14 } from "hono/http-exception";
27982
+ import { HTTPException as HTTPException15 } from "hono/http-exception";
27302
27983
  async function loadNode(deps, nodePath) {
27303
27984
  const persisted = await tryWithSqlite(
27304
27985
  { databasePath: deps.options.dbPath, autoBackup: false },
@@ -27306,7 +27987,7 @@ async function loadNode(deps, nodePath) {
27306
27987
  );
27307
27988
  const node = persisted?.nodes.find((n) => n.path === nodePath);
27308
27989
  if (!node) {
27309
- throw new HTTPException14(404, {
27990
+ throw new HTTPException15(404, {
27310
27991
  message: tx(SERVER_TEXTS.nodeNotFound, { path: sanitizeForTerminal(nodePath) })
27311
27992
  });
27312
27993
  }
@@ -27355,9 +28036,9 @@ function registerActionsRoutes(app, deps) {
27355
28036
  let absPath;
27356
28037
  try {
27357
28038
  assertContained(deps.runtimeContext.cwd, node.path);
27358
- absPath = resolve36(deps.runtimeContext.cwd, node.path);
28039
+ absPath = resolve39(deps.runtimeContext.cwd, node.path);
27359
28040
  } catch (err) {
27360
- throw new HTTPException15(400, { message: formatErrorMessage(err) });
28041
+ throw new HTTPException16(400, { message: formatErrorMessage(err) });
27361
28042
  }
27362
28043
  const result = invokeAction(action, absPath, node, body, deps.runtimeContext.cwd);
27363
28044
  const report = result.report;
@@ -27397,7 +28078,7 @@ function registerActionsRoutes(app, deps) {
27397
28078
  }
27398
28079
  function parseSegment(value, name) {
27399
28080
  if (!QUALIFIED_ID_SEGMENT2.test(value)) {
27400
- throw new HTTPException15(400, {
28081
+ throw new HTTPException16(400, {
27401
28082
  message: tx(SERVER_TEXTS.qualifiedIdMalformed, {
27402
28083
  name,
27403
28084
  value: sanitizeForTerminal(value)
@@ -27410,7 +28091,7 @@ function resolveInvokableAction(kernel, actionId) {
27410
28091
  const ext = kernel.registry.get("action", actionId);
27411
28092
  const action = ext;
27412
28093
  if (!action || typeof action.invoke !== "function") {
27413
- throw new HTTPException15(404, {
28094
+ throw new HTTPException16(404, {
27414
28095
  message: tx(SERVER_TEXTS.actionUnknown, { actionId: sanitizeForTerminal(actionId) })
27415
28096
  });
27416
28097
  }
@@ -27442,7 +28123,7 @@ async function materializeWrites(writes, body, cwd) {
27442
28123
  } catch (err) {
27443
28124
  if (err instanceof EConsentRequiredError) throw err;
27444
28125
  if (err instanceof ESidecarWritersForbiddenError) throw err;
27445
- throw new HTTPException15(500, { message: formatErrorMessage(err) });
28126
+ throw new HTTPException16(500, { message: formatErrorMessage(err) });
27446
28127
  }
27447
28128
  }
27448
28129
  function buildEnvelope5(actionId, nodePath, report, startedAt) {
@@ -27455,7 +28136,7 @@ function buildEnvelope5(actionId, nodePath, report, startedAt) {
27455
28136
  }
27456
28137
 
27457
28138
  // server/routes/scan.ts
27458
- import { HTTPException as HTTPException16 } from "hono/http-exception";
28139
+ import { HTTPException as HTTPException17 } from "hono/http-exception";
27459
28140
 
27460
28141
  // server/scan-mutex.ts
27461
28142
  var inFlight = null;
@@ -27463,14 +28144,14 @@ async function withScanMutex(fn) {
27463
28144
  if (inFlight !== null) {
27464
28145
  throw new ScanBusyError();
27465
28146
  }
27466
- let resolve40;
28147
+ let resolve43;
27467
28148
  inFlight = new Promise((r) => {
27468
- resolve40 = r;
28149
+ resolve43 = r;
27469
28150
  });
27470
28151
  try {
27471
28152
  return await fn();
27472
28153
  } finally {
27473
- resolve40();
28154
+ resolve43();
27474
28155
  inFlight = null;
27475
28156
  }
27476
28157
  }
@@ -27491,6 +28172,99 @@ function noopWritable() {
27491
28172
  });
27492
28173
  }
27493
28174
 
28175
+ // core/runtime/i18n/scan-spinner.texts.ts
28176
+ var SCAN_SPINNER_TEXTS = {
28177
+ /**
28178
+ * Animated (TTY) / single-line (non-TTY) label shown while a batch is
28179
+ * in flight. No em dash, the trailing ellipsis is three ASCII dots.
28180
+ */
28181
+ scanning: "Scanning...",
28182
+ /**
28183
+ * Headline word for the completion line, framed by the caller as
28184
+ * `{{glyph}} {{updated}}` (+ optional ` · {{stats}}` segment).
28185
+ */
28186
+ updated: "Map updated",
28187
+ /**
28188
+ * Optional stats segment appended after the headline when the batch
28189
+ * outcome carried `nodesCount` / `durationMs`. The caller composes
28190
+ * whichever of `{{nodes}}` / `{{durationMs}}` were defined and joins
28191
+ * them with the middle-dot separator before passing the finished line.
28192
+ */
28193
+ nodesSegment: "{{nodes}} nodes",
28194
+ durationSegment: "{{durationMs}}ms"
28195
+ };
28196
+
28197
+ // core/runtime/scan-spinner.ts
28198
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
28199
+ var FRAME_INTERVAL_MS = 80;
28200
+ var CLEAR_LINE = "\r\x1B[2K";
28201
+ var ESC_CYAN = "\x1B[36m";
28202
+ var ESC_GREEN = "\x1B[32m";
28203
+ var ESC_RESET2 = "\x1B[0m";
28204
+ function createScanSpinner(stream, opts = {}) {
28205
+ const colorEnabled = opts.colorEnabled === true;
28206
+ const label = opts.label ?? SCAN_SPINNER_TEXTS.scanning;
28207
+ const isTty = stream.isTTY === true;
28208
+ let active2 = false;
28209
+ let frameIndex = 0;
28210
+ let timer = null;
28211
+ const safeWrite = (chunk) => {
28212
+ try {
28213
+ stream.write(chunk);
28214
+ } catch {
28215
+ }
28216
+ };
28217
+ const tintFrame = (frame) => colorEnabled ? `${ESC_CYAN}${frame}${ESC_RESET2}` : frame;
28218
+ const drawFrame = () => {
28219
+ const frame = FRAMES[frameIndex % FRAMES.length];
28220
+ frameIndex += 1;
28221
+ safeWrite(`${CLEAR_LINE}${tintFrame(frame)} ${label}`);
28222
+ };
28223
+ const start = () => {
28224
+ if (active2) return;
28225
+ active2 = true;
28226
+ if (!isTty) {
28227
+ safeWrite(`${label}
28228
+ `);
28229
+ return;
28230
+ }
28231
+ frameIndex = 0;
28232
+ drawFrame();
28233
+ timer = setInterval(drawFrame, FRAME_INTERVAL_MS);
28234
+ timer.unref?.();
28235
+ };
28236
+ const buildStatsSegment = (stats) => {
28237
+ const segments = [];
28238
+ if (stats.nodesCount !== void 0) {
28239
+ segments.push(tx(SCAN_SPINNER_TEXTS.nodesSegment, { nodes: stats.nodesCount }));
28240
+ }
28241
+ if (stats.durationMs !== void 0) {
28242
+ segments.push(tx(SCAN_SPINNER_TEXTS.durationSegment, { durationMs: stats.durationMs }));
28243
+ }
28244
+ return segments.length > 0 ? ` \xB7 ${segments.join(" \xB7 ")}` : "";
28245
+ };
28246
+ const stop = (stats) => {
28247
+ if (!active2) return;
28248
+ active2 = false;
28249
+ if (timer !== null) {
28250
+ clearInterval(timer);
28251
+ timer = null;
28252
+ }
28253
+ if (isTty) safeWrite(CLEAR_LINE);
28254
+ const check = colorEnabled ? `${ESC_GREEN}\u2713${ESC_RESET2}` : "\u2713";
28255
+ const statsSegment = stats ? buildStatsSegment(stats) : "";
28256
+ safeWrite(`${check} ${SCAN_SPINNER_TEXTS.updated}${statsSegment}
28257
+ `);
28258
+ };
28259
+ return {
28260
+ start,
28261
+ stop,
28262
+ get active() {
28263
+ return active2;
28264
+ }
28265
+ };
28266
+ }
28267
+
27494
28268
  // server/events.ts
27495
28269
  function buildWatcherStartedEvent(data) {
27496
28270
  return {
@@ -27512,6 +28286,9 @@ function buildWatcherErrorEvent(data) {
27512
28286
  // server/watcher.ts
27513
28287
  function createWatcherService(opts) {
27514
28288
  let currentRuntime = null;
28289
+ const spinner = opts.scanProgress ? createScanSpinner(opts.scanProgress.stream, {
28290
+ colorEnabled: opts.scanProgress.colorEnabled
28291
+ }) : void 0;
27515
28292
  const buildRuntimeOpts = () => {
27516
28293
  const runtimeOpts = {
27517
28294
  dbPath: opts.options.dbPath,
@@ -27527,7 +28304,16 @@ function createWatcherService(opts) {
27527
28304
  subscribeBeforeInitial: true,
27528
28305
  failOnInitialBatchError: false,
27529
28306
  events: {
28307
+ // Light the "scanning" indicator the moment a batch begins (file
28308
+ // save to re-scan). No-op when no `scanProgress` was passed.
28309
+ onBatchStart: () => spinner?.start(),
27530
28310
  onBatch: (outcome) => {
28311
+ spinner?.stop(
28312
+ outcome.kind === "ok" ? {
28313
+ nodesCount: outcome.result.stats.nodesCount,
28314
+ durationMs: outcome.result.stats.durationMs
28315
+ } : void 0
28316
+ );
27531
28317
  if (outcome.kind === "error") {
27532
28318
  log.warn(
27533
28319
  tx(SERVER_TEXTS.watcherBatchFailed, {
@@ -27573,6 +28359,9 @@ function createWatcherService(opts) {
27573
28359
  if (opts.debounceMsOverride !== void 0) {
27574
28360
  runtimeOpts.debounceMsOverride = opts.debounceMsOverride;
27575
28361
  }
28362
+ if (opts.options.maxScan !== void 0) {
28363
+ runtimeOpts.maxScanOverride = opts.options.maxScan;
28364
+ }
27576
28365
  if (opts.options.maxNodes !== void 0) {
27577
28366
  runtimeOpts.maxNodesOverride = opts.options.maxNodes;
27578
28367
  }
@@ -27628,13 +28417,16 @@ function registerScanRoute(app, deps) {
27628
28417
  if (parseBooleanFlag(c.req.query("fresh"))) {
27629
28418
  return c.json(await runFreshScan(deps));
27630
28419
  }
28420
+ if (parseBooleanFlag(c.req.query("meta"))) {
28421
+ return c.json(await loadPersistedScanMeta(deps));
28422
+ }
27631
28423
  return c.json(await loadPersistedScan(deps));
27632
28424
  });
27633
28425
  app.post("/api/scan", async (c) => runPersistedScan(c, deps));
27634
28426
  }
27635
28427
  async function runPersistedScan(c, deps) {
27636
28428
  if (deps.options.noBuiltIns || deps.options.noPlugins) {
27637
- throw new HTTPException16(400, { message: SERVER_TEXTS.scanPostRequiresFullPipeline });
28429
+ throw new HTTPException17(400, { message: SERVER_TEXTS.scanPostRequiresFullPipeline });
27638
28430
  }
27639
28431
  const dbExists = await tryWithSqlite(
27640
28432
  { databasePath: deps.options.dbPath, autoBackup: false },
@@ -27665,13 +28457,15 @@ async function runPersistedScan(c, deps) {
27665
28457
  // the operator via the Settings UI (PATCH /api/active-provider)
27666
28458
  // before the scan, not via interactive prompt here.
27667
28459
  yes: true,
27668
- // `--max-nodes` from the `sm serve` invocation (or the bare
27669
- // `sm --max-nodes <N>` shortcut) flows through to every scan
27670
- // the BFF runs so the override is honoured end-to-end.
28460
+ // `--max-scan` (walk ceiling) and `--max-nodes` (render cap)
28461
+ // from the `sm serve` invocation (or the bare `sm --max-scan
28462
+ // <N>` / `sm --max-nodes <N>` shortcut) flow through to every
28463
+ // scan the BFF runs so both overrides are honoured end-to-end.
28464
+ ...deps.options.maxScan !== void 0 ? { maxScan: deps.options.maxScan } : {},
27671
28465
  ...deps.options.maxNodes !== void 0 ? { maxNodes: deps.options.maxNodes } : {}
27672
28466
  });
27673
28467
  if (outcome.kind !== "ok") {
27674
- throw new HTTPException16(500, {
28468
+ throw new HTTPException17(500, {
27675
28469
  message: outcome.kind === "guard-trip" ? tx(SERVER_TEXTS.scanGuardTrip, { existing: outcome.existing }) : outcome.message
27676
28470
  });
27677
28471
  }
@@ -27691,6 +28485,20 @@ async function buildBffResolverOverride(deps) {
27691
28485
  fallbackResolver: deps.pluginRuntime.resolveEnabled
27692
28486
  });
27693
28487
  }
28488
+ async function loadPersistedScanMeta(deps) {
28489
+ const opened = await tryWithSqlite(
28490
+ {
28491
+ databasePath: deps.options.dbPath,
28492
+ autoBackup: false,
28493
+ versionCheck: { currentVersion: VERSION, printer: bffVersionCheckPrinter }
28494
+ },
28495
+ async (adapter) => adapter.scans.loadMeta()
28496
+ );
28497
+ if (opened === null) {
28498
+ return emptyScanResult();
28499
+ }
28500
+ return opened;
28501
+ }
27694
28502
  async function loadPersistedScan(deps) {
27695
28503
  const opened = await tryWithSqlite(
27696
28504
  {
@@ -27719,7 +28527,7 @@ async function loadPersistedScan(deps) {
27719
28527
  if (list) list.push(r);
27720
28528
  else byPath3.set(r.nodePath, [r]);
27721
28529
  }
27722
- const tagsByPath = groupTagsByPath2(tagRows);
28530
+ const tagsByPath = groupTagsByPath3(tagRows);
27723
28531
  return { loaded, favSet, contribByPath: byPath3, tagsByPath };
27724
28532
  }
27725
28533
  );
@@ -27736,7 +28544,7 @@ async function loadPersistedScan(deps) {
27736
28544
  }))
27737
28545
  };
27738
28546
  }
27739
- function groupTagsByPath2(rows) {
28547
+ function groupTagsByPath3(rows) {
27740
28548
  const buckets = /* @__PURE__ */ new Map();
27741
28549
  for (const r of rows) {
27742
28550
  const set = buckets.get(r.nodePath);
@@ -27749,7 +28557,7 @@ function groupTagsByPath2(rows) {
27749
28557
  }
27750
28558
  async function runFreshScan(deps) {
27751
28559
  if (deps.options.noBuiltIns || deps.options.noPlugins) {
27752
- throw new HTTPException16(400, { message: SERVER_TEXTS.freshScanRequiresPipeline });
28560
+ throw new HTTPException17(400, { message: SERVER_TEXTS.freshScanRequiresPipeline });
27753
28561
  }
27754
28562
  const resolveEnabledOverride = await buildBffResolverOverride(deps);
27755
28563
  const outcome = await runScanForCommand({
@@ -27779,12 +28587,14 @@ async function runFreshScan(deps) {
27779
28587
  // BFF has no TTY; ambiguous activeProvider is the operator's
27780
28588
  // problem to resolve via the Settings UI, not via prompt here.
27781
28589
  yes: true,
27782
- // Carry `--max-nodes` from `sm serve` into the fresh-scan path
27783
- // too so a UI-driven refresh honours the same cap as the watcher.
28590
+ // Carry `--max-scan` (walk ceiling) and `--max-nodes` (render cap)
28591
+ // from `sm serve` into the fresh-scan path too so a UI-driven
28592
+ // refresh honours the same knobs as the watcher.
28593
+ ...deps.options.maxScan !== void 0 ? { maxScan: deps.options.maxScan } : {},
27784
28594
  ...deps.options.maxNodes !== void 0 ? { maxNodes: deps.options.maxNodes } : {}
27785
28595
  });
27786
28596
  if (outcome.kind !== "ok") {
27787
- throw new HTTPException16(500, {
28597
+ throw new HTTPException17(500, {
27788
28598
  message: outcome.kind === "guard-trip" ? tx(SERVER_TEXTS.freshScanGuardTrip, { existing: outcome.existing }) : outcome.message
27789
28599
  });
27790
28600
  }
@@ -27810,12 +28620,14 @@ function emptyScanResult() {
27810
28620
  scannedAt: Date.now(),
27811
28621
  roots: ["."],
27812
28622
  providers: [],
27813
- // Surface the design default so the SPA reads the same field shape
27814
- // on cold boot as on populated DBs. 256 mirrors `scan.maxNodes`
27815
- // from `src/config/defaults.json`; the temporary testing default
27816
- // (2) only applies after a real scan walks through the kernel.
27817
- recommendedNodeLimit: 256,
27818
- overrideMaxNodes: null,
28623
+ // Surface the design defaults so the SPA reads the same field shape
28624
+ // on cold boot as on populated DBs. 50000 mirrors `scan.maxScan`
28625
+ // (the walk ceiling) and 256 mirrors `scan.maxNodes` (the render
28626
+ // cap), both from `src/config/defaults.json`. A real scan
28627
+ // overwrites these with the live values on next run.
28628
+ scanCeiling: 5e4,
28629
+ scanTruncated: false,
28630
+ maxRenderNodes: 256,
27819
28631
  nodes: [],
27820
28632
  links: [],
27821
28633
  issues: [],
@@ -27996,13 +28808,13 @@ function attachBroadcasterRoute(app, broadcaster) {
27996
28808
 
27997
28809
  // server/app.ts
27998
28810
  var BODY_LIMIT_BYTES = 1024 * 1024;
27999
- var DbMissingError = class extends HTTPException17 {
28811
+ var DbMissingError = class extends HTTPException18 {
28000
28812
  constructor(message) {
28001
28813
  super(500, { message });
28002
28814
  this.name = "DbMissingError";
28003
28815
  }
28004
28816
  };
28005
- var BulkValidationError = class extends HTTPException17 {
28817
+ var BulkValidationError = class extends HTTPException18 {
28006
28818
  id;
28007
28819
  code;
28008
28820
  constructor(init) {
@@ -28012,7 +28824,7 @@ var BulkValidationError = class extends HTTPException17 {
28012
28824
  this.code = init.code;
28013
28825
  }
28014
28826
  };
28015
- var LoopbackGateError = class extends HTTPException17 {
28827
+ var LoopbackGateError = class extends HTTPException18 {
28016
28828
  code;
28017
28829
  constructor(init) {
28018
28830
  super(403, { message: init.message });
@@ -28020,7 +28832,7 @@ var LoopbackGateError = class extends HTTPException17 {
28020
28832
  this.code = init.code;
28021
28833
  }
28022
28834
  };
28023
- var ConflictError = class extends HTTPException17 {
28835
+ var ConflictError = class extends HTTPException18 {
28024
28836
  code;
28025
28837
  constructor(init) {
28026
28838
  super(409, { message: init.message });
@@ -28028,7 +28840,7 @@ var ConflictError = class extends HTTPException17 {
28028
28840
  this.code = init.code;
28029
28841
  }
28030
28842
  };
28031
- var ActionRefusedError = class extends HTTPException17 {
28843
+ var ActionRefusedError = class extends HTTPException18 {
28032
28844
  /** Refusal code: the report's `reason` when present, else `'action-refused'`. */
28033
28845
  code;
28034
28846
  details;
@@ -28056,7 +28868,7 @@ function createApp(deps) {
28056
28868
  bodyLimit({
28057
28869
  maxSize: BODY_LIMIT_BYTES,
28058
28870
  onError: () => {
28059
- throw new HTTPException17(413, { message: tx(SERVER_TEXTS.bodyTooLarge, { maxBytes: String(BODY_LIMIT_BYTES) }) });
28871
+ throw new HTTPException18(413, { message: tx(SERVER_TEXTS.bodyTooLarge, { maxBytes: String(BODY_LIMIT_BYTES) }) });
28060
28872
  }
28061
28873
  })
28062
28874
  );
@@ -28089,6 +28901,8 @@ function createApp(deps) {
28089
28901
  registerNodesRoutes(app, routeDeps);
28090
28902
  registerLinksRoute(app, routeDeps);
28091
28903
  registerIssuesRoute(app, routeDeps);
28904
+ registerFoldersRoute(app, routeDeps);
28905
+ registerBranchRoute(app, routeDeps);
28092
28906
  registerGraphRoute(app, routeDeps);
28093
28907
  registerConfigRoute(app, routeDeps);
28094
28908
  registerPluginsRoute(app, routeDeps);
@@ -28102,7 +28916,7 @@ function createApp(deps) {
28102
28916
  registerActiveProviderRoute(app, routeDeps);
28103
28917
  registerProjectIgnoreRoute(app, routeDeps);
28104
28918
  app.all("/api/*", (c) => {
28105
- throw new HTTPException17(404, {
28919
+ throw new HTTPException18(404, {
28106
28920
  message: tx(SERVER_TEXTS.unknownApiEndpoint, { path: sanitizeForTerminal(c.req.path) })
28107
28921
  });
28108
28922
  });
@@ -28110,7 +28924,7 @@ function createApp(deps) {
28110
28924
  app.use("*", createStaticHandler({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
28111
28925
  app.get("*", createSpaFallback({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
28112
28926
  app.notFound((c) => {
28113
- throw new HTTPException17(404, {
28927
+ throw new HTTPException18(404, {
28114
28928
  message: tx(SERVER_TEXTS.unknownPath, { path: sanitizeForTerminal(c.req.path) })
28115
28929
  });
28116
28930
  });
@@ -28163,7 +28977,7 @@ function formatError2(err, c) {
28163
28977
  }
28164
28978
  const conflict = formatConflict(err, c);
28165
28979
  if (conflict) return conflict;
28166
- if (err instanceof HTTPException17) {
28980
+ if (err instanceof HTTPException18) {
28167
28981
  const status = err.status;
28168
28982
  const envelope = {
28169
28983
  ok: false,
@@ -28509,6 +29323,8 @@ function validateServerOptions(input) {
28509
29323
  if (watcherError !== null) return { ok: false, error: watcherError };
28510
29324
  const debounceError = validateWatcherDebounce(input.watcherDebounceMs);
28511
29325
  if (debounceError !== null) return { ok: false, error: debounceError };
29326
+ const maxScanError = validateMaxScan(input.maxScan);
29327
+ if (maxScanError !== null) return { ok: false, error: maxScanError };
28512
29328
  const maxNodesError = validateMaxNodes(input.maxNodes);
28513
29329
  if (maxNodesError !== null) return { ok: false, error: maxNodesError };
28514
29330
  const noUiError = validateNoUi(filled.noUi, filled.uiDist);
@@ -28528,6 +29344,9 @@ function validateServerOptions(input) {
28528
29344
  if (input.watcherDebounceMs !== void 0) {
28529
29345
  options.watcherDebounceMs = input.watcherDebounceMs;
28530
29346
  }
29347
+ if (input.maxScan !== void 0) {
29348
+ options.maxScan = input.maxScan;
29349
+ }
28531
29350
  if (input.maxNodes !== void 0) {
28532
29351
  options.maxNodes = input.maxNodes;
28533
29352
  }
@@ -28596,6 +29415,17 @@ function validateWatcherDebounce(value) {
28596
29415
  }
28597
29416
  return null;
28598
29417
  }
29418
+ function validateMaxScan(value) {
29419
+ if (value === void 0) return null;
29420
+ if (!Number.isInteger(value) || value < 1) {
29421
+ return {
29422
+ code: "max-scan-invalid",
29423
+ message: `--max-scan must be an integer >= 1 (got ${value})`,
29424
+ value: String(value)
29425
+ };
29426
+ }
29427
+ return null;
29428
+ }
28599
29429
  function validateMaxNodes(value) {
28600
29430
  if (value === void 0) return null;
28601
29431
  if (!Number.isInteger(value) || value < 1) {
@@ -28620,7 +29450,7 @@ function validateNoUi(noUi, uiDist) {
28620
29450
 
28621
29451
  // server/paths.ts
28622
29452
  import { existsSync as existsSync30, statSync as statSync10 } from "fs";
28623
- import { dirname as dirname19, isAbsolute as isAbsolute11, join as join20, resolve as resolve37 } from "path";
29453
+ import { dirname as dirname19, isAbsolute as isAbsolute14, join as join20, resolve as resolve40 } from "path";
28624
29454
  import { fileURLToPath as fileURLToPath6 } from "url";
28625
29455
  var DEFAULT_UI_REL = join20("ui", "dist", "ui", "browser");
28626
29456
  var PACKAGE_UI_REL = "ui";
@@ -28631,7 +29461,7 @@ function resolveDefaultUiDist(ctx) {
28631
29461
  return walkUpForUi(ctx.cwd);
28632
29462
  }
28633
29463
  function resolveExplicitUiDist(ctx, raw) {
28634
- return isAbsolute11(raw) ? raw : resolve37(ctx.cwd, raw);
29464
+ return isAbsolute14(raw) ? raw : resolve40(ctx.cwd, raw);
28635
29465
  }
28636
29466
  function isUiBundleDir(path) {
28637
29467
  if (!existsSync30(path)) return false;
@@ -28665,7 +29495,7 @@ function resolvePackageBundledUiFrom(here) {
28665
29495
  return null;
28666
29496
  }
28667
29497
  function walkUpForUi(startDir) {
28668
- let current = resolve37(startDir);
29498
+ let current = resolve40(startDir);
28669
29499
  for (let i = 0; i < 64; i++) {
28670
29500
  const candidate = join20(current, DEFAULT_UI_REL);
28671
29501
  if (isUiBundleDir(candidate)) return candidate;
@@ -28711,6 +29541,7 @@ async function createServer(options, extra = {}) {
28711
29541
  broadcaster
28712
29542
  };
28713
29543
  if (debounce !== void 0) svcOpts.debounceMsOverride = debounce;
29544
+ if (extra.scanProgress) svcOpts.scanProgress = extra.scanProgress;
28714
29545
  const candidate = createWatcherService(svcOpts);
28715
29546
  try {
28716
29547
  await candidate.start();
@@ -28904,6 +29735,12 @@ var SERVE_TEXTS = {
28904
29735
  */
28905
29736
  watcherDebounceInvalid: "{{glyph}} sm serve: --watcher-debounce-ms must be a non-negative integer (got {{value}}).\n {{hint}}\n",
28906
29737
  watcherDebounceInvalidHint: "Pass an integer >= 0 (e.g. 250).",
29738
+ /**
29739
+ * §3.1b error block for an invalid `--max-scan <N>`. Same shape as
29740
+ * the watcher-debounce template family.
29741
+ */
29742
+ maxScanInvalid: "{{glyph}} sm serve: --max-scan must be an integer >= 1 (got {{value}}).\n {{hint}}\n",
29743
+ maxScanInvalidHint: "Pass a positive integer, e.g. --max-scan 50000.",
28907
29744
  /**
28908
29745
  * §3.1b error block for an invalid `--max-nodes <N>`. Same shape as
28909
29746
  * the watcher-debounce template family.
@@ -28959,7 +29796,7 @@ var SERVE_TEXTS = {
28959
29796
  };
28960
29797
 
28961
29798
  // cli/util/serve-banner.ts
28962
- import { relative as relative7, isAbsolute as isAbsolute12 } from "path";
29799
+ import { relative as relative8, isAbsolute as isAbsolute15 } from "path";
28963
29800
  var ESC2 = {
28964
29801
  reset: "\x1B[0m",
28965
29802
  bold: "\x1B[1m",
@@ -29106,9 +29943,9 @@ function resolveAnsi(colorEnabled) {
29106
29943
  }
29107
29944
  function formatDbPath(dbPath, cwd) {
29108
29945
  const safe = sanitizeForTerminal(dbPath);
29109
- if (!isAbsolute12(safe)) return safe;
29110
- const rel = relative7(cwd, safe);
29111
- if (rel === "" || rel.startsWith("..") || isAbsolute12(rel)) {
29946
+ if (!isAbsolute15(safe)) return safe;
29947
+ const rel = relative8(cwd, safe);
29948
+ if (rel === "" || rel.startsWith("..") || isAbsolute15(rel)) {
29112
29949
  return safe;
29113
29950
  }
29114
29951
  return rel;
@@ -29189,9 +30026,13 @@ var ServeCommand = class extends SmCommand {
29189
30026
  // who want to tighten / relax the watcher's batching window without
29190
30027
  // editing settings.json. Hidden flag, the Usage block omits it.
29191
30028
  watcherDebounceMs = Option32.String("--watcher-debounce-ms", { required: false, hidden: true });
30029
+ maxScan = Option32.String("--max-scan", {
30030
+ required: false,
30031
+ description: "Per-invocation override of scan.maxScan (default 50000), the WALK-INTAKE ceiling. The scan walks, parses, analyzes, and reference-validates the full corpus up to this number. Bidirectional: raises OR lowers the ceiling. Applies to every scan the server runs (initial watcher pass, debounced batches, POST /api/scan, GET /api/scan?fresh=1). Same flag is honoured on the bare `sm` invocation, which routes to `sm serve`."
30032
+ });
29192
30033
  maxNodes = Option32.String("--max-nodes", {
29193
30034
  required: false,
29194
- description: "Per-invocation override of scan.maxNodes (default 256). Bidirectional: raises OR lowers the recommended cap on classified nodes. Applies to every scan the server runs (initial watcher pass, debounced batches, POST /api/scan, GET /api/scan?fresh=1). Same flag is honoured on the bare `sm` invocation, which routes to `sm serve`."
30035
+ description: "Per-invocation override of scan.maxNodes (default 256), the MAP RENDER cap (pure metadata): it does NOT bound the scan, only how many nodes the graph view projects onto the canvas. Bidirectional: raises OR lowers the render cap. Same flag is honoured on the bare `sm` invocation, which routes to `sm serve`."
29195
30036
  });
29196
30037
  // Long-running daemon, `done in <…>` after a graceful shutdown is
29197
30038
  // noise. Mirrors `sm watch`'s opt-out.
@@ -29271,6 +30112,17 @@ var ServeCommand = class extends SmCommand {
29271
30112
  );
29272
30113
  return ExitCode.Error;
29273
30114
  }
30115
+ const maxScanResult = parseMaxScan(this.maxScan);
30116
+ if (!maxScanResult.ok) {
30117
+ this.printer.info(
30118
+ tx(SERVE_TEXTS.maxScanInvalid, {
30119
+ glyph: errGlyph,
30120
+ value: sanitizeForTerminal(maxScanResult.value),
30121
+ hint: stderrAnsi.dim(SERVE_TEXTS.maxScanInvalidHint)
30122
+ })
30123
+ );
30124
+ return ExitCode.Error;
30125
+ }
29274
30126
  const maxNodesResult = parseMaxNodes(this.maxNodes);
29275
30127
  if (!maxNodesResult.ok) {
29276
30128
  this.printer.info(
@@ -29295,6 +30147,7 @@ var ServeCommand = class extends SmCommand {
29295
30147
  if (portResult.port !== void 0) input.port = portResult.port;
29296
30148
  if (this.host !== void 0) input.host = this.host;
29297
30149
  if (debounceResult.value !== void 0) input.watcherDebounceMs = debounceResult.value;
30150
+ if (maxScanResult.value !== void 0) input.maxScan = maxScanResult.value;
29298
30151
  if (maxNodesResult.value !== void 0) input.maxNodes = maxNodesResult.value;
29299
30152
  const validation = validateServerOptions(input);
29300
30153
  if (!validation.ok) {
@@ -29303,10 +30156,19 @@ var ServeCommand = class extends SmCommand {
29303
30156
  }
29304
30157
  const driftAbort = await this.#rebuildOnDrift(dbPath, stderrAnsi, warnGlyph);
29305
30158
  if (driftAbort !== null) return driftAbort;
30159
+ const stderr = this.context.stderr;
30160
+ const isTTY = stderr.isTTY === true;
30161
+ const colorEnabled = resolveColorEnabled({
30162
+ isTTY,
30163
+ noColorFlag: this.noColor,
30164
+ env: process.env
30165
+ });
29306
30166
  await initSentryBff(VERSION);
29307
30167
  let handle;
29308
30168
  try {
29309
- handle = await createServer(validation.options);
30169
+ handle = await createServer(validation.options, {
30170
+ scanProgress: { stream: stderr, colorEnabled }
30171
+ });
29310
30172
  } catch (err) {
29311
30173
  const message = formatErrorMessage(err);
29312
30174
  this.printer.info(
@@ -29319,13 +30181,6 @@ var ServeCommand = class extends SmCommand {
29319
30181
  );
29320
30182
  return ExitCode.Error;
29321
30183
  }
29322
- const stderr = this.context.stderr;
29323
- const isTTY = stderr.isTTY === true;
29324
- const colorEnabled = resolveColorEnabled({
29325
- isTTY,
29326
- noColorFlag: this.noColor,
29327
- env: process.env
29328
- });
29329
30184
  let referencePaths = [];
29330
30185
  try {
29331
30186
  const cfg = loadConfig({ cwd: runtimeCtx.cwd }).effective;
@@ -29397,6 +30252,12 @@ function parseDebounce(raw) {
29397
30252
  if (parsed === null) return { ok: false, value: raw };
29398
30253
  return { ok: true, value: parsed };
29399
30254
  }
30255
+ function parseMaxScan(raw) {
30256
+ if (raw === void 0) return { ok: true, value: void 0 };
30257
+ const n = Number(raw);
30258
+ if (!Number.isInteger(n) || n < 1) return { ok: false, value: raw };
30259
+ return { ok: true, value: n };
30260
+ }
29400
30261
  function parseMaxNodes(raw) {
29401
30262
  if (raw === void 0) return { ok: true, value: void 0 };
29402
30263
  const n = Number(raw);
@@ -29455,6 +30316,12 @@ function formatValidationError(err, ansi) {
29455
30316
  value: sanitizeForTerminal(err.value),
29456
30317
  hint: ansi.dim(SERVE_TEXTS.watcherDebounceInvalidHint)
29457
30318
  });
30319
+ case "max-scan-invalid":
30320
+ return tx(SERVE_TEXTS.maxScanInvalid, {
30321
+ glyph: errGlyph,
30322
+ value: sanitizeForTerminal(err.value),
30323
+ hint: ansi.dim(SERVE_TEXTS.maxScanInvalidHint)
30324
+ });
29458
30325
  case "max-nodes-invalid":
29459
30326
  return tx(SERVE_TEXTS.maxNodesInvalid, {
29460
30327
  glyph: errGlyph,
@@ -29841,7 +30708,7 @@ function rankConfidenceForGrouping(c) {
29841
30708
 
29842
30709
  // cli/commands/sidecar.ts
29843
30710
  import { unlink as unlink3 } from "fs/promises";
29844
- import { resolve as resolve38 } from "path";
30711
+ import { resolve as resolve41 } from "path";
29845
30712
  import { Command as Command36, Option as Option34 } from "clipanion";
29846
30713
 
29847
30714
  // cli/i18n/sidecar.texts.ts
@@ -29999,7 +30866,7 @@ var SidecarRefreshCommand = class extends SmCommand {
29999
30866
  let absPath;
30000
30867
  try {
30001
30868
  assertContained(ctx.cwd, node.path);
30002
- absPath = resolve38(ctx.cwd, node.path);
30869
+ absPath = resolve41(ctx.cwd, node.path);
30003
30870
  } catch (err) {
30004
30871
  this.printer.error(
30005
30872
  tx(SIDECAR_TEXTS.refreshFailed, { glyph: errGlyph, message: formatErrorMessage(err) })
@@ -30283,7 +31150,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
30283
31150
  let absPath;
30284
31151
  try {
30285
31152
  assertContained(ctx.cwd, node.path);
30286
- absPath = resolve38(ctx.cwd, node.path);
31153
+ absPath = resolve41(ctx.cwd, node.path);
30287
31154
  } catch (err) {
30288
31155
  this.printer.error(
30289
31156
  tx(SIDECAR_TEXTS.annotateFailed, { glyph: errGlyph, message: formatErrorMessage(err) })
@@ -30535,7 +31402,7 @@ var STUB_COMMANDS = [
30535
31402
 
30536
31403
  // cli/commands/tutorial.ts
30537
31404
  import { cpSync as cpSync2, existsSync as existsSync32, mkdirSync as mkdirSync6, readdirSync as readdirSync10, rmSync as rmSync2, statSync as statSync11 } from "fs";
30538
- import { dirname as dirname20, join as join21, resolve as resolve39 } from "path";
31405
+ import { dirname as dirname20, join as join21, resolve as resolve42 } from "path";
30539
31406
  import { createInterface as createInterface5 } from "readline";
30540
31407
  import { fileURLToPath as fileURLToPath7 } from "url";
30541
31408
  import { Command as Command38, Option as Option36 } from "clipanion";
@@ -30881,11 +31748,11 @@ function resolveSkillSourceDir() {
30881
31748
  const here = dirname20(fileURLToPath7(import.meta.url));
30882
31749
  const candidates = [
30883
31750
  // dev: src/cli/commands/ → repo-root .claude/skills/sm-tutorial/
30884
- resolve39(here, "../../..", SKILL_SOURCE_DIR),
31751
+ resolve42(here, "../../..", SKILL_SOURCE_DIR),
30885
31752
  // bundled: dist/cli.js → dist/cli/tutorial/sm-tutorial (sibling)
30886
- resolve39(here, "cli/tutorial", SKILL_SLUG),
31753
+ resolve42(here, "cli/tutorial", SKILL_SLUG),
30887
31754
  // bundled fallback: any-depth → cli/tutorial/sm-tutorial
30888
- resolve39(here, "../cli/tutorial", SKILL_SLUG)
31755
+ resolve42(here, "../cli/tutorial", SKILL_SLUG)
30889
31756
  ];
30890
31757
  for (const candidate of candidates) {
30891
31758
  if (existsSync32(candidate) && statSync11(candidate).isDirectory()) {
@@ -31110,4 +31977,4 @@ function resolveBareDefault() {
31110
31977
  process.exit(ExitCode.Error);
31111
31978
  }
31112
31979
  //# sourceMappingURL=cli.js.map
31113
- //# debugId=d6e5d903-0942-5718-94b3-53a96de15b21
31980
+ //# debugId=d7ccbbda-dc56-582c-966a-4822e4bdcd83