contract-driven-delivery 2.0.10 → 2.0.12

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/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.12] - 2026-05-04
4
+
5
+ Tiny patch — closes the last line-ending hole.
6
+
7
+ ### Fixed
8
+
9
+ - **`cdd-kit code-map --check` is now line-ending agnostic**: 2.0.11 fixed
10
+ the digest paths (`# sources-digest:`) to be portable, but `--check`'s
11
+ string-comparison fallback still saw CRLF (committed via
12
+ `core.autocrlf=true`) vs LF (always emitted) as different and reported
13
+ "would change". The pre-commit hook then regenerated the map on every
14
+ commit on Windows even when content was bit-identical. Fixed by
15
+ normalizing CRLF/CR → LF on both sides before comparison, matching the
16
+ digest functions' approach.
17
+
18
+ After upgrading, the hook stops triggering noisy regenerations on
19
+ Windows. No re-scan needed; this is purely a comparison fix.
20
+
21
+ ## [2.0.11] - 2026-05-04
22
+
23
+ Final portability fix in the digest series. After 2.0.10 made digests
24
+ repo-relative and content-keyed, a real consumer repo on Windows
25
+ (`core.autocrlf=true`) still produced different digests than the same
26
+ repo on Linux/Mac (`core.autocrlf=false`) — because the file BYTES
27
+ differ even when the file content is logically identical.
28
+
29
+ ### Fixed
30
+
31
+ - **All hash inputs are now line-ending normalized**. `\r\n` and stand-alone
32
+ `\r` are converted to `\n` before SHA-256 is computed. Applied uniformly
33
+ across the four places that hash files for cdd-kit's digests:
34
+ - `inputsDigest()` in `src/commands/context-scan.ts`
35
+ (project-map / contracts-index)
36
+ - `inputDigest()` in `src/commands/doctor.ts`
37
+ (freshness check against committed indexes)
38
+ - `inputsDigest()` in `src/commands/new-change.ts`
39
+ (auto-rerun decision in /cdd-new flow)
40
+ - `computeSourcesDigest()` in `src/commands/code-map.ts`
41
+ (`# sources-digest:` header in code-map.yml)
42
+
43
+ All four now share `src/utils/digest.ts → sha256OfFileNormalized()`,
44
+ so the rule is in exactly one place.
45
+
46
+ ### Migration
47
+
48
+ After upgrading, re-run **once**:
49
+
50
+ ```bash
51
+ cdd-kit context-scan
52
+ cdd-kit code-map
53
+ git add specs/context/ .cdd/code-map.yml
54
+ git commit -m "chore: regenerate indexes & code-map (cdd-kit 2.0.11)"
55
+ ```
56
+
57
+ From then on, fresh clones on any OS / autocrlf setting produce identical
58
+ digests, eliminating the last source of false-positive doctor warnings.
59
+
3
60
  ## [2.0.10] - 2026-05-04
4
61
 
5
62
  Two more context-scan determinism bugs, both surfaced verifying the 2.0.9
package/dist/cli/index.js CHANGED
@@ -376,27 +376,44 @@ var init_update = __esm({
376
376
  }
377
377
  });
378
378
 
379
- // src/commands/context-scan.ts
380
- var context_scan_exports = {};
381
- __export(context_scan_exports, {
382
- contextScan: () => contextScan
383
- });
384
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, readdirSync as readdirSync4, writeFileSync as writeFileSync3 } from "fs";
379
+ // src/utils/digest.ts
380
+ import { readFileSync as readFileSync6 } from "fs";
385
381
  import { createHash as createHash2 } from "crypto";
