assertledger 1.2.0 → 1.3.1

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.
@@ -1,20 +1,20 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  import { createReadStream } from "node:fs";
4
- import { cp, lstat, mkdir, mkdtemp, open, readdir, readFile, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
4
+ import { cp, link, lstat, mkdir, mkdtemp, open, readdir, readFile, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
7
  import { Readable } from "node:stream";
8
8
  import { pipeline } from "node:stream/promises";
9
9
  import { fileURLToPath, pathToFileURL } from "node:url";
10
10
  import { createScanner, SyntaxKind } from "typescript/unstable/ast";
11
- import { AdapterSchema, isRepositoryExcludeName, parseAgenticBenchmarkAcquisitionRequest, parseAgenticBenchmarkAcquisitionResult, parseEvidenceManifest, parseRepositoryAudit, parseRepositoryInitConfig, parseRepositoryInitConfigV2, parseRepositoryInitLock, parseRepositoryInitLockV2, parseRepositoryInitResult, parseRepositoryInitResultV2, parseVersionedRepositoryInitConfig, parseVersionedRepositoryInitLock, parseVerificationRequest, parseVersionedVerificationRequest, RepositoryInitDetectionsSchema, repositoryInitConfigDigest, repositoryInitConfigV2Digest, repositoryInitLockDigest, repositoryInitLockV2Digest, } from "../contracts/index.js";
11
+ import { AdapterSchema, isRepositoryExcludeName, parseAgenticBenchmarkAcquisitionRequest, parseAgenticBenchmarkAcquisitionResult, parseEvidenceManifest, parseRepositoryAudit, parseRepositoryInitConfig, parseRepositoryInitConfigV2, parseRepositoryInitLock, parseRepositoryInitLockV2, parseRepositoryInitResult, parseRepositoryInitResultV2, parseVerificationRequest, parseVersionedRepositoryInitConfig, parseVersionedRepositoryInitLock, parseVersionedVerificationRequest, RepositoryInitDetectionsSchema, repositoryInitConfigDigest, repositoryInitConfigV2Digest, repositoryInitLockDigest, repositoryInitLockV2Digest, } from "../contracts/index.js";
12
12
  import { assertSafeRelativePath, canonicalize, createAgenticBenchmark, decideEvidence, portablePathKey, replayAgenticBenchmark, replayEvidenceManifest, sealManifestArtifact, sha256Canonical, } from "../core/index.js";
13
- import { NODE_TEST_ADAPTER_PROFILE } from "./adapters/node-test-profile.js";
14
13
  import { BUN_TEST_ADAPTER_PROFILE } from "./adapters/bun-test-profile.js";
14
+ import { NODE_TEST_ADAPTER_PROFILE } from "./adapters/node-test-profile.js";
15
15
  import { runNodeTestRuntimePreflight, } from "./adapters/node-test-runtime.js";
16
- import { CONTAINER_LIMITATIONS, CONTAINER_REPORTER_FILE, CONTAINER_RESULT_FILE, CONTAINER_WORKSPACE, DEFAULT_CONTAINER_RUNTIME_COMMAND, prepareContainerBackend, runContainerExecution, } from "./container.js";
17
16
  import { normalizeRuntimeFacts, RUNTIME_FACTS_VERSION } from "./adapters/runtime-facts.js";
17
+ import { CONTAINER_LIMITATIONS, CONTAINER_REPORTER_FILE, CONTAINER_RESULT_FILE, CONTAINER_WORKSPACE, DEFAULT_CONTAINER_RUNTIME_COMMAND, prepareContainerBackend, runContainerExecution, } from "./container.js";
18
18
  import { NODE_TEST_REPORTER_SOURCE } from "./node-test-reporter.js";
19
19
  import { runBunRuntimeDoctorChecks, runRuntimeDoctorChecks, } from "./runtime-doctor.js";
20
20
  const DEFAULT_EXCLUDES = new Set([".git", ".testforge", "node_modules"]);
@@ -35,6 +35,20 @@ const INIT_LOCK_FILE = "assertledger.lock.json";
35
35
  const INIT_MANAGED_FILES = new Set([INIT_CONFIG_FILE, INIT_LOCK_FILE]);
36
36
  const INIT_PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun", "uv", "poetry", "pip"];
37
37
  const INIT_FRAMEWORKS = ["node:test", "vitest", "jest", "bun:test", "pytest"];
38
+ export class RepositoryInitWriteError extends Error {
39
+ targetPath;
40
+ temporaryPath;
41
+ temporaryCleanup;
42
+ installedPaths;
43
+ constructor(cause, targetPath, temporaryPath, temporaryCleanup, installedPaths) {
44
+ super("INIT_WRITE_FAILED", { cause });
45
+ this.name = "RepositoryInitWriteError";
46
+ this.targetPath = targetPath;
47
+ this.temporaryPath = temporaryPath;
48
+ this.temporaryCleanup = temporaryCleanup;
49
+ this.installedPaths = installedPaths;
50
+ }
51
+ }
38
52
  async function removeTemporaryDirectory(directory) {
39
53
  await rm(directory, TEMPORARY_CLEANUP_OPTIONS);
40
54
  }
@@ -310,6 +324,13 @@ async function configuredRepositoryFramework(root) {
310
324
  return undefined;
311
325
  }
312
326
  }
327
+ /** The refusal of a repository link. The message stays the stable code, which callers match and
328
+ * the CLI classifies by value; the link found travels as the cause so a facade can name it. */
329
+ function repositoryLinkError(relative) {
330
+ return new Error("UNSUPPORTED_REPOSITORY_SYMLINK", {
331
+ cause: relative.split(path.sep).join("/"),
332
+ });
333
+ }
313
334
  async function walkFiles(root, excludes) {
314
335
  const files = [];
315
336
  const spellings = new Map();
@@ -324,7 +345,7 @@ async function walkFiles(root, excludes) {
324
345
  const relative = path.posix.join(relativeDirectory.split(path.sep).join("/"), entry.name);
325
346
  const entryStats = await lstat(path.join(root, ...relative.split("/")));
326
347
  if (entryStats.isSymbolicLink())
327
- throw new Error("UNSUPPORTED_REPOSITORY_SYMLINK");
348
+ throw repositoryLinkError(relative);
328
349
  registerPortablePath(spellings, relative);
329
350
  if (entryStats.isDirectory())
330
351
  await visit(relative);
@@ -584,16 +605,75 @@ function initJavaScriptModuleSpecifiers(source) {
584
605
  specifiers.delete("");
585
606
  return specifiers;
586
607
  }
587
- async function atomicInitWrite(root, relative, content) {
608
+ async function atomicInitWrite(root, relative, content, action, installedPaths, dependencies = {}) {
588
609
  const target = path.join(root, relative);
589
610
  const temporary = path.join(root, `.${relative}.${process.pid}.${Date.now()}.tmp`);
611
+ const openTemporary = dependencies.openTemporary ?? ((temporaryPath) => open(temporaryPath, "wx"));
612
+ const writeTemporary = dependencies.writeTemporary ??
613
+ ((_temporaryPath, temporaryContent, handle) => handle.writeFile(temporaryContent));
614
+ const renameTemporary = dependencies.renameTemporary ?? rename;
615
+ const linkTemporary = dependencies.linkTemporary ?? link;
616
+ const removeTemporary = dependencies.removeTemporary ?? ((temporaryPath) => rm(temporaryPath, { force: true }));
617
+ let temporaryOwned = false;
618
+ let temporaryHandle;
619
+ let temporaryHandleClosed = true;
620
+ let targetInstalled = false;
590
621
  try {
591
- await writeFile(temporary, content, { flag: "wx" });
592
- await rename(temporary, target);
622
+ temporaryHandle = await openTemporary(temporary);
623
+ temporaryOwned = true;
624
+ temporaryHandleClosed = false;
625
+ try {
626
+ await writeTemporary(temporary, content, temporaryHandle);
627
+ }
628
+ finally {
629
+ await temporaryHandle.close();
630
+ temporaryHandleClosed = true;
631
+ }
632
+ if (action === "CREATE") {
633
+ await linkTemporary(temporary, target);
634
+ targetInstalled = true;
635
+ await removeTemporary(temporary);
636
+ }
637
+ else {
638
+ await renameTemporary(temporary, target);
639
+ targetInstalled = true;
640
+ }
593
641
  }
594
642
  catch (error) {
595
- await rm(temporary, { force: true });
596
- throw error;
643
+ let temporaryCleanup = "NOT_OWNED";
644
+ if (!temporaryOwned) {
645
+ try {
646
+ await lstat(temporary);
647
+ temporaryCleanup = "UNRESOLVED";
648
+ }
649
+ catch (inspectionError) {
650
+ if (inspectionError.code !== "ENOENT") {
651
+ temporaryCleanup = "UNRESOLVED";
652
+ }
653
+ }
654
+ }
655
+ if (temporaryOwned && !temporaryHandleClosed && temporaryHandle !== undefined) {
656
+ try {
657
+ await temporaryHandle.close();
658
+ temporaryHandleClosed = true;
659
+ }
660
+ catch {
661
+ temporaryCleanup = "UNRESOLVED";
662
+ }
663
+ }
664
+ if (temporaryOwned) {
665
+ temporaryCleanup = "REMOVED";
666
+ try {
667
+ await removeTemporary(temporary);
668
+ }
669
+ catch {
670
+ temporaryCleanup = "UNRESOLVED";
671
+ }
672
+ }
673
+ throw new RepositoryInitWriteError(error, target, temporary, temporaryCleanup, [
674
+ ...installedPaths,
675
+ ...(targetInstalled ? [target] : []),
676
+ ]);
597
677
  }
598
678
  }
599
679
  function initEmptyDetections(reasonCodes) {
@@ -620,7 +700,7 @@ function initTerminalResult(status, detections, reasonCodes) {
620
700
  };
621
701
  return bunBuiltIn ? parseRepositoryInitResultV2(result) : parseRepositoryInitResult(result);
622
702
  }
623
- export async function initializeRepository(requestedRoot, options = {}) {
703
+ export async function initializeRepository(requestedRoot, options = {}, writeDependencies = {}) {
624
704
  let root;
625
705
  try {
626
706
  root = await realpath(requestedRoot);
@@ -1082,11 +1162,14 @@ export async function initializeRepository(requestedRoot, options = {}) {
1082
1162
  ? parseRepositoryInitResultV2(resultValue)
1083
1163
  : parseRepositoryInitResult(resultValue);
1084
1164
  if (!options.dryRun) {
1165
+ const installedPaths = [];
1085
1166
  for (const action of actions) {
1086
1167
  const file = plannedFiles.find((candidate) => candidate.path === action.path);
1087
1168
  if (file === undefined)
1088
1169
  throw new Error("INIT_PLAN_INCONSISTENT");
1089
- await atomicInitWrite(root, file.path, file.content);
1170
+ await atomicInitWrite(root, file.path, file.content, action.kind, installedPaths, writeDependencies);
1171
+ if (action.kind === "CREATE")
1172
+ installedPaths.push(path.join(root, file.path));
1090
1173
  }
1091
1174
  }
1092
1175
  return result;