@intentius/chant 0.54.1 → 0.54.3

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.
@@ -86,10 +86,7 @@ export declare const ENTROPY_RULE_ID = "SEC010";
86
86
  * trigger it.
87
87
  */
88
88
  export declare function looksLikeIdentifierSlug(value: string): boolean;
89
- /**
90
- * Scan a set of files for likely secrets/credentials. Pure with respect to
91
- * the filesystem and network. Findings never carry the matched value —
92
- * only its rule id, kind, file/line, and a redaction-safe fingerprint.
93
- */
89
+ /** Is this a dependency lockfile the secrets scan should skip? */
90
+ export declare function isLockfilePath(path: string): boolean;
94
91
  export declare function scanForSecrets(files: ScannableFile[], opts?: SecretsScanOptions): AuditFinding[];
95
92
  //# sourceMappingURL=secrets.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../src/audit/secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAG3C,qFAAqF;AACrF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,0GAA0G;IAC1G,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kFAAkF;IAClF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yFAAyF;IACzF,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;CAC3B;AAOD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYvD;AAkDD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,CAuBtE;AA2GD,2FAA2F;AAC3F,eAAO,MAAM,eAAe,WAAW,CAAC;AAIxC;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAK9D;AAsCD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,IAAI,GAAE,kBAAuB,GAAG,YAAY,EAAE,CAkEpG"}
1
+ {"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../../src/audit/secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAG3C,qFAAqF;AACrF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,0GAA0G;IAC1G,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kFAAkF;IAClF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yFAAyF;IACzF,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;CAC3B;AAOD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAYvD;AAkDD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,CAuBtE;AA2GD,2FAA2F;AAC3F,eAAO,MAAM,eAAe,WAAW,CAAC;AAIxC;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAK9D;AAqED,kEAAkE;AAClE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGpD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,EAAE,IAAI,GAAE,kBAAuB,GAAG,YAAY,EAAE,CAmEpG"}
@@ -275,8 +275,22 @@ export interface CrossEnvComparison {
275
275
  envB: string;
276
276
  digestA?: string;
277
277
  digestB?: string;
278
- /** True only when both envs have a recorded digest and they match. */
278
+ /**
279
+ * True only when both envs have a recorded digest and their *comparable*
280
+ * identities match. For a record carrying `inputDigest` (a pinned helm
281
+ * deploy, chant#1242/#1243), that field is the comparable identity —
282
+ * `ReleaseRecord.inputDigest`'s own contract: profiles are per cluster, two
283
+ * environments legitimately render to different bytes, so cross-environment
284
+ * "is prod running what staging tested" joins on the input side while
285
+ * `digest` proves the exact bytes each cluster got. Records without it
286
+ * compare on `digest` exactly as before.
287
+ */
279
288
  same: boolean;
289
+ /** The identities `same` was decided on (`inputDigest ?? digest` per side), when both sides had one and they differ from the digests shown. */
290
+ comparedOn?: {
291
+ a: string;
292
+ b: string;
293
+ };
280
294
  }
281
295
  /** Compare the latest recorded digest for `component` between two environments' release records. */