386
- import { basename, dirname as dirname3, join as join8, relative as relative2 } from "path";
387
- function sha256OfFile(path) {
382
+ function normalizeContentForHash(buf) {
383
+ if (!buf.includes(13))
384
+ return buf;
385
+ const text = buf.toString("utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
386
+ return Buffer.from(text, "utf8");
387
+ }
388
+ function sha256OfFileNormalized(path) {
389
+ let buf;
388
390
  try {
389
- return createHash2("sha256").update(readFileSync6(path)).digest("hex");
391
+ buf = readFileSync6(path);
390
392
  } catch {
391
393
  return "";
392
394
  }
395
+ return createHash2("sha256").update(normalizeContentForHash(buf)).digest("hex");
393
396
  }
397
+ var init_digest = __esm({
398
+ "src/utils/digest.ts"() {
399
+ "use strict";
400
+ }
401
+ });
402
+
403
+ // src/commands/context-scan.ts
404
+ var context_scan_exports = {};
405
+ __export(context_scan_exports, {
406
+ contextScan: () => contextScan
407
+ });
408
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync7, readdirSync as readdirSync4, writeFileSync as writeFileSync3 } from "fs";
409
+ import { createHash as createHash3 } from "crypto";
410
+ import { basename, dirname as dirname3, join as join8, relative as relative2 } from "path";
394
411
  function inputsDigest(paths, cwd) {
395
412
  const combined = paths.slice().sort().map((p) => {
396
413
  const rel = relative2(cwd, p).replace(/\\/g, "/");
397
- return `${rel}:${sha256OfFile(p)}`;
414
+ return `${rel}:${sha256OfFileNormalized(p)}`;
398
415
  }).join("\n");
399
- return createHash2("sha256").update(combined).digest("hex");
416
+ return createHash3("sha256").update(combined).digest("hex");
400
417
  }
401
418
  function stripGlobSuffix(pattern) {
402
419
  return pattern.replace(/\/\*\*$/, "").replace(/\/\*$/, "");
@@ -406,7 +423,7 @@ function getForbiddenPaths(cwd) {
406
423
  const policyPath = join8(cwd, ".cdd", "context-policy.json");
407
424
  try {
408
425
  if (existsSync7(policyPath)) {
409
- const policy = JSON.parse(readFileSync6(policyPath, "utf8"));
426
+ const policy = JSON.parse(readFileSync7(policyPath, "utf8"));
410
427
  for (const pattern of policy.forbiddenPaths ?? []) {
411
428
  forbidden.add(stripGlobSuffix(pattern));
412
429
  }
@@ -580,7 +597,7 @@ async function contextScan(opts = {}) {
580
597
  for (const file of contractFiles) {
581
598
  const relPath = relative2(cwd, file).replace(/\\/g, "/");
582
599
  const dir = dirname3(relPath).replace(/\\/g, "/");
583
- const { title, summary, metadata } = parseContractMetadata(readFileSync6(file, "utf8"));
600
+ const { title, summary, metadata } = parseContractMetadata(readFileSync7(file, "utf8"));
584
601
  const contractType = deriveContractType(relPath, metadata);
585
602
  const owner = metadata.owner ?? "unknown";
586
603
  const surface2 = metadata.surface ?? dir;
@@ -647,6 +664,7 @@ var init_context_scan = __esm({
647
664
  "src/commands/context-scan.ts"() {
648
665
  "use strict";
649
666
  init_logger();
667
+ init_digest();
650
668
  DEFAULT_FORBIDDEN = [
651
669
  ".claude",
652
670
  ".git",
@@ -7482,7 +7500,7 @@ var require_dist = __commonJS({
7482
7500
  });
7483
7501
 
7484
7502
  // src/code-map/config.ts
7485
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
7503
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
7486
7504
  import { join as join11 } from "path";
7487
7505
  import { load as yamlLoad } from "js-yaml";
7488
7506
  function asStringArray(value, key, where) {
@@ -7509,7 +7527,7 @@ function loadCodeMapConfig(cwd) {
7509
7527
  }
7510
7528
  let text;
7511
7529
  try {
7512
- text = readFileSync8(filePath, "utf8");
7530
+ text = readFileSync9(filePath, "utf8");
7513
7531
  } catch (err) {
7514
7532
  throw new Error(`failed to read ${CONFIG_REL_PATH}: ${err.message}`);
7515
7533
  }
@@ -7993,7 +8011,7 @@ __export(javascript_exports, {
7993
8011
  parseJsSource: () => parseJsSource,
7994
8012
  parseSourceWithPlugins: () => parseSourceWithPlugins
7995
8013
  });
7996
- import { readFileSync as readFileSync10 } from "fs";
8014
+ import { readFileSync as readFileSync11 } from "fs";
7997
8015
  import { parse } from "@babel/parser";
7998
8016
  function parseSourceWithPlugins(source, plugins) {
7999
8017
  return parse(source, {
@@ -8295,7 +8313,7 @@ var init_javascript = __esm({
8295
8313
  async scan(absolutePath, repoRoot) {
8296
8314
  let source;
8297
8315
  try {
8298
- source = readFileSync10(absolutePath, "utf8");
8316
+ source = readFileSync11(absolutePath, "utf8");
8299
8317
  } catch (err) {
8300
8318
  throw err;
8301
8319
  }
@@ -8315,7 +8333,7 @@ var typescript_exports = {};
8315
8333
  __export(typescript_exports, {
8316
8334
  tsScanner: () => tsScanner
8317
8335
  });
8318
- import { readFileSync as readFileSync11 } from "fs";
8336
+ import { readFileSync as readFileSync12 } from "fs";
8319
8337
  var TypeScriptScanner, tsScanner;
8320
8338
  var init_typescript = __esm({
8321
8339
  "src/code-map/scanners/typescript.ts"() {
@@ -8327,7 +8345,7 @@ var init_typescript = __esm({
8327
8345
  async scan(absolutePath, repoRoot) {
8328
8346
  let source;
8329
8347
  try {
8330
- source = readFileSync11(absolutePath, "utf8");
8348
+ source = readFileSync12(absolutePath, "utf8");
8331
8349
  } catch (err) {
8332
8350
  throw err;
8333
8351
  }
@@ -8363,7 +8381,7 @@ var vue_exports = {};
8363
8381
  __export(vue_exports, {
8364
8382
  vueScanner: () => vueScanner
8365
8383
  });
8366
- import { readFileSync as readFileSync12 } from "fs";
8384
+ import { readFileSync as readFileSync13 } from "fs";
8367
8385
  import { parse as parse2 } from "@vue/compiler-sfc";
8368
8386
  var VueScanner, vueScanner;
8369
8387
  var init_vue = __esm({
@@ -8376,7 +8394,7 @@ var init_vue = __esm({
8376
8394
  async scan(absolutePath, repoRoot) {
8377
8395
  let source;
8378
8396
  try {
8379
- source = readFileSync12(absolutePath, "utf8");
8397
+ source = readFileSync13(absolutePath, "utf8");
8380
8398
  } catch (err) {
8381
8399
  throw err;
8382
8400
  }
@@ -8463,24 +8481,19 @@ __export(code_map_exports, {
8463
8481
  codeMap: () => codeMap,
8464
8482
  computeSourcesDigest: () => computeSourcesDigest
8465
8483
  });
8466
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
8484
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
8467
8485
  import { resolve, dirname as dirname4, relative as relative5 } from "path";
8468
- import { createHash as createHash4 } from "crypto";
8486
+ import { createHash as createHash5 } from "crypto";
8469
8487
  import { createRequire } from "module";
8470
8488
  import { fileURLToPath as fileURLToPath2 } from "url";
8471
8489
  import { join as join14 } from "path";
8472
8490
  function computeSourcesDigest(absolutePaths, cwd) {
8473
8491
  const lines = absolutePaths.slice().sort().map((p) => {
8474
8492
  const rel = relative5(cwd, p).replace(/\\/g, "/");
8475
- let contentHash;
8476
- try {
8477
- contentHash = createHash4("sha256").update(readFileSync13(p)).digest("hex");
8478
- } catch {
8479
- contentHash = "missing";
8480
- }
8493
+ const contentHash = sha256OfFileNormalized(p) || "missing";
8481
8494
  return `${rel}:${contentHash}`;
8482
8495
  });
8483
- return createHash4("sha256").update(lines.join("\n")).digest("hex");
8496
+ return createHash5("sha256").update(lines.join("\n")).digest("hex");
8484
8497
  }
8485
8498
  async function codeMap(opts) {
8486
8499
  const root = resolve(process.cwd(), opts.path);
@@ -8552,8 +8565,8 @@ async function codeMap(opts) {
8552
8565
  log.warn(`${w.path}: ${w.message}`);
8553
8566
  }
8554
8567
  if (opts.check) {
8555
- const existing = existsSync11(opts.out) ? readFileSync13(opts.out, "utf8") : "";
8556
- const normalize = (s) => s.replace(/^# generated: [^\n]+\n/m, "# generated: <normalized>\n");
8568
+ const existing = existsSync11(opts.out) ? readFileSync14(opts.out, "utf8") : "";
8569
+ const normalize = (s) => s.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/^# generated: [^\n]+\n/m, "# generated: <normalized>\n");
8557
8570
  if (normalize(existing) !== normalize(yamlBody)) {
8558
8571
  log.error(`code-map out of date: ${opts.out} would change. Run \`cdd-kit code-map\` to regenerate.`);
8559
8572
  return 1;
@@ -8574,9 +8587,10 @@ var init_code_map = __esm({
8574
8587
  init_yaml_writer();
8575
8588
  init_orchestrator();
8576
8589
  init_config();
8590
+ init_digest();
8577
8591
  _require = createRequire(import.meta.url);
8578
8592
  _pkgPath = join14(fileURLToPath2(import.meta.url), "..", "..", "..", "package.json");
8579
- _pkg = JSON.parse(readFileSync13(_pkgPath, "utf8"));
8593
+ _pkg = JSON.parse(readFileSync14(_pkgPath, "utf8"));
8580
8594
  }
8581
8595
  });
8582
8596
 
@@ -8585,7 +8599,7 @@ var freshness_exports = {};
8585
8599
  __export(freshness_exports, {
8586
8600
  checkCodeMapFreshness: () => checkCodeMapFreshness
8587
8601
  });
8588
- import { existsSync as existsSync12, readFileSync as readFileSync14, statSync as statSync3 } from "fs";
8602
+ import { existsSync as existsSync12, readFileSync as readFileSync15, statSync as statSync3 } from "fs";
8589
8603
  import { join as join15 } from "path";
8590
8604
  function checkCodeMapFreshness(cwd, mapRel = ".cdd/code-map.yml", include, exclude) {
8591
8605
  const mapPath = join15(cwd, mapRel);
@@ -8641,7 +8655,7 @@ function checkCodeMapFreshness(cwd, mapRel = ".cdd/code-map.yml", include, exclu
8641
8655
  }
8642
8656
  function readSourcesDigest(mapPath) {
8643
8657
  try {
8644
- const head = readFileSync14(mapPath, "utf8").slice(0, 2048);
8658
+ const head = readFileSync15(mapPath, "utf8").slice(0, 2048);
8645
8659
  const m = head.match(/^# sources-digest:\s*([a-f0-9]+)/m);
8646
8660
  return m ? m[1] : null;
8647
8661
  } catch {
@@ -8663,7 +8677,7 @@ __export(migrate_exports, {
8663
8677
  migrate: () => migrate
8664
8678
  });
8665
8679
  import { join as join18 } from "path";
8666
- import { cpSync as cpSync2, existsSync as existsSync15, mkdirSync as mkdirSync7, readdirSync as readdirSync8, readFileSync as readFileSync17, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "fs";
8680
+ import { cpSync as cpSync2, existsSync as existsSync15, mkdirSync as mkdirSync7, readdirSync as readdirSync8, readFileSync as readFileSync18, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "fs";
8667
8681
  import yaml3 from "js-yaml";
8668
8682
  function backupChangeDir(cwd, changeId, sessionStamp) {
8669
8683
  const backupRoot = join18(cwd, ".cdd", "migrate-backup", sessionStamp);
@@ -8812,7 +8826,7 @@ function migrateTasksFile(changeId, changeDir, enableContextGovernance, detected
8812
8826
  warnings.push("tasks.md not found and tasks.yml missing \u2014 skipping tasks migration");
8813
8827
  return;
8814
8828
  }
8815
- const raw = readFileSync17(legacyPath, "utf8");
8829
+ const raw = readFileSync18(legacyPath, "utf8");
8816
8830
  const fm = parseLegacyFrontmatter(raw);
8817
8831
  const bodyMatch = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);
8818
8832
  const body = bodyMatch ? bodyMatch[1] : raw;
@@ -8906,7 +8920,7 @@ function migrateAgentLogs(changeDir, changed, pendingWrites, pendingDeletes) {
8906
8920
  const yamlFull = join18(agentLogDir, yamlName);
8907
8921
  if (existsSync15(yamlFull))
8908
8922
  continue;
8909
- const raw = readFileSync17(fullPath, "utf8");
8923
+ const raw = readFileSync18(fullPath, "utf8");
8910
8924
  const parsed = parseLegacyAgentLog(raw);
8911
8925
  const yamlOut = yaml3.dump(parsed, { lineWidth: -1, noRefs: true });
8912
8926
  pendingWrites.push({ path: yamlFull, content: yamlOut });
@@ -8922,7 +8936,7 @@ function migrateOne(changeId, changeDir, enableContextGovernance) {
8922
8936
  let detectedTier = null;
8923
8937
  const classifPath = join18(changeDir, "change-classification.md");
8924
8938
  if (existsSync15(classifPath)) {
8925
- const content = readFileSync17(classifPath, "utf8");
8939
+ const content = readFileSync18(classifPath, "utf8");
8926
8940
  const hasNewTierFormat = /^## Tier\s*\n\s*-\s*\d\s*$/m.test(content);
8927
8941
  const oldMatch = content.match(/\*\*Tier[:\*]+\s*(?:Tier\s*)?(\d)/i) ?? content.match(/^-?\s*Tier:\s*(?:Tier\s*)?(\d)/mi);
8928
8942
  if (oldMatch)
@@ -9087,7 +9101,7 @@ function ensureGitignoreEntry(cwd, entry) {
9087
9101
  const re = new RegExp(`^\\s*${trimmed.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\s*$`, "m");
9088
9102
  let existing = "";
9089
9103
  if (existsSync15(path))
9090
- existing = readFileSync17(path, "utf8");
9104
+ existing = readFileSync18(path, "utf8");
9091
9105
  if (re.test(existing))
9092
9106
  return false;
9093
9107
  const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
@@ -9112,7 +9126,7 @@ var upgrade_exports = {};
9112
9126
  __export(upgrade_exports, {
9113
9127
  upgrade: () => upgrade
9114
9128
  });
9115
- import { existsSync as existsSync16, mkdirSync as mkdirSync8, readdirSync as readdirSync9, copyFileSync as copyFileSync3, readFileSync as readFileSync18, writeFileSync as writeFileSync9 } from "fs";
9129
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, readdirSync as readdirSync9, copyFileSync as copyFileSync3, readFileSync as readFileSync19, writeFileSync as writeFileSync9 } from "fs";
9116
9130
  import { dirname as dirname5, join as join19, relative as relative6 } from "path";
9117
9131
  function planMissingFiles(srcDir, destDir, label, planned) {
9118
9132
  if (!existsSync16(srcDir))
@@ -9203,7 +9217,7 @@ async function upgrade(opts = {}) {
9203
9217
  if (existsSync16(modelPolicyPath)) {
9204
9218
  let existing = {};
9205
9219
  try {
9206
- existing = JSON.parse(readFileSync18(modelPolicyPath, "utf8"));
9220
+ existing = JSON.parse(readFileSync19(modelPolicyPath, "utf8"));
9207
9221
  } catch {
9208
9222
  }
9209
9223
  const merged = {
@@ -9243,11 +9257,11 @@ var refresh_exports = {};
9243
9257
  __export(refresh_exports, {
9244
9258
  refresh: () => refresh
9245
9259
  });
9246
- import { existsSync as existsSync17, mkdirSync as mkdirSync9, readdirSync as readdirSync10, copyFileSync as copyFileSync4, readFileSync as readFileSync19, writeFileSync as writeFileSync10 } from "fs";
9260
+ import { existsSync as existsSync17, mkdirSync as mkdirSync9, readdirSync as readdirSync10, copyFileSync as copyFileSync4, readFileSync as readFileSync20, writeFileSync as writeFileSync10 } from "fs";
9247
9261
  import { dirname as dirname6, join as join20, relative as relative7 } from "path";
9248
- import { createHash as createHash5 } from "crypto";
9262
+ import { createHash as createHash6 } from "crypto";
9249
9263
  function fileHash2(filePath) {
9250
- return createHash5("sha256").update(readFileSync19(filePath)).digest("hex");
9264
+ return createHash6("sha256").update(readFileSync20(filePath)).digest("hex");
9251
9265
  }
9252
9266
  function planForceRefresh(srcDir, destDir, sectionLabel) {
9253
9267
  const plan = [];
@@ -9299,7 +9313,7 @@ function ensureGitignoreEntry2(cwd, entry) {
9299
9313
  const re = new RegExp(`^\\s*${trimmed.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\s*$`, "m");
9300
9314
  let existing = "";
9301
9315
  if (existsSync17(path))
9302
- existing = readFileSync19(path, "utf8");
9316
+ existing = readFileSync20(path, "utf8");
9303
9317
  if (re.test(existing))
9304
9318
  return false;
9305
9319
  const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
@@ -9355,7 +9369,7 @@ function resyncModelPolicy(cwd) {
9355
9369
  const desired = {};
9356
9370
  const agentFiles = readdirSync10(AGENTS_HOME, { withFileTypes: true }).filter((d) => d.isFile() && d.name.endsWith(".md"));
9357
9371
  for (const f of agentFiles) {
9358
- const content = readFileSync19(join20(AGENTS_HOME, f.name), "utf8");
9372
+ const content = readFileSync20(join20(AGENTS_HOME, f.name), "utf8");
9359
9373
  const fm = parseAgentFrontmatter(content);
9360
9374
  if (fm.name && fm.model)
9361
9375
  desired[fm.name] = fm.model;
@@ -9365,7 +9379,7 @@ function resyncModelPolicy(cwd) {
9365
9379
  let existing = {};
9366
9380
  if (existsSync17(policyPath)) {
9367
9381
  try {
9368
- existing = JSON.parse(readFileSync19(policyPath, "utf8"));
9382
+ existing = JSON.parse(readFileSync20(policyPath, "utf8"));
9369
9383
  } catch {
9370
9384
  }
9371
9385
  }
@@ -9574,8 +9588,8 @@ var doctor_exports = {};
9574
9588
  __export(doctor_exports, {
9575
9589
  doctor: () => doctor
9576
9590
  });
9577
- import { existsSync as existsSync18, readdirSync as readdirSync11, readFileSync as readFileSync20 } from "fs";
9578
- import { createHash as createHash6 } from "crypto";
9591
+ import { existsSync as existsSync18, readdirSync as readdirSync11, readFileSync as readFileSync21 } from "fs";
9592
+ import { createHash as createHash7 } from "crypto";
9579
9593
  import { join as join21, relative as relative8 } from "path";
9580
9594
  function fileExists(cwd, relPath) {
9581
9595
  return existsSync18(join21(cwd, relPath));
@@ -9592,24 +9606,17 @@ function findFiles(dir, predicate, found = []) {
9592
9606
  }
9593
9607
  return found;
9594
9608
  }
9595
- function sha256OfFile3(path) {
9596
- try {
9597
- return createHash6("sha256").update(readFileSync20(path)).digest("hex");
9598
- } catch {
9599
- return "";
9600
- }
9601
- }
9602
9609
  function inputDigest(paths, cwd) {
9603
9610
  const combined = paths.slice().sort().map((p) => {
9604
9611
  const rel = relative8(cwd, p).replace(/\\/g, "/");
9605
- return `${rel}:${sha256OfFile3(p)}`;
9612
+ return `${rel}:${sha256OfFileNormalized(p)}`;
9606
9613
  }).join("\n");
9607
- return createHash6("sha256").update(combined).digest("hex");
9614
+ return createHash7("sha256").update(combined).digest("hex");
9608
9615
  }
9609
9616
  function readContextIndexMetadata(filePath) {
9610
9617
  if (!existsSync18(filePath))
9611
9618
  return {};
9612
- const text = readFileSync20(filePath, "utf8");
9619
+ const text = readFileSync21(filePath, "utf8");
9613
9620
  const out = {};
9614
9621
  const digestMatch = text.match(/^inputs-digest:\s*([a-f0-9]+)/m);
9615
9622
  if (digestMatch)
@@ -9674,7 +9681,7 @@ function checkContextFreshness(cwd) {
9674
9681
  }
9675
9682
  function readAgentModel(path) {
9676
9683
  try {
9677
- const text = readFileSync20(path, "utf8");
9684
+ const text = readFileSync21(path, "utf8");
9678
9685
  const m = text.match(/^model:\s*(\S+)/m);
9679
9686
  return m ? m[1] : null;
9680
9687
  } catch {
@@ -9687,7 +9694,7 @@ function checkModelPolicyDrift(cwd) {
9687
9694
  return [];
9688
9695
  let policy;
9689
9696
  try {
9690
- policy = JSON.parse(readFileSync20(policyPath, "utf8"));
9697
+ policy = JSON.parse(readFileSync21(policyPath, "utf8"));
9691
9698
  } catch {
9692
9699
  return [{ level: "warning", message: ".cdd/model-policy.json is not valid JSON" }];
9693
9700
  }
@@ -9788,7 +9795,7 @@ function checkCodeMap(cwd) {
9788
9795
  const more = probe.staleCount > 3 ? ` (+${probe.staleCount - 3} more)` : "";
9789
9796
  findings.push({ level: "warning", message: `code-map stale: ${top}${more}; run \`cdd-kit code-map\`` });
9790
9797
  }
9791
- const text = readFileSync20(mapPath, "utf8");
9798
+ const text = readFileSync21(mapPath, "utf8");
9792
9799
  const m = text.match(/^# files: (\d+), src-lines: (\d+), map-lines: (\d+), compression: ([\d.]+)x/m);
9793
9800
  if (m) {
9794
9801
  findings.push({ level: "ok", message: `code-map: ${m[1]} files, ${m[4]}x compression` });
@@ -9868,7 +9875,7 @@ async function attemptAutoFixes(cwd, report) {
9868
9875
  try {
9869
9876
  let existing = {};
9870
9877
  try {
9871
- existing = JSON.parse(readFileSync20(policyPath, "utf8"));
9878
+ existing = JSON.parse(readFileSync21(policyPath, "utf8"));
9872
9879
  } catch {
9873
9880
  }
9874
9881
  const merged = {
@@ -9961,6 +9968,7 @@ var init_doctor = __esm({
9961
9968
  init_logger();
9962
9969
  init_provider();
9963
9970
  init_freshness();
9971
+ init_digest();
9964
9972
  }
9965
9973
  });
9966
9974
 
@@ -9969,7 +9977,7 @@ var lint_agents_exports = {};
9969
9977
  __export(lint_agents_exports, {
9970
9978
  lintAgents: () => lintAgents
9971
9979
  });
9972
- import { readdirSync as readdirSync12, readFileSync as readFileSync21 } from "fs";
9980
+ import { readdirSync as readdirSync12, readFileSync as readFileSync22 } from "fs";
9973
9981
  import { join as join22 } from "path";
9974
9982
  import { load as yamlLoad2 } from "js-yaml";
9975
9983
  function extractRequiredArtifactsSection(content) {
@@ -10024,7 +10032,7 @@ async function lintAgents(opts) {
10024
10032
  const filePath = join22(agentsDir, filename);
10025
10033
  let content;
10026
10034
  try {
10027
- content = readFileSync21(filePath, "utf8");
10035
+ content = readFileSync22(filePath, "utf8");
10028
10036
  } catch {
10029
10037
  violations.push({
10030
10038
  file: filename,
@@ -10137,7 +10145,7 @@ __export(archive_exports, {
10137
10145
  archive: () => archive
10138
10146
  });
10139
10147
  import { join as join23 } from "path";
10140
- import { existsSync as existsSync19, mkdirSync as mkdirSync10, renameSync as renameSync2, readFileSync as readFileSync22, writeFileSync as writeFileSync11, appendFileSync, cpSync as cpSync3, rmSync as rmSync3 } from "fs";
10148
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, renameSync as renameSync2, readFileSync as readFileSync23, writeFileSync as writeFileSync11, appendFileSync, cpSync as cpSync3, rmSync as rmSync3 } from "fs";
10141
10149
  import yaml4 from "js-yaml";
10142
10150
  async function archive(changeId) {
10143
10151
  const cwd = process.cwd();
@@ -10157,7 +10165,7 @@ async function archive(changeId) {
10157
10165
  const tasksPath = join23(changeDir, "tasks.yml");
10158
10166
  if (existsSync19(tasksPath)) {
10159
10167
  try {
10160
- const raw = readFileSync22(tasksPath, "utf8");
10168
+ const raw = readFileSync23(tasksPath, "utf8");
10161
10169
  const data = yaml4.load(raw);
10162
10170
  if (data?.status === "gate-blocked") {
10163
10171
  log.warn("tasks.yml has status: gate-blocked \u2014 archiving anyway (change was paused).");
@@ -10213,7 +10221,7 @@ __export(abandon_exports, {
10213
10221
  abandon: () => abandon
10214
10222
  });
10215
10223
  import { join as join24 } from "path";
10216
- import { existsSync as existsSync20, readFileSync as readFileSync23, writeFileSync as writeFileSync12, appendFileSync as appendFileSync2, mkdirSync as mkdirSync11 } from "fs";
10224
+ import { existsSync as existsSync20, readFileSync as readFileSync24, writeFileSync as writeFileSync12, appendFileSync as appendFileSync2, mkdirSync as mkdirSync11 } from "fs";
10217
10225
  import yaml5 from "js-yaml";
10218
10226
  async function abandon(changeId, opts) {
10219
10227
  const cwd = process.cwd();
@@ -10224,7 +10232,7 @@ async function abandon(changeId, opts) {
10224
10232
  process.exit(1);
10225
10233
  }
10226
10234
  if (existsSync20(tasksPath)) {
10227
- const raw = readFileSync23(tasksPath, "utf8");
10235
+ const raw = readFileSync24(tasksPath, "utf8");
10228
10236
  const data = yaml5.load(raw) ?? {};
10229
10237
  data["status"] = "abandoned";
10230
10238
  if (!data["change-id"]) {
@@ -10267,7 +10275,7 @@ __export(list_changes_exports, {
10267
10275
  listChanges: () => listChanges
10268
10276
  });
10269
10277
  import { join as join25 } from "path";
10270
- import { existsSync as existsSync21, readdirSync as readdirSync13, readFileSync as readFileSync24 } from "fs";
10278
+ import { existsSync as existsSync21, readdirSync as readdirSync13, readFileSync as readFileSync25 } from "fs";
10271
10279
  import yaml6 from "js-yaml";
10272
10280
  async function listChanges() {
10273
10281
  const cwd = process.cwd();
@@ -10287,7 +10295,7 @@ async function listChanges() {
10287
10295
  let pending = 0;
10288
10296
  if (existsSync21(tasksPath)) {
10289
10297
  try {
10290
- const raw = readFileSync24(tasksPath, "utf8");
10298
+ const raw = readFileSync25(tasksPath, "utf8");
10291
10299
  const data = yaml6.load(raw);
10292
10300
  if (data?.status)
10293
10301
  status = data.status;
@@ -10318,7 +10326,7 @@ __export(context_exports, {
10318
10326
  rejectContextExpansion: () => rejectContextExpansion,
10319
10327
  requestContextExpansion: () => requestContextExpansion
10320
10328
  });
10321
- import { existsSync as existsSync22, readFileSync as readFileSync25, writeFileSync as writeFileSync13 } from "fs";
10329
+ import { existsSync as existsSync22, readFileSync as readFileSync26, writeFileSync as writeFileSync13 } from "fs";
10322
10330
  import { join as join26 } from "path";
10323
10331
  function normalizePath(path) {
10324
10332
  return path.replace(/\\/g, "/").replace(/^\.\//, "").trim();
@@ -10341,7 +10349,7 @@ function readManifest(changeId) {
10341
10349
  log.error(`context manifest not found: specs/changes/${changeId}/context-manifest.md`);
10342
10350
  process.exit(1);
10343
10351
  }
10344
- return readFileSync25(manifestPath, "utf8");
10352
+ return readFileSync26(manifestPath, "utf8");
10345
10353
  }
10346
10354
  function writeManifest(changeId, content) {
10347
10355
  writeFileSync13(manifestPathFor(changeId), content.endsWith("\n") ? content : `${content}
@@ -10569,7 +10577,7 @@ var init_context = __esm({
10569
10577
  });
10570
10578
 
10571
10579
  // src/cli/index.ts
10572
- import { readFileSync as readFileSync26 } from "fs";
10580
+ import { readFileSync as readFileSync27 } from "fs";
10573
10581
  import { fileURLToPath as fileURLToPath3 } from "url";
10574
10582
  import { dirname as dirname7, join as join27 } from "path";
10575
10583
  import { Command } from "commander";
@@ -10992,24 +11000,18 @@ init_update();
10992
11000
  // src/commands/new-change.ts
10993
11001
  init_paths();
10994
11002
  import { join as join9, relative as relative3 } from "path";
10995
- import { createHash as createHash3 } from "crypto";
10996
- import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "fs";
11003
+ import { createHash as createHash4 } from "crypto";
11004
+ import { existsSync as existsSync8, readFileSync as readFileSync8, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "fs";
10997
11005
  import yaml from "js-yaml";
10998
11006
  init_logger();
10999
11007
  init_context_scan();
11000
- function sha256OfFile2(path) {
11001
- try {
11002
- return createHash3("sha256").update(readFileSync7(path)).digest("hex");
11003
- } catch {
11004
- return "";
11005
- }
11006
- }
11008
+ init_digest();
11007
11009
  function inputsDigest2(paths, cwd) {
11008
11010
  const combined = paths.slice().sort().map((p) => {
11009
11011
  const rel = relative3(cwd, p).replace(/\\/g, "/");
11010
- return `${rel}:${sha256OfFile2(p)}`;
11012
+ return `${rel}:${sha256OfFileNormalized(p)}`;
11011
11013
  }).join("\n");
11012
- return createHash3("sha256").update(combined).digest("hex");
11014
+ return createHash4("sha256").update(combined).digest("hex");
11013
11015
  }
11014
11016
  function findContractFiles2(dir, found = []) {
11015
11017
  if (!existsSync8(dir))
@@ -11027,7 +11029,7 @@ function findContractFiles2(dir, found = []) {
11027
11029
  function readIndexDigest(filePath) {
11028
11030
  if (!existsSync8(filePath))
11029
11031
  return null;
11030
- const m = readFileSync7(filePath, "utf8").match(/^inputs-digest:\s*([a-f0-9]+)/m);
11032
+ const m = readFileSync8(filePath, "utf8").match(/^inputs-digest:\s*([a-f0-9]+)/m);
11031
11033
  return m ? m[1] : null;
11032
11034
  }
11033
11035
  async function ensureFreshContextIndexes(cwd) {
@@ -11119,7 +11121,7 @@ async function newChange(name, opts) {
11119
11121
  if (dependencies.length > 0) {
11120
11122
  const tasksPath = join9(changeDir, "tasks.yml");
11121
11123
  if (existsSync8(tasksPath)) {
11122
- const raw = readFileSync7(tasksPath, "utf8");
11124
+ const raw = readFileSync8(tasksPath, "utf8");
11123
11125
  const data = yaml.load(raw) ?? {};
11124
11126
  data["depends-on"] = dependencies;
11125
11127
  writeFileSync4(tasksPath, yaml.dump(data, { lineWidth: -1, noRefs: true }), "utf8");
@@ -11223,7 +11225,7 @@ async function validate(opts) {
11223
11225
  var import_ajv = __toESM(require_ajv(), 1);
11224
11226
  var import_ajv_formats = __toESM(require_dist(), 1);
11225
11227
  init_logger();
11226
- import { existsSync as existsSync13, readFileSync as readFileSync15, readdirSync as readdirSync7 } from "fs";
11228
+ import { existsSync as existsSync13, readFileSync as readFileSync16, readdirSync as readdirSync7 } from "fs";
11227
11229
  import { homedir as homedir3 } from "os";
11228
11230
  import { join as join16 } from "path";
11229
11231
  import yaml2 from "js-yaml";
@@ -11411,7 +11413,7 @@ function extractRequiredArtifactTypes(cwd, agentName) {
11411
11413
  if (!existsSync13(candidate))
11412
11414
  continue;
11413
11415
  try {
11414
- content = readFileSync15(candidate, "utf8");
11416
+ content = readFileSync16(candidate, "utf8");
11415
11417
  break;
11416
11418
  } catch {
11417
11419
  }
@@ -11452,7 +11454,7 @@ function loadContextPolicy(cwd) {
11452
11454
  if (!existsSync13(policyPath))
11453
11455
  return defaults;
11454
11456
  try {
11455
- const custom = JSON.parse(readFileSync15(policyPath, "utf8"));
11457
+ const custom = JSON.parse(readFileSync16(policyPath, "utf8"));
11456
11458
  return {
11457
11459
  ...defaults,
11458
11460
  ...custom,
@@ -11466,7 +11468,7 @@ function loadContextPolicy(cwd) {
11466
11468
  }
11467
11469
  function loadYamlFile(path) {
11468
11470
  try {
11469
- const raw = readFileSync15(path, "utf8");
11471
+ const raw = readFileSync16(path, "utf8");
11470
11472
  return { data: yaml2.load(raw), parseError: null };
11471
11473
  } catch (err) {
11472
11474
  return { data: null, parseError: err.message };
@@ -11528,7 +11530,7 @@ function lintTasksFile(tasksPath, errors, warnings) {
11528
11530
  function resolveTier(changeDir) {
11529
11531
  const classifPath = join16(changeDir, "change-classification.md");
11530
11532
  const classificationPresent = existsSync13(classifPath);
11531
- const classificationText = classificationPresent ? readFileSync15(classifPath, "utf8") : "";
11533
+ const classificationText = classificationPresent ? readFileSync16(classifPath, "utf8") : "";
11532
11534
  const classificationHasLooseMarker = classificationPresent && TIER_PATTERN.test(classificationText);
11533
11535
  const tasksPath = join16(changeDir, "tasks.yml");
11534
11536
  if (existsSync13(tasksPath)) {
@@ -11593,7 +11595,7 @@ function enforceTierRequirements(changeDir, agentLogDir, errors, warnings) {
11593
11595
  }
11594
11596
  }
11595
11597
  if (resolution.source === "tasks-frontmatter" && resolution.classificationPresent) {
11596
- const text = readFileSync15(join16(changeDir, "change-classification.md"), "utf8");
11598
+ const text = readFileSync16(join16(changeDir, "change-classification.md"), "utf8");
11597
11599
  const structured = text.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
11598
11600
  const bold = text.match(/\*\*Tier:\*\*\s*Tier\s*(\d)\b/i);
11599
11601
  const classifTier = structured ? parseInt(structured[1], 10) : bold ? parseInt(bold[1], 10) : NaN;
@@ -11710,7 +11712,7 @@ async function gate(changeId, opts = {}) {
11710
11712
  let approvedExpansions = [];
11711
11713
  errors.push(...validateDependencies(cwd, changeId, changeDir));
11712
11714
  if (hasManifest) {
11713
- const manifest = parseContextManifest(readFileSync15(manifestPath, "utf8"));
11715
+ const manifest = parseContextManifest(readFileSync16(manifestPath, "utf8"));
11714
11716
  allowedPaths = manifest.allowedPaths;
11715
11717
  approvedExpansions = manifest.approvedExpansions;
11716
11718
  if (manifest.pendingExpansions > 0) {
@@ -11738,7 +11740,7 @@ async function gate(changeId, opts = {}) {
11738
11740
  continue;
11739
11741
  if (f === "tasks.yml")
11740
11742
  continue;
11741
- const content = readFileSync15(join16(changeDir, f), "utf8");
11743
+ const content = readFileSync16(join16(changeDir, f), "utf8");
11742
11744
  const minChars = MIN_CHARS[f] ?? 100;
11743
11745
  if (meaningfulChars(content) < minChars) {
11744
11746
  errors.push(`${f}: appears to be a stub (< ${minChars} meaningful chars)`);
@@ -11840,7 +11842,7 @@ async function gate(changeId, opts = {}) {
11840
11842
  }
11841
11843
  const runtimeLog = join16(cwd, ".cdd", "runtime", `${changeId}-files-read.jsonl`);
11842
11844
  if (existsSync13(runtimeLog)) {
11843
- const runtimePaths = readFileSync15(runtimeLog, "utf8").split("\n").filter(Boolean).map((line) => {
11845
+ const runtimePaths = readFileSync16(runtimeLog, "utf8").split("\n").filter(Boolean).map((line) => {
11844
11846
  try {
11845
11847
  return JSON.parse(line).path;
11846
11848
  } catch {
@@ -11926,7 +11928,7 @@ async function gate(changeId, opts = {}) {
11926
11928
  // src/commands/install-hooks.ts
11927
11929
  init_paths();
11928
11930
  init_logger();
11929
- import { existsSync as existsSync14, readFileSync as readFileSync16, writeFileSync as writeFileSync7, chmodSync as chmodSync2, mkdirSync as mkdirSync6 } from "fs";
11931
+ import { existsSync as existsSync14, readFileSync as readFileSync17, writeFileSync as writeFileSync7, chmodSync as chmodSync2, mkdirSync as mkdirSync6 } from "fs";
11930
11932
  import { join as join17 } from "path";
11931
11933
  var START_MARKER2 = "# cdd-kit-managed-block-start";
11932
11934
  var END_MARKER2 = "# cdd-kit-managed-block-end";
@@ -11940,12 +11942,12 @@ async function installHooks() {
11940
11942
  const hooksDir = join17(gitDir, "hooks");
11941
11943
  mkdirSync6(hooksDir, { recursive: true });
11942
11944
  const dest = join17(hooksDir, "pre-commit");
11943
- const ourHook = readFileSync16(join17(ASSET.hooks, "pre-commit"), "utf8");
11945
+ const ourHook = readFileSync17(join17(ASSET.hooks, "pre-commit"), "utf8");
11944
11946
  let final;
11945
11947
  if (!existsSync14(dest)) {
11946
11948
  final = ourHook;
11947
11949
  } else {
11948
- const existing = readFileSync16(dest, "utf8");
11950
+ const existing = readFileSync17(dest, "utf8");
11949
11951
  const startIdx = existing.indexOf(START_MARKER2);
11950
11952
  const endIdx = existing.indexOf(END_MARKER2);
11951
11953
  if (startIdx >= 0 && endIdx > startIdx) {
@@ -11980,7 +11982,7 @@ async function installHooks() {
11980
11982
 
11981
11983
  // src/cli/index.ts
11982
11984
  var __dirname2 = dirname7(fileURLToPath3(import.meta.url));
11983
- var pkg = JSON.parse(readFileSync26(join27(__dirname2, "..", "..", "package.json"), "utf8"));
11985
+ var pkg = JSON.parse(readFileSync27(join27(__dirname2, "..", "..", "package.json"), "utf8"));
11984
11986
  var program = new Command();
11985
11987
  program.name("cdd-kit").description("Contract-Driven Delivery Kit CLI").version(pkg.version);
11986
11988
  program.command("init").description(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contract-driven-delivery",
3
- "version": "2.0.10",
3
+ "version": "2.0.12",
4
4
  "description": "Contract-driven delivery kit for AI coding agents with deterministic context indexes, manifest-backed read-scope governance, and orchestrated contracts-first delivery.",
5
5
  "keywords": [
6
6
  "contract-driven",