contract-driven-delivery 2.0.20 → 2.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -7499,20 +7499,50 @@ var require_dist = __commonJS({
7499
7499
  }
7500
7500
  });
7501
7501
 
7502
+ // src/utils/gitignore.ts
7503
+ import { existsSync as existsSync12, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
7504
+ import { join as join13 } from "path";
7505
+ function ensureGitignoreEntry(cwd, entry, comment = "cdd-kit generated backups (do not commit)") {
7506
+ const path = join13(cwd, ".gitignore");
7507
+ const trimmed = entry.trim();
7508
+ if (!trimmed)
7509
+ return false;
7510
+ const re = new RegExp(`^\\s*${trimmed.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
7511
+ let existing = "";
7512
+ if (existsSync12(path))
7513
+ existing = readFileSync11(path, "utf8");
7514
+ if (re.test(existing))
7515
+ return false;
7516
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
7517
+ const block = existing.length === 0 ? `# ${comment}
7518
+ ${trimmed}
7519
+ ` : `${sep}
7520
+ # ${comment}
7521
+ ${trimmed}
7522
+ `;
7523
+ writeFileSync6(path, existing + block, "utf8");
7524
+ return true;
7525
+ }
7526
+ var init_gitignore = __esm({
7527
+ "src/utils/gitignore.ts"() {
7528
+ "use strict";
7529
+ }
7530
+ });
7531
+
7502
7532
  // src/commands/migrate.ts
7503
7533
  var migrate_exports = {};
7504
7534
  __export(migrate_exports, {
7505
7535
  migrate: () => migrate
7506
7536
  });
7507
- import { join as join13 } from "path";
7508
- import { cpSync as cpSync2, existsSync as existsSync12, mkdirSync as mkdirSync6, readdirSync as readdirSync7, readFileSync as readFileSync11, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
7537
+ import { join as join14 } from "path";
7538
+ import { cpSync as cpSync2, existsSync as existsSync13, mkdirSync as mkdirSync6, readdirSync as readdirSync7, readFileSync as readFileSync12, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
7509
7539
  import yaml2 from "js-yaml";
7510
7540
  function backupChangeDir(cwd, changeId, sessionStamp) {
7511
- const backupRoot = join13(cwd, ".cdd", "migrate-backup", sessionStamp);
7512
- const backupDir2 = join13(backupRoot, changeId);
7541
+ const backupRoot = join14(cwd, ".cdd", "migrate-backup", sessionStamp);
7542
+ const backupDir2 = join14(backupRoot, changeId);
7513
7543
  mkdirSync6(backupRoot, { recursive: true });
7514
- const sourceDir = join13(cwd, "specs", "changes", changeId);
7515
- if (existsSync12(sourceDir)) {
7544
+ const sourceDir = join14(cwd, "specs", "changes", changeId);
7545
+ if (existsSync13(sourceDir)) {
7516
7546
  cpSync2(sourceDir, backupDir2, { recursive: true });
7517
7547
  }
7518
7548
  return backupDir2;
@@ -7645,16 +7675,16 @@ function parseLegacyTaskList(body) {
7645
7675
  return rows;
7646
7676
  }
7647
7677
  function migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pendingWrites, pendingDeletes) {
7648
- const newPath = join13(changeDir, "tasks.yml");
7649
- const legacyPath = join13(changeDir, "tasks.md");
7650
- if (existsSync12(newPath)) {
7678
+ const newPath = join14(changeDir, "tasks.yml");
7679
+ const legacyPath = join14(changeDir, "tasks.md");
7680
+ if (existsSync13(newPath)) {
7651
7681
  return;
7652
7682
  }
7653
- if (!existsSync12(legacyPath)) {
7683
+ if (!existsSync13(legacyPath)) {
7654
7684
  warnings.push("tasks.md not found and tasks.yml missing \u2014 skipping tasks migration");
7655
7685
  return;
7656
7686
  }
7657
- const raw = readFileSync11(legacyPath, "utf8");
7687
+ const raw = readFileSync12(legacyPath, "utf8");
7658
7688
  const fm = parseLegacyFrontmatter(raw);
7659
7689
  const bodyMatch = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);
7660
7690
  const body = bodyMatch ? bodyMatch[1] : raw;
@@ -7738,17 +7768,17 @@ function parseLegacyAgentLog(content) {
7738
7768
  return data;
7739
7769
  }
7740
7770
  function migrateAgentLogs(changeDir, changed, pendingWrites, pendingDeletes) {
7741
- const agentLogDir = join13(changeDir, "agent-log");
7742
- if (!existsSync12(agentLogDir))
7771
+ const agentLogDir = join14(changeDir, "agent-log");
7772
+ if (!existsSync13(agentLogDir))
7743
7773
  return;
7744
7774
  const mdLogs = readdirSync7(agentLogDir).filter((f) => f.endsWith(".md"));
7745
7775
  for (const f of mdLogs) {
7746
- const fullPath = join13(agentLogDir, f);
7776
+ const fullPath = join14(agentLogDir, f);
7747
7777
  const yamlName = f.replace(/\.md$/, ".yml");
7748
- const yamlFull = join13(agentLogDir, yamlName);
7749
- if (existsSync12(yamlFull))
7778
+ const yamlFull = join14(agentLogDir, yamlName);
7779
+ if (existsSync13(yamlFull))
7750
7780
  continue;
7751
- const raw = readFileSync11(fullPath, "utf8");
7781
+ const raw = readFileSync12(fullPath, "utf8");
7752
7782
  const parsed = parseLegacyAgentLog(raw);
7753
7783
  const yamlOut = yaml2.dump(parsed, { lineWidth: -1, noRefs: true });
7754
7784
  pendingWrites.push({ path: yamlFull, content: yamlOut });
@@ -7757,15 +7787,15 @@ function migrateAgentLogs(changeDir, changed, pendingWrites, pendingDeletes) {
7757
7787
  }
7758
7788
  }
7759
7789
  function ensureImplementationPlanScaffold(changeId, changeDir, changed, warnings, pendingWrites) {
7760
- const planPath = join13(changeDir, "implementation-plan.md");
7761
- if (existsSync12(planPath))
7790
+ const planPath = join14(changeDir, "implementation-plan.md");
7791
+ if (existsSync13(planPath))
7762
7792
  return;
7763
- const templatePath = join13(ASSET.specsTemplates, "implementation-plan.md");
7764
- if (!existsSync12(templatePath)) {
7793
+ const templatePath = join14(ASSET.specsTemplates, "implementation-plan.md");
7794
+ if (!existsSync13(templatePath)) {
7765
7795
  warnings.push("implementation-plan.md template not found; run cdd-kit upgrade --yes after updating cdd-kit");
7766
7796
  return;
7767
7797
  }
7768
- const template = readFileSync11(templatePath, "utf8").replace(/<change-id>/g, changeId).replace(/<id>/g, changeId);
7798
+ const template = readFileSync12(templatePath, "utf8").replace(/<change-id>/g, changeId).replace(/<id>/g, changeId);
7769
7799
  pendingWrites.push({ path: planPath, content: template });
7770
7800
  changed.push("implementation-plan.md: added scaffold");
7771
7801
  warnings.push("implementation-plan.md scaffold added; fill it before implementation agents continue");
@@ -7776,9 +7806,9 @@ function migrateOne(changeId, changeDir, enableContextGovernance) {
7776
7806
  const pending = [];
7777
7807
  const deletes = [];
7778
7808
  let detectedTier = null;
7779
- const classifPath = join13(changeDir, "change-classification.md");
7780
- if (existsSync12(classifPath)) {
7781
- const content = readFileSync11(classifPath, "utf8");
7809
+ const classifPath = join14(changeDir, "change-classification.md");
7810
+ if (existsSync13(classifPath)) {
7811
+ const content = readFileSync12(classifPath, "utf8");
7782
7812
  const hasNewTierFormat = /^## Tier\s*\n\s*-\s*\d\s*$/m.test(content);
7783
7813
  const oldMatch = content.match(/\*\*Tier[:\*]+\s*(?:Tier\s*)?(\d)/i) ?? content.match(/^-?\s*Tier:\s*(?:Tier\s*)?(\d)/mi);
7784
7814
  if (oldMatch)
@@ -7809,8 +7839,8 @@ function migrateOne(changeId, changeDir, enableContextGovernance) {
7809
7839
  migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pending, deletes);
7810
7840
  ensureImplementationPlanScaffold(changeId, changeDir, changed, warnings, pending);
7811
7841
  migrateAgentLogs(changeDir, changed, pending, deletes);
7812
- const manifestPath = join13(changeDir, "context-manifest.md");
7813
- if (!existsSync12(manifestPath)) {
7842
+ const manifestPath = join14(changeDir, "context-manifest.md");
7843
+ if (!existsSync13(manifestPath)) {
7814
7844
  changed.push(enableContextGovernance ? "context-manifest.md: added context-governance v1 manifest scaffold" : "context-manifest.md: added legacy context manifest scaffold");
7815
7845
  pending.push({
7816
7846
  path: manifestPath,
@@ -7826,7 +7856,7 @@ function commitWritesAtomically(pending, deletes) {
7826
7856
  try {
7827
7857
  for (const write of pending) {
7828
7858
  const tmp = `${write.path}.cdd-migrate.tmp`;
7829
- writeFileSync6(tmp, write.content, "utf8");
7859
+ writeFileSync7(tmp, write.content, "utf8");
7830
7860
  renames.push({ tmp, final: write.path });
7831
7861
  }
7832
7862
  } catch (err) {
@@ -7855,8 +7885,8 @@ async function migrate(changeId, opts = {}) {
7855
7885
  const noBackup = opts.noBackup ?? false;
7856
7886
  const idsToMigrate = [];
7857
7887
  if (opts.all) {
7858
- const changesDir = join13(cwd, "specs", "changes");
7859
- if (!existsSync12(changesDir)) {
7888
+ const changesDir = join14(cwd, "specs", "changes");
7889
+ if (!existsSync13(changesDir)) {
7860
7890
  log.info("No specs/changes/ directory found \u2014 nothing to migrate.");
7861
7891
  return;
7862
7892
  }
@@ -7864,8 +7894,8 @@ async function migrate(changeId, opts = {}) {
7864
7894
  ...readdirSync7(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
7865
7895
  );
7866
7896
  } else if (changeId) {
7867
- const specificDir = join13(cwd, "specs", "changes", changeId);
7868
- if (!existsSync12(specificDir)) {
7897
+ const specificDir = join14(cwd, "specs", "changes", changeId);
7898
+ if (!existsSync13(specificDir)) {
7869
7899
  log.error(`Change not found: specs/changes/${changeId}`);
7870
7900
  process.exit(1);
7871
7901
  }
@@ -7885,10 +7915,10 @@ async function migrate(changeId, opts = {}) {
7885
7915
  const sessionStamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7886
7916
  let migratedCount = 0;
7887
7917
  let upToDateCount = 0;
7888
- const backupRoot = join13(cwd, ".cdd", "migrate-backup", sessionStamp);
7918
+ const backupRoot = join14(cwd, ".cdd", "migrate-backup", sessionStamp);
7889
7919
  for (const id of idsToMigrate) {
7890
- const changeDir = join13(cwd, "specs", "changes", id);
7891
- if (!existsSync12(changeDir)) {
7920
+ const changeDir = join14(cwd, "specs", "changes", id);
7921
+ if (!existsSync13(changeDir)) {
7892
7922
  log.warn(` ${id}: directory not found \u2014 skipping`);
7893
7923
  continue;
7894
7924
  }
@@ -7936,32 +7966,12 @@ async function migrate(changeId, opts = {}) {
7936
7966
  }
7937
7967
  }
7938
7968
  }
7939
- function ensureGitignoreEntry(cwd, entry) {
7940
- const path = join13(cwd, ".gitignore");
7941
- const trimmed = entry.trim();
7942
- if (!trimmed)
7943
- return false;
7944
- const re = new RegExp(`^\\s*${trimmed.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\s*$`, "m");
7945
- let existing = "";
7946
- if (existsSync12(path))
7947
- existing = readFileSync11(path, "utf8");
7948
- if (re.test(existing))
7949
- return false;
7950
- const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
7951
- const block = existing.length === 0 ? `# cdd-kit generated backups (do not commit)
7952
- ${trimmed}
7953
- ` : `${sep}
7954
- # cdd-kit generated backups (do not commit)
7955
- ${trimmed}
7956
- `;
7957
- writeFileSync6(path, existing + block, "utf8");
7958
- return true;
7959
- }
7960
7969
  var init_migrate = __esm({
7961
7970
  "src/commands/migrate.ts"() {
7962
7971
  "use strict";
7963
7972
  init_logger();
7964
7973
  init_paths();
7974
+ init_gitignore();
7965
7975
  }
7966
7976
  });
7967
7977
 
@@ -7970,34 +7980,34 @@ var upgrade_exports = {};
7970
7980
  __export(upgrade_exports, {
7971
7981
  upgrade: () => upgrade
7972
7982
  });
7973
- import { existsSync as existsSync13, mkdirSync as mkdirSync7, readdirSync as readdirSync8, copyFileSync as copyFileSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
7974
- import { dirname as dirname4, join as join14, relative as relative4 } from "path";
7983
+ import { existsSync as existsSync14, mkdirSync as mkdirSync7, readdirSync as readdirSync8, copyFileSync as copyFileSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
7984
+ import { dirname as dirname4, join as join15, relative as relative4 } from "path";
7975
7985
  function planMissingFiles(srcDir, destDir, label, planned) {
7976
- if (!existsSync13(srcDir))
7986
+ if (!existsSync14(srcDir))
7977
7987
  return;
7978
7988
  for (const entry of readdirSync8(srcDir, { withFileTypes: true })) {
7979
- const src = join14(srcDir, entry.name);
7980
- const dest = join14(destDir, entry.name);
7989
+ const src = join15(srcDir, entry.name);
7990
+ const dest = join15(destDir, entry.name);
7981
7991
  if (entry.isDirectory()) {
7982
- planMissingFiles(src, dest, join14(label, entry.name), planned);
7992
+ planMissingFiles(src, dest, join15(label, entry.name), planned);
7983
7993
  continue;
7984
7994
  }
7985
- if (!existsSync13(dest)) {
7986
- planned.push({ src, dest, rel: join14(label, relative4(srcDir, src)) });
7995
+ if (!existsSync14(dest)) {
7996
+ planned.push({ src, dest, rel: join15(label, relative4(srcDir, src)) });
7987
7997
  }
7988
7998
  }
7989
7999
  }
7990
8000
  function planProviderGuidance(cwd, provider, planned) {
7991
8001
  if (provider === "claude" || provider === "both") {
7992
- if (!existsSync13(join14(cwd, "CLAUDE.md"))) {
7993
- planned.push({ src: ASSET.claudeTemplate, dest: join14(cwd, "CLAUDE.md"), rel: "CLAUDE.md" });
8002
+ if (!existsSync14(join15(cwd, "CLAUDE.md"))) {
8003
+ planned.push({ src: ASSET.claudeTemplate, dest: join15(cwd, "CLAUDE.md"), rel: "CLAUDE.md" });
7994
8004
  }
7995
- if (!existsSync13(join14(cwd, "AGENTS.md"))) {
7996
- planned.push({ src: ASSET.agentsTemplate, dest: join14(cwd, "AGENTS.md"), rel: "AGENTS.md" });
8005
+ if (!existsSync14(join15(cwd, "AGENTS.md"))) {
8006
+ planned.push({ src: ASSET.agentsTemplate, dest: join15(cwd, "AGENTS.md"), rel: "AGENTS.md" });
7997
8007
  }
7998
8008
  }
7999
- if ((provider === "codex" || provider === "both") && !existsSync13(join14(cwd, "CODEX.md"))) {
8000
- planned.push({ src: ASSET.codexTemplate, dest: join14(cwd, "CODEX.md"), rel: "CODEX.md" });
8009
+ if ((provider === "codex" || provider === "both") && !existsSync14(join15(cwd, "CODEX.md"))) {
8010
+ planned.push({ src: ASSET.codexTemplate, dest: join15(cwd, "CODEX.md"), rel: "CODEX.md" });
8001
8011
  }
8002
8012
  }
8003
8013
  function applyCopy(plan) {
@@ -8015,12 +8025,12 @@ async function upgrade(opts = {}) {
8015
8025
  }
8016
8026
  const provider = inferProvider(cwd, requestedProvider);
8017
8027
  const plan = [];
8018
- planMissingFiles(ASSET.contracts, join14(cwd, "contracts"), "contracts", plan);
8019
- planMissingFiles(ASSET.specsTemplates, join14(cwd, "specs", "templates"), "specs/templates", plan);
8020
- planMissingFiles(ASSET.testsTemplates, join14(cwd, "tests", "templates"), "tests/templates", plan);
8021
- planMissingFiles(ASSET.ci, join14(cwd, "ci"), "ci", plan);
8022
- planMissingFiles(ASSET.githubWorkflows, join14(cwd, ".github", "workflows"), ".github/workflows", plan);
8023
- planMissingFiles(ASSET.cddConfig, join14(cwd, ".cdd"), ".cdd", plan);
8028
+ planMissingFiles(ASSET.contracts, join15(cwd, "contracts"), "contracts", plan);
8029
+ planMissingFiles(ASSET.specsTemplates, join15(cwd, "specs", "templates"), "specs/templates", plan);
8030
+ planMissingFiles(ASSET.testsTemplates, join15(cwd, "tests", "templates"), "tests/templates", plan);
8031
+ planMissingFiles(ASSET.ci, join15(cwd, "ci"), "ci", plan);
8032
+ planMissingFiles(ASSET.githubWorkflows, join15(cwd, ".github", "workflows"), ".github/workflows", plan);
8033
+ planMissingFiles(ASSET.cddConfig, join15(cwd, ".cdd"), ".cdd", plan);
8024
8034
  planProviderGuidance(cwd, provider, plan);
8025
8035
  log.blank();
8026
8036
  log.info(`Upgrade provider: ${provider}`);
@@ -8057,11 +8067,11 @@ async function upgrade(opts = {}) {
8057
8067
  return;
8058
8068
  }
8059
8069
  applyCopy(plan);
8060
- const modelPolicyPath = join14(cwd, ".cdd", "model-policy.json");
8061
- if (existsSync13(modelPolicyPath)) {
8070
+ const modelPolicyPath = join15(cwd, ".cdd", "model-policy.json");
8071
+ if (existsSync14(modelPolicyPath)) {
8062
8072
  let existing = {};
8063
8073
  try {
8064
- existing = JSON.parse(readFileSync12(modelPolicyPath, "utf8"));
8074
+ existing = JSON.parse(readFileSync13(modelPolicyPath, "utf8"));
8065
8075
  } catch {
8066
8076
  }
8067
8077
  const merged = {
@@ -8070,7 +8080,7 @@ async function upgrade(opts = {}) {
8070
8080
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
8071
8081
  roles: existing.roles && typeof existing.roles === "object" ? existing.roles : {}
8072
8082
  };
8073
- writeFileSync7(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
8083
+ writeFileSync8(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
8074
8084
  }
8075
8085
  log.blank();
8076
8086
  log.ok(`Upgrade complete: ${plan.length} missing file(s) added.`);
@@ -8230,8 +8240,8 @@ var init_yaml_writer = __esm({
8230
8240
  });
8231
8241
 
8232
8242
  // src/code-map/config.ts
8233
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
8234
- import { join as join15 } from "path";
8243
+ import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
8244
+ import { join as join16 } from "path";
8235
8245
  import { load as yamlLoad } from "js-yaml";
8236
8246
  function asStringArray(value, key, where) {
8237
8247
  if (value === void 0)
@@ -8247,8 +8257,8 @@ function asStringArray(value, key, where) {
8247
8257
  return value;
8248
8258
  }
8249
8259
  function loadCodeMapConfig(cwd) {
8250
- const filePath = join15(cwd, CONFIG_REL_PATH);
8251
- if (!existsSync14(filePath)) {
8260
+ const filePath = join16(cwd, CONFIG_REL_PATH);
8261
+ if (!existsSync15(filePath)) {
8252
8262
  return {
8253
8263
  include: [...BUILTIN_INCLUDE],
8254
8264
  exclude: [...BUILTIN_EXCLUDE],
@@ -8257,7 +8267,7 @@ function loadCodeMapConfig(cwd) {
8257
8267
  }
8258
8268
  let text;
8259
8269
  try {
8260
- text = readFileSync13(filePath, "utf8");
8270
+ text = readFileSync14(filePath, "utf8");
8261
8271
  } catch (err) {
8262
8272
  throw new Error(`failed to read ${CONFIG_REL_PATH}: ${err.message}`);
8263
8273
  }
@@ -8328,7 +8338,7 @@ var init_config = __esm({
8328
8338
 
8329
8339
  // src/code-map/include-exclude.ts
8330
8340
  import { readdirSync as readdirSync9, statSync as statSync2 } from "fs";
8331
- import { join as join16 } from "path";
8341
+ import { join as join17 } from "path";
8332
8342
  import picomatch from "picomatch";
8333
8343
  function walkRepo(root, opts = {}) {
8334
8344
  const includes = opts.include ?? [];
@@ -8347,7 +8357,7 @@ function walkRepo(root, opts = {}) {
8347
8357
  }
8348
8358
  for (const entry of entries) {
8349
8359
  const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
8350
- const absPath = join16(dir, entry.name);
8360
+ const absPath = join17(dir, entry.name);
8351
8361
  if (entry.isDirectory() || entry.isSymbolicLink()) {
8352
8362
  const dirPattern = `${relPath}/**`;
8353
8363
  if (isExcluded(relPath) || isExcluded(dirPattern))
@@ -8418,6 +8428,260 @@ var init_orchestrator = __esm({
8418
8428
  }
8419
8429
  });
8420
8430
 
8431
+ // src/code-map/freshness.ts
8432
+ import { existsSync as existsSync16, readFileSync as readFileSync15, statSync as statSync3 } from "fs";
8433
+ import { join as join18 } from "path";
8434
+ function checkCodeMapFreshness(cwd, mapRel = ".cdd/code-map.yml", include, exclude) {
8435
+ const mapPath = join18(cwd, mapRel);
8436
+ let cfg;
8437
+ try {
8438
+ cfg = loadCodeMapConfig(cwd);
8439
+ } catch (err) {
8440
+ return {
8441
+ status: "config-error",
8442
+ staleFiles: [],
8443
+ staleCount: 0,
8444
+ mapPath,
8445
+ configError: err.message
8446
+ };
8447
+ }
8448
+ const includeFinal = [...cfg.include, ...include ?? []];
8449
+ const excludeFinal = [...cfg.exclude, ...exclude ?? []];
8450
+ const sourceFiles = walkRepo(cwd, { include: includeFinal, exclude: excludeFinal });
8451
+ if (!existsSync16(mapPath)) {
8452
+ if (sourceFiles.length === 0) {
8453
+ return { status: "missing-greenfield", staleFiles: [], staleCount: 0, mapPath };
8454
+ }
8455
+ return { status: "missing-with-sources", staleFiles: [], staleCount: 0, mapPath };
8456
+ }
8457
+ const mapMtime = statSync3(mapPath).mtimeMs;
8458
+ const staleAll = [];
8459
+ for (const absPath of sourceFiles) {
8460
+ try {
8461
+ const mtime = statSync3(absPath).mtimeMs;
8462
+ if (mtime > mapMtime) {
8463
+ const rel = absPath.replace(/\\/g, "/").replace(cwd.replace(/\\/g, "/") + "/", "");
8464
+ staleAll.push(rel);
8465
+ }
8466
+ } catch {
8467
+ }
8468
+ }
8469
+ if (staleAll.length === 0) {
8470
+ return { status: "ok", staleFiles: [], staleCount: 0, mapPath };
8471
+ }
8472
+ const declaredDigest = readSourcesDigest(mapPath);
8473
+ if (declaredDigest !== null) {
8474
+ const actualDigest = computeSourcesDigest(sourceFiles, cwd);
8475
+ if (actualDigest === declaredDigest) {
8476
+ return { status: "ok", staleFiles: [], staleCount: 0, mapPath };
8477
+ }
8478
+ }
8479
+ return {
8480
+ status: "stale",
8481
+ staleFiles: staleAll.slice(0, 5),
8482
+ staleCount: staleAll.length,
8483
+ mapPath
8484
+ };
8485
+ }
8486
+ function readSourcesDigest(mapPath) {
8487
+ try {
8488
+ const head = readFileSync15(mapPath, "utf8").slice(0, 2048);
8489
+ const m = head.match(/^# sources-digest:\s*([a-f0-9]+)/m);
8490
+ return m ? m[1] : null;
8491
+ } catch {
8492
+ return null;
8493
+ }
8494
+ }
8495
+ var init_freshness = __esm({
8496
+ "src/code-map/freshness.ts"() {
8497
+ "use strict";
8498
+ init_include_exclude();
8499
+ init_config();
8500
+ init_code_map();
8501
+ }
8502
+ });
8503
+
8504
+ // src/code-map/index-reader.ts
8505
+ import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
8506
+ import yaml3 from "js-yaml";
8507
+ async function ensureCodeMapFresh(mapPath, refresh2) {
8508
+ if (!refresh2)
8509
+ return { refreshed: false };
8510
+ const freshness = checkCodeMapFreshness(process.cwd(), mapPath);
8511
+ if (freshness.status === "config-error") {
8512
+ return {
8513
+ refreshed: false,
8514
+ error: `.cdd/code-map-config.yml is invalid: ${freshness.configError}`
8515
+ };
8516
+ }
8517
+ if (freshness.status === "missing-with-sources" || freshness.status === "missing-greenfield" || freshness.status === "stale") {
8518
+ const { codeMap: codeMap2 } = await Promise.resolve().then(() => (init_code_map(), code_map_exports));
8519
+ const exit = await codeMap2({
8520
+ path: ".",
8521
+ out: mapPath,
8522
+ include: [],
8523
+ exclude: [],
8524
+ check: false,
8525
+ maxLines: 1e5,
8526
+ silent: true
8527
+ });
8528
+ if (exit !== 0) {
8529
+ return {
8530
+ refreshed: false,
8531
+ error: `could not refresh ${mapPath}; run \`cdd-kit code-map\` for details.`
8532
+ };
8533
+ }
8534
+ return { refreshed: true };
8535
+ }
8536
+ return { refreshed: false };
8537
+ }
8538
+ function sidecarPathFor(mapPath) {
8539
+ return `${mapPath.replace(/\.ya?ml$/i, "")}.index.json`;
8540
+ }
8541
+ function tryLoadSidecar(mapPath, mapText) {
8542
+ const sidecarPath = sidecarPathFor(mapPath);
8543
+ if (!existsSync17(sidecarPath))
8544
+ return null;
8545
+ const headerDigest = mapText.match(/^# sources-digest:\s*([a-f0-9]+)/m)?.[1];
8546
+ if (!headerDigest)
8547
+ return null;
8548
+ try {
8549
+ const raw = JSON.parse(readFileSync16(sidecarPath, "utf8"));
8550
+ if (raw && raw.sourcesDigest === headerDigest && Array.isArray(raw.entries)) {
8551
+ return raw.entries;
8552
+ }
8553
+ } catch {
8554
+ }
8555
+ return null;
8556
+ }
8557
+ function loadCodeMapEntries(mapPath) {
8558
+ if (!existsSync17(mapPath)) {
8559
+ throw new Error(`${mapPath} is missing; run \`cdd-kit code-map\` first.`);
8560
+ }
8561
+ const text = readFileSync16(mapPath, "utf8");
8562
+ const fromSidecar = tryLoadSidecar(mapPath, text);
8563
+ if (fromSidecar)
8564
+ return fromSidecar;
8565
+ const totalLinesByPath = extractTotalLines(text);
8566
+ const raw = yaml3.load(text, { schema: yaml3.JSON_SCHEMA });
8567
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
8568
+ return [];
8569
+ const entries = [];
8570
+ for (const [path, value] of Object.entries(raw)) {
8571
+ if (!value || typeof value !== "object" || Array.isArray(value))
8572
+ continue;
8573
+ const obj = value;
8574
+ entries.push({
8575
+ path,
8576
+ total_lines: totalLinesByPath.get(path) ?? (typeof obj.total_lines === "number" ? obj.total_lines : 0),
8577
+ imports: Array.isArray(obj.imports) ? obj.imports : [],
8578
+ constants: Array.isArray(obj.constants) ? obj.constants : [],
8579
+ classes: Array.isArray(obj.classes) ? obj.classes : [],
8580
+ functions: Array.isArray(obj.functions) ? obj.functions : [],
8581
+ interfaces: Array.isArray(obj.interfaces) ? obj.interfaces : [],
8582
+ types: Array.isArray(obj.types) ? obj.types : [],
8583
+ enums: Array.isArray(obj.enums) ? obj.enums : []
8584
+ });
8585
+ }
8586
+ return entries;
8587
+ }
8588
+ function extractTotalLines(text) {
8589
+ const totals = /* @__PURE__ */ new Map();
8590
+ for (const line of text.split(/\r?\n/)) {
8591
+ const m = line.match(/^((?:'[^']*(?:''[^']*)*')|[^#:\s][^#]*?):\s*#\s*(\d+)\s+lines\b/);
8592
+ if (!m)
8593
+ continue;
8594
+ totals.set(unquoteYamlKey(m[1].trim()), Number(m[2]));
8595
+ }
8596
+ return totals;
8597
+ }
8598
+ function unquoteYamlKey(key) {
8599
+ if (key.startsWith("'") && key.endsWith("'")) {
8600
+ return key.slice(1, -1).replace(/''/g, "'");
8601
+ }
8602
+ return key;
8603
+ }
8604
+ var init_index_reader = __esm({
8605
+ "src/code-map/index-reader.ts"() {
8606
+ "use strict";
8607
+ init_freshness();
8608
+ }
8609
+ });
8610
+
8611
+ // src/code-map/worker-dispatch.ts
8612
+ var worker_dispatch_exports = {};
8613
+ __export(worker_dispatch_exports, {
8614
+ scanLangWithWorkers: () => scanLangWithWorkers
8615
+ });
8616
+ import { execFile } from "child_process";
8617
+ import { writeFileSync as writeFileSync9, unlinkSync } from "fs";
8618
+ import { randomBytes } from "crypto";
8619
+ import { join as join19 } from "path";
8620
+ import { tmpdir } from "os";
8621
+ function chunk(arr, parts) {
8622
+ const size = Math.ceil(arr.length / parts);
8623
+ const out = [];
8624
+ for (let i = 0; i < arr.length; i += size)
8625
+ out.push(arr.slice(i, i + size));
8626
+ return out;
8627
+ }
8628
+ function scanChunkInChild(cliEntry, lang, files, repoRoot) {
8629
+ return new Promise((resolve2, reject) => {
8630
+ if (!ALLOWED_LANGS.has(lang)) {
8631
+ return reject(new Error(`refusing to spawn worker for unknown lang: ${lang}`));
8632
+ }
8633
+ const listFile = join19(
8634
+ tmpdir(),
8635
+ `cdd-cm-worker-${process.pid}-${randomBytes(12).toString("hex")}.txt`
8636
+ );
8637
+ writeFileSync9(listFile, files.join("\n") + "\n", { encoding: "utf8", mode: 384 });
8638
+ execFile(
8639
+ process.execPath,
8640
+ [cliEntry, "__code-map-scan", "--lang", lang, "--batch-file", listFile, "--repo-root", repoRoot],
8641
+ { encoding: "utf8", timeout: CHILD_TIMEOUT_MS, maxBuffer: 100 * 1024 * 1024, shell: false, windowsHide: true },
8642
+ (err, stdout) => {
8643
+ try {
8644
+ unlinkSync(listFile);
8645
+ } catch {
8646
+ }
8647
+ if (err)
8648
+ return reject(err);
8649
+ try {
8650
+ const parsed = JSON.parse(stdout);
8651
+ resolve2({ entries: parsed.entries ?? [], warnings: parsed.warnings ?? [] });
8652
+ } catch (e) {
8653
+ reject(e);
8654
+ }
8655
+ }
8656
+ );
8657
+ });
8658
+ }
8659
+ async function scanLangWithWorkers(scanner, lang, files, repoRoot, workers, cliEntry) {
8660
+ if (files.length === 0)
8661
+ return { entries: [], warnings: [] };
8662
+ const chunks = chunk(files, Math.max(1, Math.min(workers, files.length)));
8663
+ try {
8664
+ const results = await Promise.all(
8665
+ chunks.map((c) => scanChunkInChild(cliEntry, lang, c, repoRoot))
8666
+ );
8667
+ return {
8668
+ entries: results.flatMap((r) => r.entries),
8669
+ warnings: results.flatMap((r) => r.warnings)
8670
+ };
8671
+ } catch {
8672
+ return scanInProcess(scanner, files, repoRoot);
8673
+ }
8674
+ }
8675
+ var CHILD_TIMEOUT_MS, ALLOWED_LANGS;
8676
+ var init_worker_dispatch = __esm({
8677
+ "src/code-map/worker-dispatch.ts"() {
8678
+ "use strict";
8679
+ init_orchestrator();
8680
+ CHILD_TIMEOUT_MS = parseInt(process.env["CDD_CODE_MAP_WORKER_TIMEOUT_MS"] ?? "120000", 10);
8681
+ ALLOWED_LANGS = /* @__PURE__ */ new Set(["js", "ts", "vue"]);
8682
+ }
8683
+ });
8684
+
8421
8685
  // src/code-map/scanners/common.ts
8422
8686
  import { relative as relative5 } from "path";
8423
8687
  function canonicalRelPath(absolutePath, repoRoot) {
@@ -8443,9 +8707,10 @@ __export(python_exports, {
8443
8707
  pythonScanner: () => pythonScanner
8444
8708
  });
8445
8709
  import { spawnSync as spawnSync2 } from "child_process";
8446
- import { writeFileSync as writeFileSync8, unlinkSync } from "fs";
8447
- import { join as join17 } from "path";
8448
- import { tmpdir } from "os";
8710
+ import { writeFileSync as writeFileSync10, unlinkSync as unlinkSync2 } from "fs";
8711
+ import { randomBytes as randomBytes2 } from "crypto";
8712
+ import { join as join20 } from "path";
8713
+ import { tmpdir as tmpdir2 } from "os";
8449
8714
  function detectPython2() {
8450
8715
  for (const candidate of ["python3", "python"]) {
8451
8716
  try {
@@ -8460,13 +8725,17 @@ function detectPython2() {
8460
8725
  }
8461
8726
  return null;
8462
8727
  }
8463
- var TIMEOUT_MS, PythonScanner, pythonScanner;
8728
+ var TIMEOUT_MS, BATCH_SIZE, PythonScanner, pythonScanner;
8464
8729
  var init_python = __esm({
8465
8730
  "src/code-map/scanners/python.ts"() {
8466
8731
  "use strict";
8467
8732
  init_paths();
8468
8733
  init_common();
8469
8734
  TIMEOUT_MS = parseInt(process.env["CDD_CODE_MAP_TIMEOUT_MS"] ?? "30000", 10);
8735
+ BATCH_SIZE = Math.max(
8736
+ 1,
8737
+ parseInt(process.env["CDD_CODE_MAP_BATCH_SIZE"] ?? "400", 10) || 400
8738
+ );
8470
8739
  PythonScanner = class {
8471
8740
  extensions = [".py"];
8472
8741
  _interpreter = void 0;
@@ -8492,10 +8761,30 @@ var init_python = __esm({
8492
8761
  });
8493
8762
  return { entries, warnings };
8494
8763
  }
8764
+ for (let i = 0; i < absolutePaths.length; i += BATCH_SIZE) {
8765
+ const chunk2 = absolutePaths.slice(i, i + BATCH_SIZE);
8766
+ const result = this.scanChunk(interpreter, chunk2, repoRoot);
8767
+ entries.push(...result.entries);
8768
+ warnings.push(...result.warnings);
8769
+ if (result.abortRemaining) {
8770
+ const remaining = absolutePaths.length - (i + chunk2.length);
8771
+ if (remaining > 0) {
8772
+ warnings.push({
8773
+ path: "",
8774
+ message: `skipping ${remaining} more .py file(s) after fatal interpreter error`
8775
+ });
8776
+ }
8777
+ break;
8778
+ }
8779
+ }
8780
+ return { entries, warnings };
8781
+ }
8782
+ scanChunk(interpreter, absolutePaths, repoRoot) {
8783
+ const entries = [];
8784
+ const warnings = [];
8495
8785
  const scriptPath = ASSET.codeMapPython;
8496
- const rand = Math.random().toString(36).slice(2);
8497
- const listFile = join17(tmpdir(), `cdd-codemap-${process.pid}-${rand}.txt`);
8498
- writeFileSync8(listFile, absolutePaths.join("\n") + "\n", "utf8");
8786
+ const listFile = join20(tmpdir2(), `cdd-codemap-${process.pid}-${randomBytes2(12).toString("hex")}.txt`);
8787
+ writeFileSync10(listFile, absolutePaths.join("\n") + "\n", { encoding: "utf8", mode: 384 });
8499
8788
  let stdout = "";
8500
8789
  let stderr = "";
8501
8790
  let exitCode = 0;
@@ -8520,24 +8809,24 @@ var init_python = __esm({
8520
8809
  path: "",
8521
8810
  message: `python interpreter not found (ENOENT); skipping ${absolutePaths.length} .py file(s)`
8522
8811
  });
8523
- return { entries, warnings };
8812
+ return { entries, warnings, abortRemaining: true };
8524
8813
  }
8525
8814
  if (errMsg.includes("ETIMEDOUT") || errMsg.includes("timeout")) {
8526
8815
  warnings.push({
8527
8816
  path: "",
8528
- message: `python scanner timed out after ${TIMEOUT_MS}ms; skipping .py files`
8817
+ message: `python scanner timed out after ${TIMEOUT_MS}ms on a batch of ${absolutePaths.length} file(s); skipping this batch (raise CDD_CODE_MAP_TIMEOUT_MS or lower CDD_CODE_MAP_BATCH_SIZE)`
8529
8818
  });
8530
8819
  return { entries, warnings };
8531
8820
  }
8532
8821
  warnings.push({
8533
8822
  path: "",
8534
- message: `python scanner error: ${errMsg}; skipping .py files`
8823
+ message: `python scanner error: ${errMsg}; skipping this batch of ${absolutePaths.length} .py file(s)`
8535
8824
  });
8536
8825
  return { entries, warnings };
8537
8826
  }
8538
8827
  } finally {
8539
8828
  try {
8540
- unlinkSync(listFile);
8829
+ unlinkSync2(listFile);
8541
8830
  } catch {
8542
8831
  }
8543
8832
  }
@@ -8546,7 +8835,7 @@ var init_python = __esm({
8546
8835
  path: "",
8547
8836
  message: "python interpreter is < 3.9 (need ast.unparse); skipping .py files"
8548
8837
  });
8549
- return { entries, warnings };
8838
+ return { entries, warnings, abortRemaining: true };
8550
8839
  }
8551
8840
  for (const line of stdout.split("\n")) {
8552
8841
  const trimmed = line.trim();
@@ -8564,7 +8853,7 @@ var init_python = __esm({
8564
8853
  }
8565
8854
  const r = parsed;
8566
8855
  entries.push({
8567
- path: canonicalRelPath(join17(repoRoot, r.path), repoRoot),
8856
+ path: canonicalRelPath(join20(repoRoot, r.path), repoRoot),
8568
8857
  total_lines: r.total_lines,
8569
8858
  imports: r.imports ?? [],
8570
8859
  constants: r.constants ?? [],
@@ -8608,7 +8897,7 @@ __export(javascript_exports, {
8608
8897
  parseJsSource: () => parseJsSource,
8609
8898
  parseSourceWithPlugins: () => parseSourceWithPlugins
8610
8899
  });
8611
- import { readFileSync as readFileSync15 } from "fs";
8900
+ import { readFileSync as readFileSync18 } from "fs";
8612
8901
  import { parse } from "@babel/parser";
8613
8902
  function parseSourceWithPlugins(source, plugins) {
8614
8903
  return parse(source, {
@@ -8689,7 +8978,7 @@ function processClassDeclaration(node, nameOverride) {
8689
8978
  methods.push({
8690
8979
  name: methodName,
8691
8980
  lines: getLineRange(m),
8692
- async: m.async
8981
+ async: m.async ?? false
8693
8982
  });
8694
8983
  }
8695
8984
  }
@@ -8910,7 +9199,7 @@ var init_javascript = __esm({
8910
9199
  async scan(absolutePath, repoRoot) {
8911
9200
  let source;
8912
9201
  try {
8913
- source = readFileSync15(absolutePath, "utf8");
9202
+ source = readFileSync18(absolutePath, "utf8");
8914
9203
  } catch (err) {
8915
9204
  throw err;
8916
9205
  }
@@ -8930,7 +9219,7 @@ var typescript_exports = {};
8930
9219
  __export(typescript_exports, {
8931
9220
  tsScanner: () => tsScanner
8932
9221
  });
8933
- import { readFileSync as readFileSync16 } from "fs";
9222
+ import { readFileSync as readFileSync19 } from "fs";
8934
9223
  var TypeScriptScanner, tsScanner;
8935
9224
  var init_typescript = __esm({
8936
9225
  "src/code-map/scanners/typescript.ts"() {
@@ -8942,7 +9231,7 @@ var init_typescript = __esm({
8942
9231
  async scan(absolutePath, repoRoot) {
8943
9232
  let source;
8944
9233
  try {
8945
- source = readFileSync16(absolutePath, "utf8");
9234
+ source = readFileSync19(absolutePath, "utf8");
8946
9235
  } catch (err) {
8947
9236
  throw err;
8948
9237
  }
@@ -8978,7 +9267,7 @@ var vue_exports = {};
8978
9267
  __export(vue_exports, {
8979
9268
  vueScanner: () => vueScanner
8980
9269
  });
8981
- import { readFileSync as readFileSync17 } from "fs";
9270
+ import { readFileSync as readFileSync20 } from "fs";
8982
9271
  import { parse as parse2 } from "@vue/compiler-sfc";
8983
9272
  var VueScanner, vueScanner;
8984
9273
  var init_vue = __esm({
@@ -8991,7 +9280,7 @@ var init_vue = __esm({
8991
9280
  async scan(absolutePath, repoRoot) {
8992
9281
  let source;
8993
9282
  try {
8994
- source = readFileSync17(absolutePath, "utf8");
9283
+ source = readFileSync20(absolutePath, "utf8");
8995
9284
  } catch (err) {
8996
9285
  throw err;
8997
9286
  }
@@ -9078,12 +9367,12 @@ __export(code_map_exports, {
9078
9367
  codeMap: () => codeMap,
9079
9368
  computeSourcesDigest: () => computeSourcesDigest
9080
9369
  });
9081
- import { existsSync as existsSync15, mkdirSync as mkdirSync8, readFileSync as readFileSync18, writeFileSync as writeFileSync9 } from "fs";
9370
+ import { existsSync as existsSync18, mkdirSync as mkdirSync8, readFileSync as readFileSync21, writeFileSync as writeFileSync11 } from "fs";
9082
9371
  import { resolve, dirname as dirname5, relative as relative6 } from "path";
9083
9372
  import { createHash as createHash5 } from "crypto";
9084
9373
  import { createRequire } from "module";
9085
9374
  import { fileURLToPath as fileURLToPath2 } from "url";
9086
- import { join as join18 } from "path";
9375
+ import { join as join21 } from "path";
9087
9376
  function computeSourcesDigest(absolutePaths, cwd) {
9088
9377
  const lines = absolutePaths.slice().sort().map((p) => {
9089
9378
  const rel = relative6(cwd, p).replace(/\\/g, "/");
@@ -9092,9 +9381,21 @@ function computeSourcesDigest(absolutePaths, cwd) {
9092
9381
  });
9093
9382
  return createHash5("sha256").update(lines.join("\n")).digest("hex");
9094
9383
  }
9384
+ function slugifySurface(surface) {
9385
+ return surface.replace(/^[./]+/, "").replace(/\/+$/, "").replace(/[\\/]+/g, "-") || "root";
9386
+ }
9095
9387
  async function codeMap(opts) {
9096
- const root = resolve(process.cwd(), opts.path);
9097
9388
  const start = Date.now();
9389
+ if (opts.surface) {
9390
+ const resolvedSurface = resolve(process.cwd(), opts.surface);
9391
+ if (!existsSync18(resolvedSurface)) {
9392
+ log.error(`code-map --surface path not found: ${opts.surface}`);
9393
+ return 1;
9394
+ }
9395
+ }
9396
+ const scanPath = opts.surface ?? opts.path;
9397
+ const root = resolve(process.cwd(), scanPath);
9398
+ const out = opts.out ?? (opts.surface ? `.cdd/code-map.${slugifySurface(opts.surface)}.yml` : ".cdd/code-map.yml");
9098
9399
  let cfg;
9099
9400
  try {
9100
9401
  cfg = loadCodeMapConfig(root);
@@ -9107,6 +9408,15 @@ async function codeMap(opts) {
9107
9408
  const files = walkRepo(root, { include, exclude });
9108
9409
  const buckets = bucketByExtension(files);
9109
9410
  const result = { entries: [], warnings: [] };
9411
+ const cliEntry = process.argv[1];
9412
+ const workers = opts.workers ?? 0;
9413
+ const useWorkers = workers > 1 && typeof cliEntry === "string" && cliEntry.length > 0;
9414
+ const dispatch = async (scanner, lang, langFiles) => {
9415
+ if (!useWorkers)
9416
+ return scanInProcess(scanner, langFiles, root);
9417
+ const { scanLangWithWorkers: scanLangWithWorkers2 } = await Promise.resolve().then(() => (init_worker_dispatch(), worker_dispatch_exports));
9418
+ return scanLangWithWorkers2(scanner, lang, langFiles, root, workers, cliEntry);
9419
+ };
9110
9420
  const tasks = [];
9111
9421
  if (buckets[".py"]?.length) {
9112
9422
  const { pythonScanner: pythonScanner2 } = await Promise.resolve().then(() => (init_python(), python_exports));
@@ -9122,7 +9432,7 @@ async function codeMap(opts) {
9122
9432
  ];
9123
9433
  if (jsFiles.length) {
9124
9434
  const { jsScanner: jsScanner2 } = await Promise.resolve().then(() => (init_javascript(), javascript_exports));
9125
- tasks.push(scanInProcess(jsScanner2, jsFiles, root));
9435
+ tasks.push(dispatch(jsScanner2, "js", jsFiles));
9126
9436
  }
9127
9437
  const tsFiles = [
9128
9438
  ...buckets[".ts"] ?? [],
@@ -9130,11 +9440,11 @@ async function codeMap(opts) {
9130
9440
  ];
9131
9441
  if (tsFiles.length) {
9132
9442
  const { tsScanner: tsScanner2 } = await Promise.resolve().then(() => (init_typescript(), typescript_exports));
9133
- tasks.push(scanInProcess(tsScanner2, tsFiles, root));
9443
+ tasks.push(dispatch(tsScanner2, "ts", tsFiles));
9134
9444
  }
9135
9445
  if (buckets[".vue"]?.length) {
9136
9446
  const { vueScanner: vueScanner2 } = await Promise.resolve().then(() => (init_vue(), vue_exports));
9137
- tasks.push(scanInProcess(vueScanner2, buckets[".vue"], root));
9447
+ tasks.push(dispatch(vueScanner2, "vue", buckets[".vue"]));
9138
9448
  }
9139
9449
  for (const r of await Promise.all(tasks)) {
9140
9450
  result.entries.push(...r.entries);
@@ -9157,25 +9467,34 @@ async function codeMap(opts) {
9157
9467
  const totalSrc = result.entries.reduce((s, e) => s + e.total_lines, 0);
9158
9468
  const mapLines = yamlBody.split("\n").length;
9159
9469
  const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
9160
- const summaryLine = `scanned ${result.entries.length} files, ${totalSrc} src lines -> ${opts.out} (${mapLines} lines, compression ${compression.toFixed(1)}x)`;
9470
+ const summaryLine = `scanned ${result.entries.length} files, ${totalSrc} src lines -> ${out} (${mapLines} lines, compression ${compression.toFixed(1)}x)`;
9161
9471
  for (const w of result.warnings) {
9162
9472
  if (!opts.silent)
9163
9473
  log.warn(`${w.path}: ${w.message}`);
9164
9474
  }
9165
9475
  if (opts.check) {
9166
- const existing = existsSync15(opts.out) ? readFileSync18(opts.out, "utf8") : "";
9476
+ const existing = existsSync18(out) ? readFileSync21(out, "utf8") : "";
9167
9477
  const normalize = (s) => s.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/^# generated: [^\n]+\n/m, "# generated: <normalized>\n");
9168
9478
  if (normalize(existing) !== normalize(yamlBody)) {
9169
9479
  if (!opts.silent)
9170
- log.error(`code-map out of date: ${opts.out} would change. Run \`cdd-kit code-map\` to regenerate.`);
9480
+ log.error(`code-map out of date: ${out} would change. Run \`cdd-kit code-map\` to regenerate.`);
9171
9481
  return 1;
9172
9482
  }
9173
9483
  if (!opts.silent)
9174
- log.ok(`code-map up to date: ${opts.out}`);
9484
+ log.ok(`code-map up to date: ${out}`);
9175
9485
  return 0;
9176
9486
  }
9177
- mkdirSync8(dirname5(opts.out), { recursive: true });
9178
- writeFileSync9(opts.out, yamlBody, "utf8");
9487
+ mkdirSync8(dirname5(out), { recursive: true });
9488
+ writeFileSync11(out, yamlBody, "utf8");
9489
+ try {
9490
+ const sidecarPath = sidecarPathFor(out);
9491
+ writeFileSync11(sidecarPath, JSON.stringify({ sourcesDigest, entries: result.entries }), "utf8");
9492
+ const rel = relative6(process.cwd(), sidecarPath).replace(/\\/g, "/");
9493
+ if (!rel.startsWith("..")) {
9494
+ ensureGitignoreEntry(process.cwd(), rel, "cdd-kit local cache (do not commit)");
9495
+ }
9496
+ } catch {
9497
+ }
9179
9498
  if (!opts.silent)
9180
9499
  log.ok(`${summaryLine} (${Date.now() - start}ms)`);
9181
9500
  return 0;
@@ -9188,10 +9507,12 @@ var init_code_map = __esm({
9188
9507
  init_yaml_writer();
9189
9508
  init_orchestrator();
9190
9509
  init_config();
9510
+ init_index_reader();
9191
9511
  init_digest();
9512
+ init_gitignore();
9192
9513
  _require = createRequire(import.meta.url);
9193
- _pkgPath = join18(fileURLToPath2(import.meta.url), "..", "..", "..", "package.json");
9194
- _pkg = JSON.parse(readFileSync18(_pkgPath, "utf8"));
9514
+ _pkgPath = join21(fileURLToPath2(import.meta.url), "..", "..", "..", "package.json");
9515
+ _pkg = JSON.parse(readFileSync21(_pkgPath, "utf8"));
9195
9516
  }
9196
9517
  });
9197
9518
 
@@ -9200,15 +9521,15 @@ var refresh_exports = {};
9200
9521
  __export(refresh_exports, {
9201
9522
  refresh: () => refresh
9202
9523
  });
9203
- import { existsSync as existsSync16, mkdirSync as mkdirSync9, readdirSync as readdirSync10, copyFileSync as copyFileSync4, readFileSync as readFileSync19, writeFileSync as writeFileSync10 } from "fs";
9204
- import { dirname as dirname6, join as join19, relative as relative7 } from "path";
9524
+ import { existsSync as existsSync19, mkdirSync as mkdirSync9, readdirSync as readdirSync10, copyFileSync as copyFileSync4, readFileSync as readFileSync22, writeFileSync as writeFileSync12 } from "fs";
9525
+ import { dirname as dirname6, join as join22, relative as relative7 } from "path";
9205
9526
  import { createHash as createHash6 } from "crypto";
9206
9527
  function fileHash2(filePath) {
9207
- return createHash6("sha256").update(readFileSync19(filePath)).digest("hex");
9528
+ return createHash6("sha256").update(readFileSync22(filePath)).digest("hex");
9208
9529
  }
9209
9530
  function planForceRefresh(srcDir, destDir, sectionLabel) {
9210
9531
  const plan = [];
9211
- if (!existsSync16(srcDir))
9532
+ if (!existsSync19(srcDir))
9212
9533
  return plan;
9213
9534
  function walk(curSrc, curDest) {
9214
9535
  let items;
@@ -9218,16 +9539,16 @@ function planForceRefresh(srcDir, destDir, sectionLabel) {
9218
9539
  return;
9219
9540
  }
9220
9541
  for (const item of items) {
9221
- const sp = join19(curSrc, item.name);
9222
- const dp = join19(curDest, item.name);
9542
+ const sp = join22(curSrc, item.name);
9543
+ const dp = join22(curDest, item.name);
9223
9544
  if (item.isDirectory()) {
9224
9545
  walk(sp, dp);
9225
9546
  continue;
9226
9547
  }
9227
9548
  if (!item.isFile())
9228
9549
  continue;
9229
- const rel = join19(sectionLabel, relative7(srcDir, sp)).replace(/\\/g, "/");
9230
- if (!existsSync16(dp)) {
9550
+ const rel = join22(sectionLabel, relative7(srcDir, sp)).replace(/\\/g, "/");
9551
+ if (!existsSync19(dp)) {
9231
9552
  plan.push({ src: sp, dest: dp, rel, action: "add" });
9232
9553
  } else if (fileHash2(sp) !== fileHash2(dp)) {
9233
9554
  plan.push({ src: sp, dest: dp, rel, action: "overwrite" });
@@ -9240,35 +9561,14 @@ function planForceRefresh(srcDir, destDir, sectionLabel) {
9240
9561
  return plan;
9241
9562
  }
9242
9563
  function planSingleFile(src, dest, rel) {
9243
- if (!existsSync16(src))
9564
+ if (!existsSync19(src))
9244
9565
  return null;
9245
- if (!existsSync16(dest))
9566
+ if (!existsSync19(dest))
9246
9567
  return { src, dest, rel, action: "add" };
9247
9568
  if (fileHash2(src) !== fileHash2(dest))
9248
9569
  return { src, dest, rel, action: "overwrite" };
9249
9570
  return { src, dest, rel, action: "skip" };
9250
9571
  }
9251
- function ensureGitignoreEntry2(cwd, entry) {
9252
- const path = join19(cwd, ".gitignore");
9253
- const trimmed = entry.trim();
9254
- if (!trimmed)
9255
- return false;
9256
- const re = new RegExp(`^\\s*${trimmed.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\s*$`, "m");
9257
- let existing = "";
9258
- if (existsSync16(path))
9259
- existing = readFileSync19(path, "utf8");
9260
- if (re.test(existing))
9261
- return false;
9262
- const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
9263
- const block = existing.length === 0 ? `# cdd-kit generated backups (do not commit)
9264
- ${trimmed}
9265
- ` : `${sep}
9266
- # cdd-kit generated backups (do not commit)
9267
- ${trimmed}
9268
- `;
9269
- writeFileSync10(path, existing + block, "utf8");
9270
- return true;
9271
- }
9272
9572
  function applyPlan(plan, backupRoot) {
9273
9573
  let added = 0;
9274
9574
  let overwritten = 0;
@@ -9276,7 +9576,7 @@ function applyPlan(plan, backupRoot) {
9276
9576
  if (item.action === "skip")
9277
9577
  continue;
9278
9578
  if (item.action === "overwrite") {
9279
- const backupPath = join19(backupRoot, item.rel);
9579
+ const backupPath = join22(backupRoot, item.rel);
9280
9580
  mkdirSync9(dirname6(backupPath), { recursive: true });
9281
9581
  copyFileSync4(item.dest, backupPath);
9282
9582
  overwritten += 1;
@@ -9305,14 +9605,14 @@ function parseAgentFrontmatter(content) {
9305
9605
  return fm;
9306
9606
  }
9307
9607
  function resyncModelPolicy(cwd) {
9308
- const policyPath = join19(cwd, ".cdd", "model-policy.json");
9608
+ const policyPath = join22(cwd, ".cdd", "model-policy.json");
9309
9609
  const result = { changed: false, diff: [], policyPath };
9310
- if (!existsSync16(AGENTS_HOME))
9610
+ if (!existsSync19(AGENTS_HOME))
9311
9611
  return result;
9312
9612
  const desired = {};
9313
9613
  const agentFiles = readdirSync10(AGENTS_HOME, { withFileTypes: true }).filter((d) => d.isFile() && d.name.endsWith(".md"));
9314
9614
  for (const f of agentFiles) {
9315
- const content = readFileSync19(join19(AGENTS_HOME, f.name), "utf8");
9615
+ const content = readFileSync22(join22(AGENTS_HOME, f.name), "utf8");
9316
9616
  const fm = parseAgentFrontmatter(content);
9317
9617
  if (fm.name && fm.model)
9318
9618
  desired[fm.name] = fm.model;
@@ -9320,9 +9620,9 @@ function resyncModelPolicy(cwd) {
9320
9620
  if (Object.keys(desired).length === 0)
9321
9621
  return result;
9322
9622
  let existing = {};
9323
- if (existsSync16(policyPath)) {
9623
+ if (existsSync19(policyPath)) {
9324
9624
  try {
9325
- existing = JSON.parse(readFileSync19(policyPath, "utf8"));
9625
+ existing = JSON.parse(readFileSync22(policyPath, "utf8"));
9326
9626
  } catch {
9327
9627
  }
9328
9628
  }
@@ -9343,7 +9643,7 @@ function resyncModelPolicy(cwd) {
9343
9643
  if (!("provider" in merged))
9344
9644
  merged["provider"] = "claude";
9345
9645
  mkdirSync9(dirname6(policyPath), { recursive: true });
9346
- writeFileSync10(policyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
9646
+ writeFileSync12(policyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
9347
9647
  result.changed = true;
9348
9648
  return result;
9349
9649
  }
@@ -9351,21 +9651,21 @@ function planTemplateRefresh(cwd) {
9351
9651
  const sections = [];
9352
9652
  sections.push({
9353
9653
  name: "specs/templates",
9354
- plan: planForceRefresh(ASSET.specsTemplates, join19(cwd, "specs", "templates"), "specs/templates")
9654
+ plan: planForceRefresh(ASSET.specsTemplates, join22(cwd, "specs", "templates"), "specs/templates")
9355
9655
  });
9356
9656
  sections.push({
9357
9657
  name: "tests/templates",
9358
- plan: planForceRefresh(ASSET.testsTemplates, join19(cwd, "tests", "templates"), "tests/templates")
9658
+ plan: planForceRefresh(ASSET.testsTemplates, join22(cwd, "tests", "templates"), "tests/templates")
9359
9659
  });
9360
- const ciTemplatesAsset = join19(ASSET.ci, "..", "ci-templates");
9361
- if (existsSync16(ciTemplatesAsset)) {
9660
+ const ciTemplatesAsset = join22(ASSET.ci, "..", "ci-templates");
9661
+ if (existsSync19(ciTemplatesAsset)) {
9362
9662
  sections.push({
9363
9663
  name: "ci-templates",
9364
- plan: planForceRefresh(ciTemplatesAsset, join19(cwd, "ci-templates"), "ci-templates")
9664
+ plan: planForceRefresh(ciTemplatesAsset, join22(cwd, "ci-templates"), "ci-templates")
9365
9665
  });
9366
9666
  }
9367
- const wfAsset = join19(ASSET.githubWorkflows, "contract-driven-gates.yml");
9368
- const wfDest = join19(cwd, ".github", "workflows", "contract-driven-gates.yml");
9667
+ const wfAsset = join22(ASSET.githubWorkflows, "contract-driven-gates.yml");
9668
+ const wfDest = join22(cwd, ".github", "workflows", "contract-driven-gates.yml");
9369
9669
  const wfPlan = planSingleFile(wfAsset, wfDest, ".github/workflows/contract-driven-gates.yml");
9370
9670
  if (wfPlan)
9371
9671
  sections.push({ name: ".github/workflows/contract-driven-gates.yml", plan: [wfPlan] });
@@ -9420,14 +9720,14 @@ async function refresh(opts) {
9420
9720
  }
9421
9721
  if (apply) {
9422
9722
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9423
- backupRoot = join19(cwd, ".cdd", ".refresh-backup", ts);
9723
+ backupRoot = join22(cwd, ".cdd", ".refresh-backup", ts);
9424
9724
  const result = applyPlan(total, backupRoot);
9425
9725
  templateAdded = result.added;
9426
9726
  templateOverwritten = result.overwritten;
9427
9727
  log.ok(` applied: +${templateAdded} added, ~${templateOverwritten} overwritten`);
9428
9728
  if (templateOverwritten > 0) {
9429
9729
  log.info(` backup saved to: ${relative7(cwd, backupRoot).replace(/\\/g, "/")}`);
9430
- if (ensureGitignoreEntry2(cwd, ".cdd/.refresh-backup/")) {
9730
+ if (ensureGitignoreEntry(cwd, ".cdd/.refresh-backup/")) {
9431
9731
  log.info(" added `.cdd/.refresh-backup/` to .gitignore");
9432
9732
  }
9433
9733
  }
@@ -9440,8 +9740,8 @@ async function refresh(opts) {
9440
9740
  }
9441
9741
  log.blank();
9442
9742
  if (!opts.noHooks) {
9443
- const markerPath = join19(cwd, HOOKS_MARKER_PATH);
9444
- if (existsSync16(markerPath)) {
9743
+ const markerPath = join22(cwd, HOOKS_MARKER_PATH);
9744
+ if (existsSync19(markerPath)) {
9445
9745
  log.info("[4/6] re-install code-map pre-commit hook (marker found)");
9446
9746
  if (apply) {
9447
9747
  try {
@@ -9518,6 +9818,7 @@ var init_refresh = __esm({
9518
9818
  "use strict";
9519
9819
  init_paths();
9520
9820
  init_logger();
9821
+ init_gitignore();
9521
9822
  init_update();
9522
9823
  init_upgrade();
9523
9824
  init_code_map();
@@ -9526,76 +9827,195 @@ var init_refresh = __esm({
9526
9827
  }
9527
9828
  });
9528
9829
 
9529
- // src/code-map/freshness.ts
9530
- import { existsSync as existsSync17, readFileSync as readFileSync20, statSync as statSync3 } from "fs";
9531
- import { join as join20 } from "path";
9532
- function checkCodeMapFreshness(cwd, mapRel = ".cdd/code-map.yml", include, exclude) {
9533
- const mapPath = join20(cwd, mapRel);
9534
- let cfg;
9830
+ // src/commands/lint-agents.ts
9831
+ var lint_agents_exports = {};
9832
+ __export(lint_agents_exports, {
9833
+ collectAgentViolations: () => collectAgentViolations,
9834
+ lintAgentContent: () => lintAgentContent,
9835
+ lintAgents: () => lintAgents
9836
+ });
9837
+ import { readdirSync as readdirSync11, readFileSync as readFileSync23 } from "fs";
9838
+ import { join as join23 } from "path";
9839
+ import { load as yamlLoad2 } from "js-yaml";
9840
+ function extractRequiredArtifactsSection(content) {
9841
+ const match = content.match(
9842
+ /### (?:Suggested|Required) artifacts for this agent\s*\n([\s\S]*?)(?=\n#{2,3} |\n---|\s*$)/
9843
+ );
9844
+ return match ? match[0] : null;
9845
+ }
9846
+ function extractFirstReadScopeSection(content) {
9847
+ const match = content.match(/## Read scope\s*\n([\s\S]*?)(?=\n## |\s*$)/);
9848
+ return match ? match[0] : null;
9849
+ }
9850
+ function extractYamlBlock(section) {
9851
+ const match = section.match(/```ya?ml\s*\n([\s\S]*?)```/);
9852
+ return match ? match[0] : null;
9853
+ }
9854
+ function extractYamlBody(section) {
9855
+ const match = section.match(/```ya?ml\s*\n([\s\S]*?)```/);
9856
+ return match ? match[1] : null;
9857
+ }
9858
+ function strayTopLevelKeys(yamlBody) {
9859
+ let parsed;
9535
9860
  try {
9536
- cfg = loadCodeMapConfig(cwd);
9537
- } catch (err) {
9538
- return {
9539
- status: "config-error",
9540
- staleFiles: [],
9541
- staleCount: 0,
9542
- mapPath,
9543
- configError: err.message
9544
- };
9861
+ parsed = yamlLoad2(yamlBody);
9862
+ } catch {
9863
+ return null;
9545
9864
  }
9546
- const includeFinal = [...cfg.include, ...include ?? []];
9547
- const excludeFinal = [...cfg.exclude, ...exclude ?? []];
9548
- const sourceFiles = walkRepo(cwd, { include: includeFinal, exclude: excludeFinal });
9549
- if (!existsSync17(mapPath)) {
9550
- if (sourceFiles.length === 0) {
9551
- return { status: "missing-greenfield", staleFiles: [], staleCount: 0, mapPath };
9552
- }
9553
- return { status: "missing-with-sources", staleFiles: [], staleCount: 0, mapPath };
9865
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
9866
+ return null;
9554
9867
  }
9555
- const mapMtime = statSync3(mapPath).mtimeMs;
9556
- const staleAll = [];
9557
- for (const absPath of sourceFiles) {
9558
- try {
9559
- const mtime = statSync3(absPath).mtimeMs;
9560
- if (mtime > mapMtime) {
9561
- const rel = absPath.replace(/\\/g, "/").replace(cwd.replace(/\\/g, "/") + "/", "");
9562
- staleAll.push(rel);
9868
+ const keys = Object.keys(parsed);
9869
+ return keys.filter((k) => k !== "artifacts");
9870
+ }
9871
+ function hasFlatBacktickKeysWithoutFence(section) {
9872
+ if (/```ya?ml\s*\nartifacts:/.test(section))
9873
+ return false;
9874
+ const withoutFence = section.replace(/```ya?ml[\s\S]*?```/g, "");
9875
+ return /^- `[a-z][a-z0-9-]+`:/m.test(withoutFence);
9876
+ }
9877
+ function lintAgentContent(filename, rawContent, opts = {}) {
9878
+ const violations = [];
9879
+ let content = rawContent;
9880
+ if (content.charCodeAt(0) === 65279) {
9881
+ violations.push({
9882
+ file: filename,
9883
+ rule: "Meta",
9884
+ message: "file starts with UTF-8 BOM (U+FEFF); frontmatter parsers may treat the first key as invalid",
9885
+ level: "error"
9886
+ });
9887
+ content = content.slice(1);
9888
+ }
9889
+ const artifactsSection = extractRequiredArtifactsSection(content);
9890
+ if (!artifactsSection) {
9891
+ violations.push({
9892
+ file: filename,
9893
+ rule: "A",
9894
+ message: "missing ### Suggested artifacts for this agent section",
9895
+ level: "error"
9896
+ });
9897
+ } else {
9898
+ const yamlBlock = extractYamlBlock(artifactsSection);
9899
+ if (!yamlBlock || !/```ya?ml\s*\nartifacts:/.test(yamlBlock)) {
9900
+ violations.push({
9901
+ file: filename,
9902
+ rule: "A",
9903
+ message: 'bad Suggested-artifacts format: missing fenced yaml block starting with "artifacts:"',
9904
+ level: "error"
9905
+ });
9906
+ } else if (!yamlBlock.includes("{ type:") && !/- \{/.test(yamlBlock)) {
9907
+ violations.push({
9908
+ file: filename,
9909
+ rule: "A",
9910
+ message: "bad Suggested-artifacts format: YAML block has no { type: ..., pointer: ... } items",
9911
+ level: "error"
9912
+ });
9913
+ } else {
9914
+ const yamlBody = extractYamlBody(artifactsSection);
9915
+ if (yamlBody) {
9916
+ const stray = strayTopLevelKeys(yamlBody);
9917
+ if (stray && stray.length > 0) {
9918
+ violations.push({
9919
+ file: filename,
9920
+ rule: "A",
9921
+ message: `bad Suggested-artifacts format: stray top-level key(s) alongside artifacts: [${stray.join(", ")}] -- these are item keys, not log keys`,
9922
+ level: "error"
9923
+ });
9924
+ }
9563
9925
  }
9564
- } catch {
9926
+ }
9927
+ if (hasFlatBacktickKeysWithoutFence(artifactsSection)) {
9928
+ violations.push({
9929
+ file: filename,
9930
+ rule: "A",
9931
+ message: "bad Suggested-artifacts format: flat backtick-keyed lines found outside YAML fence (remove old `key: value` bullet style)",
9932
+ level: "error"
9933
+ });
9565
9934
  }
9566
9935
  }
9567
- if (staleAll.length === 0) {
9568
- return { status: "ok", staleFiles: [], staleCount: 0, mapPath };
9936
+ const readScopeCount = (content.match(/^## Read scope\s*$/gm) ?? []).length;
9937
+ if (readScopeCount > 1) {
9938
+ violations.push({
9939
+ file: filename,
9940
+ rule: "B",
9941
+ message: `duplicate ## Read scope headings found (${readScopeCount} occurrences -- remove all but the first)`,
9942
+ level: "error"
9943
+ });
9569
9944
  }
9570
- const declaredDigest = readSourcesDigest(mapPath);
9571
- if (declaredDigest !== null) {
9572
- const actualDigest = computeSourcesDigest(sourceFiles, cwd);
9573
- if (actualDigest === declaredDigest) {
9574
- return { status: "ok", staleFiles: [], staleCount: 0, mapPath };
9945
+ if (readScopeCount >= 1) {
9946
+ const readScopeSection = extractFirstReadScopeSection(content);
9947
+ if (readScopeSection && !readScopeSection.includes("context-manifest.md")) {
9948
+ violations.push({
9949
+ file: filename,
9950
+ rule: "C",
9951
+ message: "## Read scope section does not reference context-manifest.md",
9952
+ level: "error"
9953
+ });
9575
9954
  }
9576
9955
  }
9577
- return {
9578
- status: "stale",
9579
- staleFiles: staleAll.slice(0, 5),
9580
- staleCount: staleAll.length,
9581
- mapPath
9582
- };
9956
+ if (!content.includes("references/agent-log-protocol.md")) {
9957
+ violations.push({
9958
+ file: filename,
9959
+ rule: "D",
9960
+ message: "missing reference to references/agent-log-protocol.md",
9961
+ level: opts.strict ? "error" : "warning"
9962
+ });
9963
+ }
9964
+ return violations;
9583
9965
  }
9584
- function readSourcesDigest(mapPath) {
9966
+ function collectAgentViolations(cwd, opts = {}) {
9967
+ const agentsDir = join23(cwd, ".claude", "agents");
9968
+ let files;
9585
9969
  try {
9586
- const head = readFileSync20(mapPath, "utf8").slice(0, 2048);
9587
- const m = head.match(/^# sources-digest:\s*([a-f0-9]+)/m);
9588
- return m ? m[1] : null;
9970
+ files = readdirSync11(agentsDir).filter((f) => f.endsWith(".md")).sort();
9589
9971
  } catch {
9590
9972
  return null;
9591
9973
  }
9974
+ const violations = [];
9975
+ for (const filename of files) {
9976
+ let content;
9977
+ try {
9978
+ content = readFileSync23(join23(agentsDir, filename), "utf8");
9979
+ } catch {
9980
+ violations.push({
9981
+ file: filename,
9982
+ rule: "A",
9983
+ message: "cannot read file",
9984
+ level: "error"
9985
+ });
9986
+ continue;
9987
+ }
9988
+ violations.push(...lintAgentContent(filename, content, opts));
9989
+ }
9990
+ return violations;
9991
+ }
9992
+ async function lintAgents(opts) {
9993
+ const cwd = process.cwd();
9994
+ const violations = collectAgentViolations(cwd, opts);
9995
+ if (violations === null) {
9996
+ log.error(
9997
+ `lint-agents: cannot read ${join23(cwd, ".claude", "agents")} -- is this a cdd-kit project?`
9998
+ );
9999
+ return 1;
10000
+ }
10001
+ for (const v of violations) {
10002
+ const prefix = v.level === "error" ? "error" : "warning";
10003
+ process.stderr.write(`${v.file}: [Rule ${v.rule}] ${prefix} -- ${v.message}
10004
+ `);
10005
+ }
10006
+ const errorCount = violations.filter((v) => v.level === "error").length;
10007
+ const warnCount = violations.filter((v) => v.level === "warning").length;
10008
+ console.log(`lint-agents: ${errorCount} error(s), ${warnCount} warning(s)`);
10009
+ if (errorCount > 0)
10010
+ return 1;
10011
+ if (opts.strict && warnCount > 0)
10012
+ return 1;
10013
+ return 0;
9592
10014
  }
9593
- var init_freshness = __esm({
9594
- "src/code-map/freshness.ts"() {
10015
+ var init_lint_agents = __esm({
10016
+ "src/commands/lint-agents.ts"() {
9595
10017
  "use strict";
9596
- init_include_exclude();
9597
- init_config();
9598
- init_code_map();
10018
+ init_logger();
9599
10019
  }
9600
10020
  });
9601
10021
 
@@ -9604,17 +10024,17 @@ var doctor_exports = {};
9604
10024
  __export(doctor_exports, {
9605
10025
  doctor: () => doctor
9606
10026
  });
9607
- import { existsSync as existsSync18, readdirSync as readdirSync11, readFileSync as readFileSync21 } from "fs";
10027
+ import { existsSync as existsSync20, readdirSync as readdirSync12, readFileSync as readFileSync24 } from "fs";
9608
10028
  import { createHash as createHash7 } from "crypto";
9609
- import { join as join21, relative as relative8 } from "path";
10029
+ import { join as join24, relative as relative8 } from "path";
9610
10030
  function fileExists(cwd, relPath) {
9611
- return existsSync18(join21(cwd, relPath));
10031
+ return existsSync20(join24(cwd, relPath));
9612
10032
  }
9613
10033
  function findFiles(dir, predicate, found = []) {
9614
- if (!existsSync18(dir))
10034
+ if (!existsSync20(dir))
9615
10035
  return found;
9616
- for (const entry of readdirSync11(dir, { withFileTypes: true })) {
9617
- const fullPath = join21(dir, entry.name);
10036
+ for (const entry of readdirSync12(dir, { withFileTypes: true })) {
10037
+ const fullPath = join24(dir, entry.name);
9618
10038
  if (entry.isDirectory())
9619
10039
  findFiles(fullPath, predicate, found);
9620
10040
  else if (entry.isFile() && predicate(entry.name))
@@ -9630,9 +10050,9 @@ function inputDigest(paths, cwd) {
9630
10050
  return createHash7("sha256").update(combined).digest("hex");
9631
10051
  }
9632
10052
  function readContextIndexMetadata(filePath) {
9633
- if (!existsSync18(filePath))
10053
+ if (!existsSync20(filePath))
9634
10054
  return {};
9635
- const text = readFileSync21(filePath, "utf8");
10055
+ const text = readFileSync24(filePath, "utf8");
9636
10056
  const out = {};
9637
10057
  const digestMatch = text.match(/^inputs-digest:\s*([a-f0-9]+)/m);
9638
10058
  if (digestMatch)
@@ -9644,14 +10064,14 @@ function readContextIndexMetadata(filePath) {
9644
10064
  }
9645
10065
  function checkContextFreshness(cwd) {
9646
10066
  const findings = [];
9647
- const projectMap = join21(cwd, "specs", "context", "project-map.md");
9648
- const contractsIndex = join21(cwd, "specs", "context", "contracts-index.md");
9649
- const contextPolicy = join21(cwd, ".cdd", "context-policy.json");
10067
+ const projectMap = join24(cwd, "specs", "context", "project-map.md");
10068
+ const contractsIndex = join24(cwd, "specs", "context", "contracts-index.md");
10069
+ const contextPolicy = join24(cwd, ".cdd", "context-policy.json");
9650
10070
  const contractFiles = findFiles(
9651
- join21(cwd, "contracts"),
10071
+ join24(cwd, "contracts"),
9652
10072
  (name) => name.endsWith(".md") && name !== "INDEX.md" && name !== "CHANGELOG.md"
9653
10073
  );
9654
- if (!existsSync18(projectMap) || !existsSync18(contractsIndex)) {
10074
+ if (!existsSync20(projectMap) || !existsSync20(contractsIndex)) {
9655
10075
  findings.push({
9656
10076
  level: "warning",
9657
10077
  message: "specs/context indexes are missing; run cdd-kit context-scan before classification"
@@ -9660,7 +10080,7 @@ function checkContextFreshness(cwd) {
9660
10080
  }
9661
10081
  const projectMapMeta = readContextIndexMetadata(projectMap);
9662
10082
  const contractsIndexMeta = readContextIndexMetadata(contractsIndex);
9663
- const projectInputDigest = inputDigest([contextPolicy].filter(existsSync18), cwd);
10083
+ const projectInputDigest = inputDigest([contextPolicy].filter(existsSync20), cwd);
9664
10084
  if (projectMapMeta.inputsDigest === void 0) {
9665
10085
  findings.push({
9666
10086
  level: "warning",
@@ -9697,7 +10117,7 @@ function checkContextFreshness(cwd) {
9697
10117
  }
9698
10118
  function readAgentModel(path) {
9699
10119
  try {
9700
- const text = readFileSync21(path, "utf8");
10120
+ const text = readFileSync24(path, "utf8");
9701
10121
  const m = text.match(/^model:\s*(\S+)/m);
9702
10122
  return m ? m[1] : null;
9703
10123
  } catch {
@@ -9705,12 +10125,12 @@ function readAgentModel(path) {
9705
10125
  }
9706
10126
  }
9707
10127
  function checkModelPolicyDrift(cwd) {
9708
- const policyPath = join21(cwd, ".cdd", "model-policy.json");
9709
- if (!existsSync18(policyPath))
10128
+ const policyPath = join24(cwd, ".cdd", "model-policy.json");
10129
+ if (!existsSync20(policyPath))
9710
10130
  return [];
9711
10131
  let policy;
9712
10132
  try {
9713
- policy = JSON.parse(readFileSync21(policyPath, "utf8"));
10133
+ policy = JSON.parse(readFileSync24(policyPath, "utf8"));
9714
10134
  } catch {
9715
10135
  return [{ level: "warning", message: ".cdd/model-policy.json is not valid JSON" }];
9716
10136
  }
@@ -9722,18 +10142,18 @@ function checkModelPolicyDrift(cwd) {
9722
10142
  }];
9723
10143
  }
9724
10144
  const candidateDirs = [
9725
- join21(cwd, ".claude", "agents"),
9726
- process.env.HOME ? join21(process.env.HOME, ".claude", "agents") : "",
9727
- process.env.USERPROFILE ? join21(process.env.USERPROFILE, ".claude", "agents") : ""
9728
- ].filter((p) => p && existsSync18(p));
10145
+ join24(cwd, ".claude", "agents"),
10146
+ process.env.HOME ? join24(process.env.HOME, ".claude", "agents") : "",
10147
+ process.env.USERPROFILE ? join24(process.env.USERPROFILE, ".claude", "agents") : ""
10148
+ ].filter((p) => p && existsSync20(p));
9729
10149
  if (candidateDirs.length === 0)
9730
10150
  return [];
9731
10151
  const findings = [];
9732
10152
  for (const [role, expected] of Object.entries(roles)) {
9733
10153
  let foundAny = false;
9734
10154
  for (const dir of candidateDirs) {
9735
- const path = join21(dir, `${role}.md`);
9736
- if (!existsSync18(path))
10155
+ const path = join24(dir, `${role}.md`);
10156
+ if (!existsSync20(path))
9737
10157
  continue;
9738
10158
  foundAny = true;
9739
10159
  const actual = readAgentModel(path);
@@ -9752,47 +10172,30 @@ function checkModelPolicyDrift(cwd) {
9752
10172
  }
9753
10173
  return findings;
9754
10174
  }
9755
- async function checkAgentLint(cwd) {
9756
- const agentsDir = join21(cwd, ".claude", "agents");
9757
- if (!existsSync18(agentsDir))
9758
- return [];
9759
- try {
9760
- const { readdirSync: rds, readFileSync: rfs } = await import("fs");
9761
- const files = rds(agentsDir).filter((f) => f.endsWith(".md"));
9762
- if (files.length === 0)
9763
- return [];
9764
- const findings = [];
9765
- for (const filename of files) {
9766
- const content = rfs(join21(agentsDir, filename), "utf8");
9767
- const artifactsSection = content.match(
9768
- /### Required artifacts for this agent\s*\n[\s\S]*?(?=\n#{2,3} |\n---|\s*$)/
9769
- )?.[0];
9770
- if (!artifactsSection || !/```ya?ml\s*\nartifacts:/.test(artifactsSection)) {
9771
- findings.push({
9772
- level: "warning",
9773
- message: `lint-agents: ${filename}: missing artifacts YAML block in Required artifacts section`
9774
- });
9775
- }
9776
- const readScopeCount = (content.match(/^## Read scope\s*$/gm) ?? []).length;
9777
- if (readScopeCount > 1) {
9778
- findings.push({
9779
- level: "warning",
9780
- message: `lint-agents: ${filename}: duplicate ## Read scope headings (${readScopeCount})`
9781
- });
9782
- }
9783
- }
9784
- if (findings.length === 0) {
9785
- findings.push({ level: "ok", message: "lint-agents: all agent prompts pass shape checks" });
9786
- }
9787
- return findings;
9788
- } catch {
10175
+ function checkAgentLint(cwd) {
10176
+ const agentsDir = join24(cwd, ".claude", "agents");
10177
+ if (!existsSync20(agentsDir))
9789
10178
  return [];
10179
+ const violations = collectAgentViolations(cwd);
10180
+ if (violations === null) {
10181
+ return [{
10182
+ level: "warning",
10183
+ message: `lint-agents: could not read ${join24(".claude", "agents")} -- agent prompts were not scanned`
10184
+ }];
10185
+ }
10186
+ const findings = violations.map((v) => ({
10187
+ level: "warning",
10188
+ message: `lint-agents: ${v.file}: [Rule ${v.rule}] ${v.message}`
10189
+ }));
10190
+ if (findings.length === 0) {
10191
+ findings.push({ level: "ok", message: "lint-agents: all agent prompts pass shape checks" });
9790
10192
  }
10193
+ return findings;
9791
10194
  }
9792
10195
  function checkCodeMap(cwd) {
9793
10196
  const findings = [];
9794
- const mapPath = join21(cwd, ".cdd", "code-map.yml");
9795
- if (!existsSync18(mapPath)) {
10197
+ const mapPath = join24(cwd, ".cdd", "code-map.yml");
10198
+ if (!existsSync20(mapPath)) {
9796
10199
  const probe2 = checkCodeMapFreshness(cwd);
9797
10200
  if (probe2.status === "config-error") {
9798
10201
  findings.push({ level: "warning", message: `.cdd/code-map-config.yml is invalid: ${probe2.configError}` });
@@ -9811,7 +10214,7 @@ function checkCodeMap(cwd) {
9811
10214
  const more = probe.staleCount > 3 ? ` (+${probe.staleCount - 3} more)` : "";
9812
10215
  findings.push({ level: "warning", message: `code-map stale: ${top}${more}; run \`cdd-kit code-map\`` });
9813
10216
  }
9814
- const text = readFileSync21(mapPath, "utf8");
10217
+ const text = readFileSync24(mapPath, "utf8");
9815
10218
  const m = text.match(/^# files: (\d+), src-lines: (\d+), map-lines: (\d+), compression: ([\d.]+)x/m);
9816
10219
  if (m) {
9817
10220
  findings.push({ level: "ok", message: `code-map: ${m[1]} files, ${m[4]}x compression` });
@@ -9843,7 +10246,7 @@ async function buildDoctorReport(cwd, opts) {
9843
10246
  }
9844
10247
  findings.push(...checkContextFreshness(cwd));
9845
10248
  findings.push(...checkModelPolicyDrift(cwd));
9846
- findings.push(...await checkAgentLint(cwd));
10249
+ findings.push(...checkAgentLint(cwd));
9847
10250
  findings.push(...checkCodeMap(cwd));
9848
10251
  const errors = findings.filter((finding) => finding.level === "error").length;
9849
10252
  const warnings = findings.filter((finding) => finding.level === "warning").length;
@@ -9887,11 +10290,11 @@ async function attemptAutoFixes(cwd, report) {
9887
10290
  }
9888
10291
  }
9889
10292
  if (/model-policy\.json has no role bindings/i.test(finding.message)) {
9890
- const policyPath = join21(cwd, ".cdd", "model-policy.json");
10293
+ const policyPath = join24(cwd, ".cdd", "model-policy.json");
9891
10294
  try {
9892
10295
  let existing = {};
9893
10296
  try {
9894
- existing = JSON.parse(readFileSync21(policyPath, "utf8"));
10297
+ existing = JSON.parse(readFileSync24(policyPath, "utf8"));
9895
10298
  } catch {
9896
10299
  }
9897
10300
  const merged = {
@@ -9915,8 +10318,8 @@ async function attemptAutoFixes(cwd, report) {
9915
10318
  "repo-context-scanner": "haiku"
9916
10319
  }
9917
10320
  };
9918
- const { writeFileSync: writeFileSync14 } = await import("fs");
9919
- writeFileSync14(policyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
10321
+ const { writeFileSync: writeFileSync16 } = await import("fs");
10322
+ writeFileSync16(policyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
9920
10323
  fixed.push(`populated .cdd/model-policy.json with default role bindings`);
9921
10324
  continue;
9922
10325
  } catch (err) {
@@ -9984,268 +10387,50 @@ var init_doctor = __esm({
9984
10387
  init_logger();
9985
10388
  init_provider();
9986
10389
  init_freshness();
10390
+ init_lint_agents();
9987
10391
  init_digest();
9988
10392
  }
9989
10393
  });
9990
10394
 
9991
- // src/commands/lint-agents.ts
9992
- var lint_agents_exports = {};
9993
- __export(lint_agents_exports, {
9994
- lintAgents: () => lintAgents
9995
- });
9996
- import { readdirSync as readdirSync12, readFileSync as readFileSync22 } from "fs";
9997
- import { join as join22 } from "path";
9998
- import { load as yamlLoad2 } from "js-yaml";
9999
- function extractRequiredArtifactsSection(content) {
10000
- const match = content.match(
10001
- /### (?:Suggested|Required) artifacts for this agent\s*\n([\s\S]*?)(?=\n#{2,3} |\n---|\s*$)/
10002
- );
10003
- return match ? match[0] : null;
10004
- }
10005
- function extractFirstReadScopeSection(content) {
10006
- const match = content.match(/## Read scope\s*\n([\s\S]*?)(?=\n## |\s*$)/);
10007
- return match ? match[0] : null;
10008
- }
10009
- function extractYamlBlock(section) {
10010
- const match = section.match(/```ya?ml\s*\n([\s\S]*?)```/);
10011
- return match ? match[0] : null;
10012
- }
10013
- function extractYamlBody(section) {
10014
- const match = section.match(/```ya?ml\s*\n([\s\S]*?)```/);
10015
- return match ? match[1] : null;
10016
- }
10017
- function strayTopLevelKeys(yamlBody) {
10018
- let parsed;
10019
- try {
10020
- parsed = yamlLoad2(yamlBody);
10021
- } catch {
10022
- return null;
10023
- }
10024
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
10025
- return null;
10395
+ // src/commands/code-map-scan-worker.ts
10396
+ var code_map_scan_worker_exports = {};
10397
+ __export(code_map_scan_worker_exports, {
10398
+ runScanWorker: () => runScanWorker
10399
+ });
10400
+ import { readFileSync as readFileSync25 } from "fs";
10401
+ async function runScanWorker(opts) {
10402
+ let scanner;
10403
+ switch (opts.lang) {
10404
+ case "js":
10405
+ scanner = (await Promise.resolve().then(() => (init_javascript(), javascript_exports))).jsScanner;
10406
+ break;
10407
+ case "ts":
10408
+ scanner = (await Promise.resolve().then(() => (init_typescript(), typescript_exports))).tsScanner;
10409
+ break;
10410
+ case "vue":
10411
+ scanner = (await Promise.resolve().then(() => (init_vue(), vue_exports))).vueScanner;
10412
+ break;
10413
+ default:
10414
+ process.stderr.write(`__code-map-scan: unknown --lang "${opts.lang}"
10415
+ `);
10416
+ return 1;
10026
10417
  }
10027
- const keys = Object.keys(parsed);
10028
- return keys.filter((k) => k !== "artifacts");
10029
- }
10030
- function hasFlatBacktickKeysWithoutFence(section) {
10031
- if (/```ya?ml\s*\nartifacts:/.test(section))
10032
- return false;
10033
- const withoutFence = section.replace(/```ya?ml[\s\S]*?```/g, "");
10034
- return /^- `[a-z][a-z0-9-]+`:/m.test(withoutFence);
10035
- }
10036
- async function lintAgents(opts) {
10037
- const cwd = process.cwd();
10038
- const agentsDir = join22(cwd, ".claude", "agents");
10039
10418
  let files;
10040
10419
  try {
10041
- files = readdirSync12(agentsDir).filter((f) => f.endsWith(".md")).sort();
10042
- } catch {
10043
- log.error(`lint-agents: cannot read ${agentsDir} ??is this a cdd-kit project?`);
10044
- return 1;
10045
- }
10046
- const violations = [];
10047
- for (const filename of files) {
10048
- const filePath = join22(agentsDir, filename);
10049
- let content;
10050
- try {
10051
- content = readFileSync22(filePath, "utf8");
10052
- } catch {
10053
- violations.push({
10054
- file: filename,
10055
- rule: "A",
10056
- message: "cannot read file",
10057
- level: "error"
10058
- });
10059
- continue;
10060
- }
10061
- if (content.charCodeAt(0) === 65279) {
10062
- violations.push({
10063
- file: filename,
10064
- rule: "Meta",
10065
- message: "file starts with UTF-8 BOM (U+FEFF); frontmatter parsers may treat the first key as invalid",
10066
- level: "error"
10067
- });
10068
- content = content.slice(1);
10069
- }
10070
- const artifactsSection = extractRequiredArtifactsSection(content);
10071
- if (!artifactsSection) {
10072
- violations.push({
10073
- file: filename,
10074
- rule: "A",
10075
- message: "missing ### Suggested artifacts for this agent section",
10076
- level: "error"
10077
- });
10078
- } else {
10079
- const yamlBlock = extractYamlBlock(artifactsSection);
10080
- if (!yamlBlock || !/```ya?ml\s*\nartifacts:/.test(yamlBlock)) {
10081
- violations.push({
10082
- file: filename,
10083
- rule: "A",
10084
- message: 'bad Suggested-artifacts format: missing fenced yaml block starting with "artifacts:"',
10085
- level: "error"
10086
- });
10087
- } else if (!yamlBlock.includes("{ type:") && !/- \{/.test(yamlBlock)) {
10088
- violations.push({
10089
- file: filename,
10090
- rule: "A",
10091
- message: "bad Suggested-artifacts format: YAML block has no { type: ..., pointer: ... } items",
10092
- level: "error"
10093
- });
10094
- } else {
10095
- const yamlBody = extractYamlBody(artifactsSection);
10096
- if (yamlBody) {
10097
- const stray = strayTopLevelKeys(yamlBody);
10098
- if (stray && stray.length > 0) {
10099
- violations.push({
10100
- file: filename,
10101
- rule: "A",
10102
- message: `bad Suggested-artifacts format: stray top-level key(s) alongside artifacts: [${stray.join(", ")}] ??these are item keys, not log keys`,
10103
- level: "error"
10104
- });
10105
- }
10106
- }
10107
- }
10108
- if (hasFlatBacktickKeysWithoutFence(artifactsSection)) {
10109
- violations.push({
10110
- file: filename,
10111
- rule: "A",
10112
- message: "bad Suggested-artifacts format: flat backtick-keyed lines found outside YAML fence (remove old `key: value` bullet style)",
10113
- level: "error"
10114
- });
10115
- }
10116
- }
10117
- const readScopeCount = (content.match(/^## Read scope\s*$/gm) ?? []).length;
10118
- if (readScopeCount > 1) {
10119
- violations.push({
10120
- file: filename,
10121
- rule: "B",
10122
- message: `duplicate ## Read scope headings found (${readScopeCount} occurrences ??remove all but the first)`,
10123
- level: "error"
10124
- });
10125
- }
10126
- if (readScopeCount >= 1) {
10127
- const readScopeSection = extractFirstReadScopeSection(content);
10128
- if (readScopeSection && !readScopeSection.includes("context-manifest.md")) {
10129
- violations.push({
10130
- file: filename,
10131
- rule: "C",
10132
- message: "## Read scope section does not reference context-manifest.md",
10133
- level: "error"
10134
- });
10135
- }
10136
- }
10137
- if (!content.includes("references/agent-log-protocol.md")) {
10138
- violations.push({
10139
- file: filename,
10140
- rule: "D",
10141
- message: "missing reference to references/agent-log-protocol.md",
10142
- level: opts.strict ? "error" : "warning"
10143
- });
10144
- }
10145
- }
10146
- for (const v of violations) {
10147
- const prefix = v.level === "error" ? "error" : "warning";
10148
- process.stderr.write(`${v.file}: [Rule ${v.rule}] ${prefix} ??${v.message}
10420
+ files = readFileSync25(opts.batchFile, "utf8").split("\n").map((s) => s.trim()).filter(Boolean);
10421
+ } catch (err) {
10422
+ process.stderr.write(`__code-map-scan: cannot read --batch-file: ${err.message}
10149
10423
  `);
10150
- }
10151
- const errorCount = violations.filter((v) => v.level === "error").length;
10152
- const warnCount = violations.filter((v) => v.level === "warning").length;
10153
- console.log(`lint-agents: ${errorCount} error(s), ${warnCount} warning(s)`);
10154
- if (errorCount > 0)
10155
- return 1;
10156
- if (opts.strict && warnCount > 0)
10157
10424
  return 1;
10158
- return 0;
10159
- }
10160
- var init_lint_agents = __esm({
10161
- "src/commands/lint-agents.ts"() {
10162
- "use strict";
10163
- init_logger();
10164
- }
10165
- });
10166
-
10167
- // src/code-map/index-reader.ts
10168
- import { existsSync as existsSync19, readFileSync as readFileSync23 } from "fs";
10169
- import yaml3 from "js-yaml";
10170
- async function ensureCodeMapFresh(mapPath, refresh2) {
10171
- if (!refresh2)
10172
- return { refreshed: false };
10173
- const freshness = checkCodeMapFreshness(process.cwd(), mapPath);
10174
- if (freshness.status === "config-error") {
10175
- return {
10176
- refreshed: false,
10177
- error: `.cdd/code-map-config.yml is invalid: ${freshness.configError}`
10178
- };
10179
- }
10180
- if (freshness.status === "missing-with-sources" || freshness.status === "missing-greenfield" || freshness.status === "stale") {
10181
- const { codeMap: codeMap2 } = await Promise.resolve().then(() => (init_code_map(), code_map_exports));
10182
- const exit = await codeMap2({
10183
- path: ".",
10184
- out: mapPath,
10185
- include: [],
10186
- exclude: [],
10187
- check: false,
10188
- maxLines: 1e5,
10189
- silent: true
10190
- });
10191
- if (exit !== 0) {
10192
- return {
10193
- refreshed: false,
10194
- error: `could not refresh ${mapPath}; run \`cdd-kit code-map\` for details.`
10195
- };
10196
- }
10197
- return { refreshed: true };
10198
- }
10199
- return { refreshed: false };
10200
- }
10201
- function loadCodeMapEntries(mapPath) {
10202
- if (!existsSync19(mapPath)) {
10203
- throw new Error(`${mapPath} is missing; run \`cdd-kit code-map\` first.`);
10204
- }
10205
- const text = readFileSync23(mapPath, "utf8");
10206
- const totalLinesByPath = extractTotalLines(text);
10207
- const raw = yaml3.load(text, { schema: yaml3.JSON_SCHEMA });
10208
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
10209
- return [];
10210
- const entries = [];
10211
- for (const [path, value] of Object.entries(raw)) {
10212
- if (!value || typeof value !== "object" || Array.isArray(value))
10213
- continue;
10214
- const obj = value;
10215
- entries.push({
10216
- path,
10217
- total_lines: totalLinesByPath.get(path) ?? (typeof obj.total_lines === "number" ? obj.total_lines : 0),
10218
- imports: Array.isArray(obj.imports) ? obj.imports : [],
10219
- constants: Array.isArray(obj.constants) ? obj.constants : [],
10220
- classes: Array.isArray(obj.classes) ? obj.classes : [],
10221
- functions: Array.isArray(obj.functions) ? obj.functions : [],
10222
- interfaces: Array.isArray(obj.interfaces) ? obj.interfaces : [],
10223
- types: Array.isArray(obj.types) ? obj.types : [],
10224
- enums: Array.isArray(obj.enums) ? obj.enums : []
10225
- });
10226
- }
10227
- return entries;
10228
- }
10229
- function extractTotalLines(text) {
10230
- const totals = /* @__PURE__ */ new Map();
10231
- for (const line of text.split(/\r?\n/)) {
10232
- const m = line.match(/^((?:'[^']*(?:''[^']*)*')|[^#:\s][^#]*?):\s*#\s*(\d+)\s+lines\b/);
10233
- if (!m)
10234
- continue;
10235
- totals.set(unquoteYamlKey(m[1].trim()), Number(m[2]));
10236
- }
10237
- return totals;
10238
- }
10239
- function unquoteYamlKey(key) {
10240
- if (key.startsWith("'") && key.endsWith("'")) {
10241
- return key.slice(1, -1).replace(/''/g, "'");
10242
10425
  }
10243
- return key;
10426
+ const result = await scanInProcess(scanner, files, opts.repoRoot);
10427
+ process.stdout.write(JSON.stringify(result));
10428
+ return 0;
10244
10429
  }
10245
- var init_index_reader = __esm({
10246
- "src/code-map/index-reader.ts"() {
10430
+ var init_code_map_scan_worker = __esm({
10431
+ "src/commands/code-map-scan-worker.ts"() {
10247
10432
  "use strict";
10248
- init_freshness();
10433
+ init_orchestrator();
10249
10434
  }
10250
10435
  });
10251
10436
 
@@ -10254,7 +10439,7 @@ var index_query_exports = {};
10254
10439
  __export(index_query_exports, {
10255
10440
  indexQuery: () => indexQuery
10256
10441
  });
10257
- import { existsSync as existsSync20 } from "fs";
10442
+ import { existsSync as existsSync21 } from "fs";
10258
10443
  async function indexQuery(term, opts) {
10259
10444
  const mapPath = opts.map || ".cdd/code-map.yml";
10260
10445
  const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 10;
@@ -10264,7 +10449,7 @@ async function indexQuery(term, opts) {
10264
10449
  return printFailure(freshness.error, opts.json);
10265
10450
  }
10266
10451
  refreshed = freshness.refreshed;
10267
- if (!existsSync20(mapPath)) {
10452
+ if (!existsSync21(mapPath)) {
10268
10453
  return printFailure(`${mapPath} is missing; run \`cdd-kit code-map\` first.`, opts.json);
10269
10454
  }
10270
10455
  let entries;
@@ -10432,7 +10617,7 @@ var index_impact_exports = {};
10432
10617
  __export(index_impact_exports, {
10433
10618
  indexImpact: () => indexImpact
10434
10619
  });
10435
- import { existsSync as existsSync21 } from "fs";
10620
+ import { existsSync as existsSync22 } from "fs";
10436
10621
  import { posix } from "path";
10437
10622
  async function indexImpact(term, opts) {
10438
10623
  const mapPath = opts.map || ".cdd/code-map.yml";
@@ -10441,7 +10626,7 @@ async function indexImpact(term, opts) {
10441
10626
  if (freshness.error) {
10442
10627
  return printFailure2(freshness.error, opts.json);
10443
10628
  }
10444
- if (!existsSync21(mapPath)) {
10629
+ if (!existsSync22(mapPath)) {
10445
10630
  return printFailure2(`${mapPath} is missing; run \`cdd-kit code-map\` first.`, opts.json);
10446
10631
  }
10447
10632
  let entries;
@@ -10684,28 +10869,28 @@ var archive_exports = {};
10684
10869
  __export(archive_exports, {
10685
10870
  archive: () => archive
10686
10871
  });
10687
- import { join as join23 } from "path";
10688
- import { existsSync as existsSync22, mkdirSync as mkdirSync10, renameSync as renameSync2, readFileSync as readFileSync24, writeFileSync as writeFileSync11, appendFileSync, cpSync as cpSync3, rmSync as rmSync3 } from "fs";
10872
+ import { join as join25 } from "path";
10873
+ import { existsSync as existsSync23, mkdirSync as mkdirSync10, renameSync as renameSync2, readFileSync as readFileSync26, writeFileSync as writeFileSync13, appendFileSync, cpSync as cpSync3, rmSync as rmSync3 } from "fs";
10689
10874
  import yaml4 from "js-yaml";
10690
10875
  async function archive(changeId) {
10691
10876
  const cwd = process.cwd();
10692
- const changeDir = join23(cwd, "specs", "changes", changeId);
10877
+ const changeDir = join25(cwd, "specs", "changes", changeId);
10693
10878
  const archiveYear = (/* @__PURE__ */ new Date()).getFullYear().toString();
10694
- const archiveBase = join23(cwd, "specs", "archive", archiveYear);
10695
- const archiveDir = join23(archiveBase, changeId);
10696
- const indexPath = join23(cwd, "specs", "archive", "INDEX.md");
10697
- if (!existsSync22(changeDir)) {
10879
+ const archiveBase = join25(cwd, "specs", "archive", archiveYear);
10880
+ const archiveDir = join25(archiveBase, changeId);
10881
+ const indexPath = join25(cwd, "specs", "archive", "INDEX.md");
10882
+ if (!existsSync23(changeDir)) {
10698
10883
  log.error(`Change not found: specs/changes/${changeId}`);
10699
10884
  process.exit(1);
10700
10885
  }
10701
- if (existsSync22(archiveDir)) {
10886
+ if (existsSync23(archiveDir)) {
10702
10887
  log.error(`Already archived: specs/archive/${archiveYear}/${changeId}`);
10703
10888
  process.exit(1);
10704
10889
  }
10705
- const tasksPath = join23(changeDir, "tasks.yml");
10706
- if (existsSync22(tasksPath)) {
10890
+ const tasksPath = join25(changeDir, "tasks.yml");
10891
+ if (existsSync23(tasksPath)) {
10707
10892
  try {
10708
- const raw = readFileSync24(tasksPath, "utf8");
10893
+ const raw = readFileSync26(tasksPath, "utf8");
10709
10894
  const data = yaml4.load(raw);
10710
10895
  if (data?.status === "gate-blocked") {
10711
10896
  log.warn("tasks.yml has status: gate-blocked \u2014 archiving anyway (change was paused).");
@@ -10718,7 +10903,7 @@ async function archive(changeId) {
10718
10903
  log.warn("tasks.yml could not be parsed \u2014 archiving anyway.");
10719
10904
  }
10720
10905
  }
10721
- if (!existsSync22(archiveBase)) {
10906
+ if (!existsSync23(archiveBase)) {
10722
10907
  mkdirSync10(archiveBase, { recursive: true });
10723
10908
  }
10724
10909
  try {
@@ -10735,8 +10920,8 @@ async function archive(changeId) {
10735
10920
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
10736
10921
  const indexLine = `| ${changeId} | ${archiveYear} | ${today} | specs/archive/${archiveYear}/${changeId}/ |
10737
10922
  `;
10738
- if (!existsSync22(indexPath)) {
10739
- writeFileSync11(indexPath, `# Archive Index
10923
+ if (!existsSync23(indexPath)) {
10924
+ writeFileSync13(indexPath, `# Archive Index
10740
10925
 
10741
10926
  | change-id | year | archived-date | path |
10742
10927
  |---|---|---|---|
@@ -10760,37 +10945,37 @@ var abandon_exports = {};
10760
10945
  __export(abandon_exports, {
10761
10946
  abandon: () => abandon
10762
10947
  });
10763
- import { join as join24 } from "path";
10764
- import { existsSync as existsSync23, readFileSync as readFileSync25, writeFileSync as writeFileSync12, appendFileSync as appendFileSync2, mkdirSync as mkdirSync11 } from "fs";
10948
+ import { join as join26 } from "path";
10949
+ import { existsSync as existsSync24, readFileSync as readFileSync27, writeFileSync as writeFileSync14, appendFileSync as appendFileSync2, mkdirSync as mkdirSync11 } from "fs";
10765
10950
  import yaml5 from "js-yaml";
10766
10951
  async function abandon(changeId, opts) {
10767
10952
  const cwd = process.cwd();
10768
- const changeDir = join24(cwd, "specs", "changes", changeId);
10769
- const tasksPath = join24(changeDir, "tasks.yml");
10770
- if (!existsSync23(changeDir)) {
10953
+ const changeDir = join26(cwd, "specs", "changes", changeId);
10954
+ const tasksPath = join26(changeDir, "tasks.yml");
10955
+ if (!existsSync24(changeDir)) {
10771
10956
  log.error(`Change not found: specs/changes/${changeId}`);
10772
10957
  process.exit(1);
10773
10958
  }
10774
- if (existsSync23(tasksPath)) {
10775
- const raw = readFileSync25(tasksPath, "utf8");
10959
+ if (existsSync24(tasksPath)) {
10960
+ const raw = readFileSync27(tasksPath, "utf8");
10776
10961
  const data = yaml5.load(raw) ?? {};
10777
10962
  data["status"] = "abandoned";
10778
10963
  if (!data["change-id"]) {
10779
10964
  data["change-id"] = changeId;
10780
10965
  }
10781
- writeFileSync12(tasksPath, yaml5.dump(data, { lineWidth: -1, noRefs: true }), "utf8");
10966
+ writeFileSync14(tasksPath, yaml5.dump(data, { lineWidth: -1, noRefs: true }), "utf8");
10782
10967
  }
10783
10968
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
10784
- const archiveDir = join24(cwd, "specs", "archive");
10785
- const indexPath = join24(archiveDir, "INDEX.md");
10969
+ const archiveDir = join26(cwd, "specs", "archive");
10970
+ const indexPath = join26(archiveDir, "INDEX.md");
10786
10971
  const reason = opts.reason ?? "no reason given";
10787
10972
  const indexLine = `| ${changeId} | abandoned | ${today} | ${reason} |
10788
10973
  `;
10789
- if (!existsSync23(archiveDir)) {
10974
+ if (!existsSync24(archiveDir)) {
10790
10975
  mkdirSync11(archiveDir, { recursive: true });
10791
10976
  }
10792
- if (!existsSync23(indexPath)) {
10793
- writeFileSync12(indexPath, `# Archive Index
10977
+ if (!existsSync24(indexPath)) {
10978
+ writeFileSync14(indexPath, `# Archive Index
10794
10979
 
10795
10980
  | change-id | status | date | notes |
10796
10981
  |---|---|---|---|
@@ -10814,15 +10999,15 @@ var list_changes_exports = {};
10814
10999
  __export(list_changes_exports, {
10815
11000
  listChanges: () => listChanges
10816
11001
  });
10817
- import { join as join25 } from "path";
10818
- import { existsSync as existsSync24, readdirSync as readdirSync13, readFileSync as readFileSync26 } from "fs";
11002
+ import { join as join27 } from "path";
11003
+ import { existsSync as existsSync25, readdirSync as readdirSync13, readFileSync as readFileSync28 } from "fs";
10819
11004
  import yaml6 from "js-yaml";
10820
11005
  async function listChanges() {
10821
11006
  const cwd = process.cwd();
10822
- const changesDir = join25(cwd, "specs", "changes");
11007
+ const changesDir = join27(cwd, "specs", "changes");
10823
11008
  log.blank();
10824
11009
  const active = [];
10825
- if (existsSync24(changesDir)) {
11010
+ if (existsSync25(changesDir)) {
10826
11011
  active.push(...readdirSync13(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name));
10827
11012
  }
10828
11013
  if (active.length === 0) {
@@ -10830,12 +11015,12 @@ async function listChanges() {
10830
11015
  } else {
10831
11016
  log.info("Active changes:");
10832
11017
  for (const id of active) {
10833
- const tasksPath = join25(changesDir, id, "tasks.yml");
11018
+ const tasksPath = join27(changesDir, id, "tasks.yml");
10834
11019
  let status = "in-progress";
10835
11020
  let pending = 0;
10836
- if (existsSync24(tasksPath)) {
11021
+ if (existsSync25(tasksPath)) {
10837
11022
  try {
10838
- const raw = readFileSync26(tasksPath, "utf8");
11023
+ const raw = readFileSync28(tasksPath, "utf8");
10839
11024
  const data = yaml6.load(raw);
10840
11025
  if (data?.status)
10841
11026
  status = data.status;
@@ -10867,8 +11052,8 @@ __export(context_exports, {
10867
11052
  rejectContextExpansion: () => rejectContextExpansion,
10868
11053
  requestContextExpansion: () => requestContextExpansion
10869
11054
  });
10870
- import { existsSync as existsSync25, readFileSync as readFileSync27, writeFileSync as writeFileSync13 } from "fs";
10871
- import { join as join26 } from "path";
11055
+ import { existsSync as existsSync26, readFileSync as readFileSync29, writeFileSync as writeFileSync15 } from "fs";
11056
+ import { join as join28 } from "path";
10872
11057
  import picomatch2 from "picomatch";
10873
11058
  function normalizePath(path) {
10874
11059
  return path.replace(/\\/g, "/").replace(/^\.\//, "").trim();
@@ -10883,18 +11068,18 @@ function validateRepoRelativePath(path) {
10883
11068
  return null;
10884
11069
  }
10885
11070
  function manifestPathFor(changeId) {
10886
- return join26(process.cwd(), "specs", "changes", changeId, "context-manifest.md");
11071
+ return join28(process.cwd(), "specs", "changes", changeId, "context-manifest.md");
10887
11072
  }
10888
11073
  function readManifest(changeId) {
10889
11074
  const manifestPath = manifestPathFor(changeId);
10890
- if (!existsSync25(manifestPath)) {
11075
+ if (!existsSync26(manifestPath)) {
10891
11076
  log.error(`context manifest not found: specs/changes/${changeId}/context-manifest.md`);
10892
11077
  process.exit(1);
10893
11078
  }
10894
- return readFileSync27(manifestPath, "utf8");
11079
+ return readFileSync29(manifestPath, "utf8");
10895
11080
  }
10896
11081
  function writeManifest(changeId, content) {
10897
- writeFileSync13(manifestPathFor(changeId), content.endsWith("\n") ? content : `${content}
11082
+ writeFileSync15(manifestPathFor(changeId), content.endsWith("\n") ? content : `${content}
10898
11083
  `, "utf8");
10899
11084
  }
10900
11085
  function sectionBody(content, heading) {
@@ -10933,11 +11118,11 @@ function pathMatches(relPath, patterns, currentChangeId) {
10933
11118
  });
10934
11119
  }
10935
11120
  function loadContextPolicy() {
10936
- const policyPath = join26(process.cwd(), ".cdd", "context-policy.json");
10937
- if (!existsSync25(policyPath))
11121
+ const policyPath = join28(process.cwd(), ".cdd", "context-policy.json");
11122
+ if (!existsSync26(policyPath))
10938
11123
  return { forbiddenPaths: DEFAULT_FORBIDDEN_PATHS };
10939
11124
  try {
10940
- const custom = JSON.parse(readFileSync27(policyPath, "utf8"));
11125
+ const custom = JSON.parse(readFileSync29(policyPath, "utf8"));
10941
11126
  return {
10942
11127
  forbiddenPaths: Array.from(/* @__PURE__ */ new Set([
10943
11128
  ...DEFAULT_FORBIDDEN_PATHS,
@@ -11219,9 +11404,10 @@ var init_context = __esm({
11219
11404
  });
11220
11405
 
11221
11406
  // src/cli/index.ts
11222
- import { readFileSync as readFileSync28 } from "fs";
11407
+ import { readFileSync as readFileSync30 } from "fs";
11408
+ import os from "os";
11223
11409
  import { fileURLToPath as fileURLToPath3 } from "url";
11224
- import { dirname as dirname7, join as join27 } from "path";
11410
+ import { dirname as dirname7, join as join29 } from "path";
11225
11411
  import { Command } from "commander";
11226
11412
 
11227
11413
  // src/commands/init.ts
@@ -12327,7 +12513,7 @@ async function installHooks() {
12327
12513
 
12328
12514
  // src/cli/index.ts
12329
12515
  var __dirname2 = dirname7(fileURLToPath3(import.meta.url));
12330
- var pkg = JSON.parse(readFileSync28(join27(__dirname2, "..", "..", "package.json"), "utf8"));
12516
+ var pkg = JSON.parse(readFileSync30(join29(__dirname2, "..", "..", "package.json"), "utf8"));
12331
12517
  var program = new Command();
12332
12518
  program.name("cdd-kit").description("Contract-Driven Delivery Kit CLI").version(pkg.version);
12333
12519
  program.command("init").description(
@@ -12388,11 +12574,24 @@ function collectRepeatable(val, acc) {
12388
12574
  acc.push(val);
12389
12575
  return acc;
12390
12576
  }
12391
- program.command("code-map [path]").description("Scan source files and emit a structural index at .cdd/code-map.yml").option("--out <path>", "Output YAML path", ".cdd/code-map.yml").option("--include <glob>", "Additional include glob (repeatable)", collectRepeatable, []).option("--exclude <glob>", "Additional exclude glob (repeatable)", collectRepeatable, []).option("--check", "Exit 1 if regenerating would change the file (no write)", false).option("--max-lines <n>", "Warn for files exceeding this line count (default 100000)", "100000").action(async (path, opts) => {
12577
+ function resolveWorkers(value) {
12578
+ if (value === void 0)
12579
+ return 0;
12580
+ const cpus = Math.max(1, (os.cpus()?.length ?? 2) - 1);
12581
+ if (value === true)
12582
+ return Math.min(cpus, 16);
12583
+ const n = parseInt(String(value), 10);
12584
+ if (!Number.isFinite(n) || n < 1)
12585
+ return 1;
12586
+ return Math.min(n, 16);
12587
+ }
12588
+ program.command("code-map [path]").description("Scan source files and emit a structural index at .cdd/code-map.yml").option("--out <path>", "Output YAML path (default .cdd/code-map.yml; with --surface, .cdd/code-map.<surface>.yml)").option("--surface <subpath>", "Scope the scan to a monorepo subtree and name the map after it").option("--workers [n]", "Parallelize JS/TS/Vue scanning across N child processes (default: CPU count - 1)").option("--include <glob>", "Additional include glob (repeatable)", collectRepeatable, []).option("--exclude <glob>", "Additional exclude glob (repeatable)", collectRepeatable, []).option("--check", "Exit 1 if regenerating would change the file (no write)", false).option("--max-lines <n>", "Warn for files exceeding this line count (default 100000)", "100000").action(async (path, opts) => {
12392
12589
  const { codeMap: codeMap2 } = await Promise.resolve().then(() => (init_code_map(), code_map_exports));
12393
12590
  const exit = await codeMap2({
12394
12591
  path: path ?? ".",
12395
12592
  out: opts.out,
12593
+ surface: opts.surface,
12594
+ workers: resolveWorkers(opts.workers),
12396
12595
  include: opts.include,
12397
12596
  exclude: opts.exclude,
12398
12597
  check: opts.check,
@@ -12400,6 +12599,11 @@ program.command("code-map [path]").description("Scan source files and emit a str
12400
12599
  });
12401
12600
  process.exit(exit);
12402
12601
  });
12602
+ program.command("__code-map-scan", { hidden: true }).requiredOption("--lang <lang>", "js | ts | vue").requiredOption("--batch-file <path>", "File listing absolute source paths, one per line").requiredOption("--repo-root <path>", "Repo root the scanned paths are relative to").action(async (opts) => {
12603
+ const { runScanWorker: runScanWorker2 } = await Promise.resolve().then(() => (init_code_map_scan_worker(), code_map_scan_worker_exports));
12604
+ const exit = await runScanWorker2({ lang: opts.lang, batchFile: opts.batchFile, repoRoot: opts.repoRoot });
12605
+ process.exit(exit);
12606
+ });
12403
12607
  var index = program.command("index").description("Query machine-readable project indexes before opening source files");
12404
12608
  index.command("query <term>").description("Search .cdd/code-map.yml for files, symbols, imports, and line ranges").option("--map <path>", "Code-map YAML path", ".cdd/code-map.yml").option("--limit <n>", "Maximum result files to print", "10").option("--json", "Print machine-readable JSON", false).option("--no-refresh", "Do not auto-regenerate stale or missing code-map before querying").action(async (term, opts) => {
12405
12609
  const { indexQuery: indexQuery2 } = await Promise.resolve().then(() => (init_index_query(), index_query_exports));