282
296
  export declare function compareAcrossEnvironments(component: string, envA: {
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/lifecycle/status.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/lifecycle/status.ts"],"names":[],"mappings":"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"}
@@ -8,7 +8,10 @@ export interface OpDiscoveryResult {
8
8
  errors: string[];
9
9
  }
10
10
  /**
11
- * Discover all Op definitions from *.op.ts files under the git root.
11
+ * Discover all Op definitions from `*.op.ts` files under the nearest chant
12
+ * project root (the directory holding `chant.config.ts`/`.json`, walking up
13
+ * from `cwd`), or under the git root when no config exists — see
14
+ * {@link findDiscoveryRoot} (#2058).
12
15
  */
13
16
  export declare function discoverOps(opts?: {
14
17
  cwd?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../src/op/discover.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC/B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAiCD;;GAEG;AACH,wBAAsB,WAAW,CAAC,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAuCrF"}
1
+ {"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../src/op/discover.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC/B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AA0DD;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAuCrF"}
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/terraform/manifest.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/terraform/manifest.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.54.1",
3
+ "version": "0.54.3",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -300,4 +300,19 @@ describe("scanForSecrets — file handling", () => {
300
300
  expect(findings[0].severity).toBe("error");
301
301
  expect(findings[0].lexicon).toBe("secrets");
302
302
  });
303
+
304
+ test("dependency lockfiles are skipped entirely — integrity hashes are not secrets", () => {
305
+ // The exact false-positive class that failed the first dogfooding repo:
306
+ // one lockfile's sha512 integrity fields read as hundreds of SEC010s.
307
+ const integrity = `"integrity": "sha512-${"a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8s9T0u1V2w3X4y5Z6".repeat(2)}"`;
308
+ for (const path of ["package-lock.json", "web/package-lock.json", "yarn.lock", "pnpm-lock.yaml", "Cargo.lock", "go.sum"]) {
309
+ expect(scanForSecrets([{ path, content: `${integrity}\n` }]), path).toEqual([]);
310
+ }
311
+ // Even a real credential shape stays silent there — the file is skipped,
312
+ // not entropy-tuned; a credential does not live in a machine-written
313
+ // lockfile, and per-file skipping is what keeps the rule explainable.
314
+ expect(scanForSecrets([{ path: "package-lock.json", content: `${FAKE_AWS_KEY}\n` }])).toEqual([]);
315
+ // A non-lockfile with the same content still fires.
316
+ expect(scanForSecrets([{ path: "config.json", content: `${integrity}\n` }]).length).toBeGreaterThan(0);
317
+ });
303
318
  });
@@ -337,6 +337,38 @@ function lineNumberFor(starts: number[], index: number): number {
337
337
  * the filesystem and network. Findings never carry the matched value —
338
338
  * only its rule id, kind, file/line, and a redaction-safe fingerprint.
339
339
  */
340
+ /**
341
+ * Machine-generated dependency lockfiles. Skipped entirely: they are wall-to-
342
+ * wall content-integrity hashes (`sha512-…` and friends), which the entropy
343
+ * heuristic reads as hundreds of "possible secrets" per file — a single
344
+ * `package-lock.json` produced 438 merge-worthy SEC010 findings on the first
345
+ * repo that dogfooded this at `--fail-on merge-worthy`. Integrity hashes are
346
+ * public by construction, a credential does not live in a lockfile, and no
347
+ * other scanner reads them.
348
+ */
349
+ const LOCKFILE_NAMES = new Set([
350
+ "package-lock.json",
351
+ "npm-shrinkwrap.json",
352
+ "yarn.lock",
353
+ "pnpm-lock.yaml",
354
+ "bun.lock",
355
+ "deno.lock",
356
+ "Cargo.lock",
357
+ "poetry.lock",
358
+ "uv.lock",
359
+ "Pipfile.lock",
360
+ "composer.lock",
361
+ "Gemfile.lock",
362
+ "go.sum",
363
+ "flake.lock",
364
+ ]);
365
+
366
+ /** Is this a dependency lockfile the secrets scan should skip? */
367
+ export function isLockfilePath(path: string): boolean {
368
+ const name = path.split("/").pop() ?? path;
369
+ return LOCKFILE_NAMES.has(name);
370
+ }
371
+
340
372
  export function scanForSecrets(files: ScannableFile[], opts: SecretsScanOptions = {}): AuditFinding[] {
341
373
  const entropyThreshold = opts.entropyThreshold ?? DEFAULT_ENTROPY_THRESHOLD;
342
374
  const entropyMinLength = opts.entropyMinLength ?? DEFAULT_ENTROPY_MIN_LENGTH;
@@ -347,6 +379,7 @@ export function scanForSecrets(files: ScannableFile[], opts: SecretsScanOptions
347
379
 
348
380
  for (const file of files) {
349
381
  if (!file.content) continue;
382
+ if (isLockfilePath(file.path)) continue;
350
383
  const lines = file.content.split("\n");
351
384
  const starts = lineStarts(file.content);
352
385
  const claimed: Array<[number, number]> = [];
@@ -1,5 +1,5 @@
1
1
  import { describe, test, expect } from "vitest";
2
- import { mkdtempSync, writeFileSync, rmSync, readFileSync, existsSync } from "fs";
2
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from "fs";
3
3
  import { tmpdir } from "os";
4
4
  import { join } from "path";
5
5
  import { carveApply, formatCarveApply } from "./carve-apply";
@@ -108,6 +108,36 @@ describe("carveApply", () => {
108
108
  });
109
109
  });
110
110
 
111
+ test("a RELATIVE --output still records stampedFiles relative to the manifest's directory, not the cwd (#2059)", async () => {
112
+ if (!parserAvailable) return;
113
+ await withEstate(async (dir) => {
114
+ // The behold carve-demo shape: run FROM the project dir with
115
+ // `--output carveout`. The bug: resolveManifestFilePath joined against
116
+ // the relative outDir, producing a cwd-relative path ("carveout/…")
117
+ // that normalize-on-write passed through — two spellings in one manifest.
118
+ const out = join(dir, "carveout");
119
+ const manifest = manifestFor("aws_s3_bucket.assets", dir);
120
+ manifest.emit = { source: "tfstate", files: [join("src", "assets.ts")], at: "t" };
121
+ writeCarveManifest(out, manifest);
122
+ mkdirSync(join(out, "src"), { recursive: true });
123
+ writeFileSync(join(out, "src", "assets.ts"), 'export const assets = new Bucket({\n BucketName: "b",\n});\n');
124
+
125
+ const prevCwd = process.cwd();
126
+ process.chdir(dir);
127
+ try {
128
+ const res = await carveApply({ from: ".", output: "carveout", env: "prod", stack: "assets", writeSource: true });
129
+ expect(res.ok).toBe(true);
130
+ const m = readCarveManifest(res.manifestPath!)!;
131
+ // One spelling: manifest-dir-relative, same as emit.files — never
132
+ // "carveout/src/assets.ts" (relative to wherever the command ran).
133
+ expect(m.apply!.stampedFiles).toEqual([join("src", "assets.ts")]);
134
+ expect(m.emit!.files).toEqual([join("src", "assets.ts")]);
135
+ } finally {
136
+ process.chdir(prevCwd);
137
+ }
138
+ });
139
+ });
140
+
111
141
  test("--write-source without an emit record in the manifest is a clear error", async () => {
112
142
  if (!parserAvailable) return;
113
143
  await withEstate(async (dir) => {
@@ -413,6 +413,44 @@ describe("status", () => {
413
413
  expect(result.digestB).toBe("sha256:abc");
414
414
  expect(result.same).toBe(false);
415
415
  });
416
+
417
+ test("pinned helm records compare on inputDigest — per-cluster bytes legitimately differ (ReleaseRecord.inputDigest's own contract)", () => {
418
+ // Same chart+values rendered against two clusters' capability profiles:
419
+ // contentDigests (recorded as `digest`) differ, inputDigests match —
420
+ // prod IS running what staging tested.
421
+ const result = compareAcrossEnvironments(
422
+ "web",
423
+ { name: "staging", records: [record({ component: "web", env: "staging", digest: "sha256:staging-bytes", inputDigest: "sha256:inputs" })] },
424
+ { name: "prod", records: [record({ component: "web", env: "prod", digest: "sha256:prod-bytes", inputDigest: "sha256:inputs" })] },
425
+ );
426
+ expect(result.same).toBe(true);
427
+ expect(result.comparedOn).toEqual({ a: "sha256:inputs", b: "sha256:inputs" });
428
+ // The exact bytes each cluster got stay visible.
429
+ expect(result.digestA).toBe("sha256:staging-bytes");
430
+ expect(result.digestB).toBe("sha256:prod-bytes");
431
+ });
432
+
433
+ test("differing inputDigests are a real difference, whatever the bytes say", () => {
434
+ const result = compareAcrossEnvironments(
435
+ "web",
436
+ { name: "staging", records: [record({ component: "web", env: "staging", digest: "sha256:same-bytes", inputDigest: "sha256:inputs-v2" })] },
437
+ { name: "prod", records: [record({ component: "web", env: "prod", digest: "sha256:same-bytes", inputDigest: "sha256:inputs-v1" })] },
438
+ );
439
+ expect(result.same).toBe(false);
440
+ });
441
+
442
+ test("one side input-identified, the other content-identified: compared honestly on what each carries", () => {
443
+ // Mixed deploys (a pinned helm release in prod, an unpinned one in
444
+ // staging where the record's digest IS the input digest): staging's
445
+ // digest against prod's inputDigest is the only meaningful join.
446
+ const result = compareAcrossEnvironments(
447
+ "web",
448
+ { name: "staging", records: [record({ component: "web", env: "staging", digest: "sha256:inputs" })] },
449
+ { name: "prod", records: [record({ component: "web", env: "prod", digest: "sha256:prod-bytes", inputDigest: "sha256:inputs" })] },
450
+ );
451
+ expect(result.same).toBe(true);
452
+ expect(result.comparedOn).toEqual({ a: "sha256:inputs", b: "sha256:inputs" });
453
+ });
416
454
  });
417
455
 
418
456
  describe("mergeLiveEvidence (#57 — per-component stack presence overlay)", () => {
@@ -478,8 +478,19 @@ export interface CrossEnvComparison {
478
478
  envB: string;
479
479
  digestA?: string;
480
480
  digestB?: string;
481
- /** True only when both envs have a recorded digest and they match. */
481
+ /**
482
+ * True only when both envs have a recorded digest and their *comparable*
483
+ * identities match. For a record carrying `inputDigest` (a pinned helm
484
+ * deploy, chant#1242/#1243), that field is the comparable identity —
485
+ * `ReleaseRecord.inputDigest`'s own contract: profiles are per cluster, two
486
+ * environments legitimately render to different bytes, so cross-environment
487
+ * "is prod running what staging tested" joins on the input side while
488
+ * `digest` proves the exact bytes each cluster got. Records without it
489
+ * compare on `digest` exactly as before.
490
+ */
482
491
  same: boolean;
492
+ /** The identities `same` was decided on (`inputDigest ?? digest` per side), when both sides had one and they differ from the digests shown. */
493
+ comparedOn?: { a: string; b: string };
483
494
  }
484
495
 
485
496
  /** Compare the latest recorded digest for `component` between two environments' release records. */
@@ -490,12 +501,16 @@ export function compareAcrossEnvironments(
490
501
  ): CrossEnvComparison {
491
502
  const latestA = latestPerComponent(envA.records).get(component);
492
503
  const latestB = latestPerComponent(envB.records).get(component);
504
+ const idA = latestA ? latestA.inputDigest ?? latestA.digest : undefined;
505
+ const idB = latestB ? latestB.inputDigest ?? latestB.digest : undefined;
506
+ const comparedOnInputs = (latestA?.inputDigest ?? latestB?.inputDigest) !== undefined;
493
507
  return {
494
508
  component,
495
509
  envA: envA.name,
496
510
  envB: envB.name,
497
511
  digestA: latestA?.digest,
498
512
  digestB: latestB?.digest,
499
- same: !!latestA && !!latestB && latestA.digest === latestB.digest,
513
+ same: !!idA && !!idB && idA === idB,
514
+ ...(comparedOnInputs && idA && idB ? { comparedOn: { a: idA, b: idB } } : {}),
500
515
  };
501
516
  }
@@ -0,0 +1,68 @@
1
+ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { discoverOps } from "./discover";
6
+
7
+ /**
8
+ * #2058 — Op discovery scoped to the project root, the way entity discovery
9
+ * is. The repro: a chant project nested in a larger checkout (behold's
10
+ * committed `example-carve/app` beside `example-writes`, `example-k8s`)
11
+ * discovered every sibling project's `*.op.ts` as its own, because the scan
12
+ * root was the git root unconditionally.
13
+ *
14
+ * The fake git root is a temp tree; the runtime mock answers `git rev-parse`
15
+ * with whatever the current test set (`realpath`ed, since macOS's tmpdir is a
16
+ * symlink and the walk compares resolved paths).
17
+ */
18
+ let fakeGitRoot = "";
19
+ vi.mock("../runtime-adapter", () => ({
20
+ getRuntime: () => ({
21
+ spawn: async (cmd: string[]) =>
22
+ cmd[0] === "git" && cmd[1] === "rev-parse"
23
+ ? { stdout: fakeGitRoot, stderr: "", exitCode: 0 }
24
+ : { stdout: "", stderr: "", exitCode: 0 },
25
+ }),
26
+ }));
27
+
28
+ /** A minimal default export shaped the way discovery validates (entity.props = OpConfig). */
29
+ const OP_FILE = (name: string): string =>
30
+ `export default { props: { name: ${JSON.stringify(name)}, overview: "t", phases: [{ name: "Run", steps: [] }] } };\n`;
31
+
32
+ describe("discoverOps — the scan root is the project, not the checkout (#2058)", () => {
33
+ let checkout: string;
34
+ beforeEach(() => {
35
+ checkout = realpathSync(mkdtempSync(join(tmpdir(), "chant-op-root-")));
36
+ fakeGitRoot = checkout;
37
+ // The monorepo shape from the issue: two sibling chant projects, each
38
+ // with its own config and ops, nested in one git checkout.
39
+ mkdirSync(join(checkout, "app", "ops"), { recursive: true });
40
+ writeFileSync(join(checkout, "app", "chant.config.json"), JSON.stringify({ lexicons: ["aws"] }));
41
+ writeFileSync(join(checkout, "app", "ops", "mine.op.ts"), OP_FILE("mine"));
42
+ mkdirSync(join(checkout, "sibling", "ops"), { recursive: true });
43
+ writeFileSync(join(checkout, "sibling", "chant.config.json"), JSON.stringify({ lexicons: ["aws"] }));
44
+ writeFileSync(join(checkout, "sibling", "ops", "theirs.op.ts"), OP_FILE("theirs"));
45
+ // A configless dir at the checkout root, with an op of its own.
46
+ mkdirSync(join(checkout, "ops"), { recursive: true });
47
+ writeFileSync(join(checkout, "ops", "root.op.ts"), OP_FILE("root"));
48
+ });
49
+ afterEach(() => rmSync(checkout, { recursive: true, force: true }));
50
+
51
+ test("from inside a project, only that project's Ops are discovered — never a sibling's", async () => {
52
+ const { ops, errors } = await discoverOps({ cwd: join(checkout, "app") });
53
+ expect(errors).toEqual([]);
54
+ expect([...ops.keys()]).toEqual(["mine"]);
55
+ });
56
+
57
+ test("the walk finds the config from a subdirectory of the project too (ops/ beside src/, #1675)", async () => {
58
+ const { ops } = await discoverOps({ cwd: join(checkout, "app", "ops") });
59
+ expect([...ops.keys()]).toEqual(["mine"]);
60
+ });
61
+
62
+ test("with no chant config anywhere up to the git root, the git root stands — #1675's original scope", async () => {
63
+ rmSync(join(checkout, "app", "chant.config.json"));
64
+ rmSync(join(checkout, "sibling", "chant.config.json"));
65
+ const { ops } = await discoverOps({ cwd: join(checkout, "app") });
66
+ expect([...ops.keys()].sort()).toEqual(["mine", "root", "theirs"]);
67
+ });
68
+ });
@@ -1,6 +1,7 @@
1
1
  import { getRuntime } from "../runtime-adapter";
2
2
  import { readdir } from "node:fs/promises";
3
- import { join } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
4
5
  import type { OpConfig } from "./types";
5
6
 
6
7
  export interface DiscoveredOp {
@@ -20,6 +21,31 @@ async function findGitRoot(cwd?: string): Promise<string> {
20
21
  return result.stdout.trim();
21
22
  }
22
23
 
24
+ /**
25
+ * The root the `*.op.ts` scan starts from (#2058): the nearest chant project
26
+ * root — the directory holding `chant.config.ts`/`chant.config.json`, walking
27
+ * up from `cwd` and never past the git root. #1675's convention keeps Op
28
+ * files OUTSIDE `sourceDir` (`ops/` beside `src/`), which is why discovery
29
+ * walks up at all — but "up" used to mean the git root unconditionally, so a
30
+ * chant project nested in a larger checkout (a monorepo, behold's committed
31
+ * examples) discovered every SIBLING project's Ops as its own. The config
32
+ * file is the project boundary, the same one entity discovery respects.
33
+ * With no config anywhere up to the git root, the git root stands —
34
+ * #1675's original scope, kept for configless layouts.
35
+ */
36
+ async function findDiscoveryRoot(cwd?: string): Promise<string> {
37
+ const gitRoot = resolve(await findGitRoot(cwd));
38
+ let dir = resolve(cwd ?? process.cwd());
39
+ for (;;) {
40
+ if (existsSync(join(dir, "chant.config.ts")) || existsSync(join(dir, "chant.config.json"))) return dir;
41
+ if (dir === gitRoot) break;
42
+ const parent = dirname(dir);
43
+ if (parent === dir) break; // filesystem root — cwd was outside the git root
44
+ dir = parent;
45
+ }
46
+ return gitRoot;
47
+ }
48
+
23
49
  async function collectOpFiles(dir: string): Promise<string[]> {
24
50
  const files: string[] = [];
25
51
  let entries;
@@ -45,14 +71,17 @@ async function collectOpFiles(dir: string): Promise<string[]> {
45
71
  }
46
72
 
47
73
  /**
48
- * Discover all Op definitions from *.op.ts files under the git root.
74
+ * Discover all Op definitions from `*.op.ts` files under the nearest chant
75
+ * project root (the directory holding `chant.config.ts`/`.json`, walking up
76
+ * from `cwd`), or under the git root when no config exists — see
77
+ * {@link findDiscoveryRoot} (#2058).
49
78
  */
50
79
  export async function discoverOps(opts?: { cwd?: string }): Promise<OpDiscoveryResult> {
51
80
  const errors: string[] = [];
52
81
  const ops = new Map<string, DiscoveredOp>();
53
82
 
54
- const gitRoot = await findGitRoot(opts?.cwd);
55
- const files = await collectOpFiles(gitRoot);
83
+ const root = await findDiscoveryRoot(opts?.cwd);
84
+ const files = await collectOpFiles(root);
56
85
 
57
86
  const nameToFile = new Map<string, string>();
58
87
 
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
12
- import { isAbsolute, join, relative } from "path";
12
+ import { isAbsolute, join, relative, resolve } from "path";
13
13
  import type { CarveReport } from "./carve";
14
14
  import type { OwnershipMarker } from "../ownership";
15
15
 
@@ -100,10 +100,14 @@ export function carveManifestPath(outDir: string, target: string): string {
100
100
  /**
101
101
  * Relative-to-`outDir` spelling of a recorded file path. Absolute entries
102
102
  * (the run-time spelling every step naturally produces, and every version-1
103
- * manifest on disk) are rewritten; already-relative entries pass through.
103
+ * manifest on disk) are rewritten against the RESOLVED outDir — a caller's
104
+ * relative `--output` must not change what "relative to the manifest's
105
+ * directory" means (#2059). Already-relative entries pass through: they can
106
+ * only have come from a reader honouring the contract, and re-basing them
107
+ * against an unknowable original cwd would corrupt, not normalize.
104
108
  */
105
109
  function relativizePath(outDir: string, p: string): string {
106
- return isAbsolute(p) ? relative(outDir, p) : p;
110
+ return isAbsolute(p) ? relative(resolve(outDir), p) : p;
107
111
  }
108
112
 
109
113
  /**
@@ -136,7 +140,11 @@ function normalizeManifest(outDir: string, manifest: CarveManifest): CarveManife
136
140
  * on-disk spellings read the same.
137
141
  */
138
142
  export function resolveManifestFilePath(manifestDir: string, p: string): string {
139
- return isAbsolute(p) ? p : join(manifestDir, p);
143
+ // Always ABSOLUTE (#2059): `join` with a relative manifestDir produced a
144
+ // cwd-relative path, which `relativizePath` then passed through untouched —
145
+ // so `carve apply --output carveout` recorded `stampedFiles` relative to
146
+ // wherever the command happened to run, two spellings in one manifest.
147
+ return isAbsolute(p) ? p : resolve(manifestDir, p);
140
148
  }
141
149
 
142
150
  /** Write (or overwrite) a manifest into the output dir. Returns the path. */