@dzhechkov/harness-core 0.8.43 → 0.8.44

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.
@@ -0,0 +1,109 @@
1
+ /** One staged artifact for signing, publication and sibling drift. Linux/macOS tar. */
2
+ import { createHash } from 'node:crypto';
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
4
+ import { join, resolve } from 'node:path';
5
+ import { packedTarballName } from './packed-install-smoke.js';
6
+
7
+ export type ExecLike = (command: string, options: { cwd: string; stdio: 'pipe'; encoding: 'utf-8'; maxBuffer: number }) => string;
8
+ export interface PackArtifactResult {
9
+ readonly tgzPath: string;
10
+ readonly sha256: string;
11
+ readonly files: readonly string[];
12
+ }
13
+
14
+ export class UnknownWorkspaceSpecError extends Error {
15
+ constructor(dependency: string, spec: string) {
16
+ super(`Unknown workspace dependency spec for ${dependency}: ${spec}; supply a version and use workspace:*, workspace:^ or workspace:~`);
17
+ this.name = 'UnknownWorkspaceSpecError';
18
+ }
19
+ }
20
+
21
+ export function rewriteWorkspaceSpecs(
22
+ pkgJsonText: string,
23
+ siblingVersions: ReadonlyMap<string, string>,
24
+ ): string {
25
+ const pkg = JSON.parse(pkgJsonText) as Record<string, unknown>;
26
+ const fields = ['dependencies', 'peerDependencies', 'optionalDependencies', 'devDependencies'] as const;
27
+ for (const field of fields) {
28
+ const candidate = pkg[field];
29
+ if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate)) continue;
30
+ const table = candidate as Record<string, unknown>;
31
+ for (const [dep, spec] of Object.entries(table)) {
32
+ if (typeof spec !== 'string') continue;
33
+ const match = /^workspace:([*^~])$/.exec(spec);
34
+ const version = siblingVersions.get(dep);
35
+ if (match === null) {
36
+ if (spec.startsWith('workspace:') || spec.startsWith('catalog:')) {
37
+ throw new UnknownWorkspaceSpecError(dep, spec);
38
+ }
39
+ continue;
40
+ }
41
+ if (version === undefined) continue;
42
+ const marker = match[1]!;
43
+ table[dep] = marker === '*' ? version : `${marker}${version}`;
44
+ }
45
+ }
46
+ return JSON.stringify(pkg, null, 2) + '\n';
47
+ }
48
+
49
+ /** Current pins, read afresh so a later sign sees a sibling's new version. */
50
+ export function readWorkspaceVersions(repoRoot: string): ReadonlyMap<string, string> {
51
+ const scope = join(repoRoot, 'packages', '@dzhechkov');
52
+ const versions = new Map<string, string>();
53
+ if (!existsSync(scope)) return versions;
54
+ for (const entry of readdirSync(scope).sort()) {
55
+ const manifest = join(scope, entry, 'package.json');
56
+ if (!existsSync(manifest)) continue;
57
+ const pkg = JSON.parse(readFileSync(manifest, 'utf8')) as { name?: unknown; version?: unknown };
58
+ if (typeof pkg.name === 'string' && typeof pkg.version === 'string') versions.set(pkg.name, pkg.version);
59
+ }
60
+ return versions;
61
+ }
62
+
63
+ // POSIX shell argument quoting: paths may contain spaces, quotes, dollars or backticks.
64
+
65
+ export function packArtifact(opts: {
66
+ pkgDir: string;
67
+ destDir: string;
68
+ exec: ExecLike;
69
+ pinVersions: ReadonlyMap<string, string>;
70
+ }): PackArtifactResult {
71
+ const pkgDir = resolve(opts.pkgDir);
72
+ const destDir = resolve(opts.destDir);
73
+ const manifestPath = join(pkgDir, 'package.json');
74
+ const original = readFileSync(manifestPath);
75
+ try {
76
+ const staged = JSON.parse(rewriteWorkspaceSpecs(original.toString('utf8'), opts.pinVersions)) as Record<string, unknown>;
77
+ // The public rewrite helper historically leaves a known shorthand without a pin unchanged.
78
+ // An artifact must never ship that unresolved value, even when its caller supplied an incomplete map.
79
+ for (const field of ['dependencies', 'peerDependencies', 'optionalDependencies', 'devDependencies']) {
80
+ const table = staged[field];
81
+ if (table === null || typeof table !== 'object' || Array.isArray(table)) continue;
82
+ for (const [dependency, value] of Object.entries(table)) {
83
+ if (typeof value === 'string' && /^workspace:[*^~]$/.test(value)) {
84
+ throw new UnknownWorkspaceSpecError(dependency, value);
85
+ }
86
+ }
87
+ }
88
+ const scripts = staged['scripts'];
89
+ if (scripts !== null && typeof scripts === 'object' && !Array.isArray(scripts)) {
90
+ delete (scripts as Record<string, unknown>)['prepublishOnly'];
91
+ }
92
+ const name = staged['name'];
93
+ const version = staged['version'];
94
+ if (typeof name !== 'string' || typeof version !== 'string') throw new Error(`Missing package name/version in ${manifestPath}`);
95
+ mkdirSync(destDir, { recursive: true });
96
+ writeFileSync(manifestPath, JSON.stringify(staged, null, 2) + '\n');
97
+ const execOptions = { cwd: pkgDir, stdio: 'pipe' as const, encoding: 'utf-8' as const, maxBuffer: 64 * 1024 * 1024 };
98
+ // The command shape is a CONTRACT: publish's test fake admits exactly this template — double-quoted
99
+ // via JSON.stringify, verbatim from the publish.ts template it replaced (publish-confirm-seam.test.ts).
100
+ opts.exec(`npm pack . --pack-destination ${JSON.stringify(destDir)}`, execOptions);
101
+ const tgzPath = join(destDir, packedTarballName(name, version));
102
+ const listing = opts.exec(`tar -tzf ${JSON.stringify(tgzPath)}`, execOptions);
103
+ const files = listing.split(/\r?\n/).filter(p => p !== '' && !p.endsWith('/')).map(p => p.replace(/^package\//, '')).sort();
104
+ const sha256 = createHash('sha256').update(readFileSync(tgzPath)).digest('hex');
105
+ return { tgzPath, sha256, files };
106
+ } finally {
107
+ writeFileSync(manifestPath, original);
108
+ }
109
+ }
package/src/profile.ts CHANGED
@@ -512,7 +512,12 @@ export function syncProfileBlock(profile: OperatorProfile, home?: string, now?:
512
512
  let backup: string | null = null;
513
513
  if (existing !== null) {
514
514
  const stamp = (now ?? new Date()).toISOString().replace(/[:.]/g, '-');
515
+ // Two syncs inside one millisecond (a fast CI runner) share a stamp: MEASURED 2026-09-24 on
516
+ // package-story-gates (73ac1a32) — the second backup overwrote the first and the test that
517
+ // promises "backups accumulate rather than overwriting" read 1 file. A taken name gets a
518
+ // counter suffix; a backup is never written over an existing one.
515
519
  backup = `${target}.dz-profile-backup-${stamp}`;
520
+ for (let n = 1; existsSync(backup); n += 1) backup = `${target}.dz-profile-backup-${stamp}-${n}`;
516
521
  writeFileSync(backup, existing, { mode: 0o600 });
517
522
  chmodSync(backup, 0o600); // writeFileSync's mode applies only at creation
518
523
  }
@@ -44,7 +44,7 @@ export type SiblingDriftStatus = 'same' | 'drift' | 'unavailable';
44
44
  * symmetric by construction (same function, same rules, both sides) but can miss a file
45
45
  * `.npmignore` excludes or include one npm would never ship.
46
46
  */
47
- export type InventorySource = 'npm-pack' | 'pnpm-pack' | 'readdir-approximation';
47
+ export type InventorySource = 'pack-artifact' | 'npm-pack' | 'pnpm-pack' | 'readdir-approximation';
48
48
 
49
49
  export interface SiblingDriftResult {
50
50
  readonly name: string;
@@ -98,7 +98,7 @@ export interface DetectSiblingDriftOptions {
98
98
  * drift".
99
99
  */
100
100
  readonly localInventory?: LocalInventory;
101
- /** Label for the injected provider's source (default `'npm-pack'`); the CLI passes `'pnpm-pack'` for a packed tree. */
101
+ /** Label for the injected provider (default 'npm-pack'); CLI uses 'pack-artifact'. */
102
102
  readonly localInventorySource?: InventorySource;
103
103
  }
104
104
 
@@ -140,6 +140,8 @@ export interface PackInventoryUnavailable {
140
140
  */
141
141
  export interface PackedTree {
142
142
  readonly packedDir: string;
143
+ /** Exact files from packArtifact().files; old providers may omit this. */
144
+ readonly paths?: readonly string[];
143
145
  }
144
146
 
145
147
  export type LocalInventoryResult = PackInventory | PackedTree | PackInventoryUnavailable;
@@ -453,7 +455,9 @@ export function detectSiblingDrift(opts: DetectSiblingDriftOptions): SiblingDrif
453
455
  }
454
456
  try {
455
457
  workspaceHashes = 'packedDir' in localResult
456
- ? hashTreeFull(localResult.packedDir, workspaceManifest)
458
+ ? localResult.paths !== undefined
459
+ ? hashTreeFromPaths(localResult.packedDir, localResult.paths, workspaceManifest)
460
+ : hashTreeFull(localResult.packedDir, workspaceManifest)
457
461
  : hashTreeFromPaths(workspaceDir, localResult.paths, workspaceManifest);
458
462
  } catch (err) {
459
463
  if (!(err instanceof InventoryListingError)) throw err;
@@ -496,3 +500,12 @@ export function detectSiblingDrift(opts: DetectSiblingDriftOptions): SiblingDrif
496
500
  }
497
501
  return results;
498
502
  }
503
+
504
+
505
+ /** Named drift evidence for both the publish BLOCKED line and its durable audit row. */
506
+ export function formatDriftFiles(changedFiles: readonly string[], source: InventorySource, max = 5): string {
507
+ const shown = changedFiles.slice(0, Math.max(0, max));
508
+ const remaining = changedFiles.length - shown.length;
509
+ const names = [...shown, ...(remaining > 0 ? [`… (+${remaining} more)`] : [])].join(', ');
510
+ return `${names || '(no changed files)'} [source: ${source}]`;
511
+ }
package/src/publish.ts CHANGED
@@ -23,7 +23,7 @@ import { createHash } from 'node:crypto';
23
23
 
24
24
  import { claimCheck } from './claim-check.js';
25
25
  import { findReleaseLine, rewriteReleaseLine, isReleaseLineToken, shortPackageName } from './release-line.js';
26
- import { packedTarballName } from './packed-install-smoke.js';
26
+ import { packArtifact, rewriteWorkspaceSpecs } from './pack-artifact.js';
27
27
 
28
28
  export type ProbeOutcome = {
29
29
  readonly attempt: number;
@@ -263,27 +263,7 @@ function maxPublished(name: string, localVersion: string, exec: PublishExec = ex
263
263
  * Mirror pnpm's package-time expansion of the three shorthand workspace dependency specs.
264
264
  * Pure by construction: callers provide both the source bytes and the sibling version table.
265
265
  */
266
- export function rewriteWorkspaceSpecs(
267
- pkgJsonText: string,
268
- siblingVersions: ReadonlyMap<string, string>,
269
- ): string {
270
- const pkg = JSON.parse(pkgJsonText) as Record<string, unknown>;
271
- const fields = ['dependencies', 'peerDependencies', 'optionalDependencies', 'devDependencies'] as const;
272
- for (const field of fields) {
273
- const candidate = pkg[field];
274
- if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate)) continue;
275
- const table = candidate as Record<string, unknown>;
276
- for (const [dep, spec] of Object.entries(table)) {
277
- if (typeof spec !== 'string') continue;
278
- const match = /^workspace:([*^~])$/.exec(spec);
279
- const version = siblingVersions.get(dep);
280
- if (match === null || version === undefined) continue;
281
- const marker = match[1]!;
282
- table[dep] = marker === '*' ? version : `${marker}${version}`;
283
- }
284
- }
285
- return JSON.stringify(pkg, null, 2) + '\n';
286
- }
266
+ export { rewriteWorkspaceSpecs } from './pack-artifact.js';
287
267
 
288
268
  /**
289
269
  * Pure half: which `workspace:`-declared deps of a package would pack to a floor that is neither
@@ -1311,34 +1291,13 @@ export function publishPackages(
1311
1291
  }
1312
1292
 
1313
1293
  if (opts.packedTransport !== undefined) {
1314
- // AM-1: pack ONCE, from a package.json whose workspace: specs are already resolved to each
1315
- // sibling's PINNED version — the SAME transformation `rewriteWorkspaceSpecs` performs — so
1316
- // the tarball about to be smoked is exactly what `npm publish <tgzPath>` ships. Lifecycle
1317
- // scripts already ran during the `build` step above; `prepublishOnly` is dropped here to
1318
- // mirror what pnpm itself strips at pack time (the manifest-freshness guard, cli.ts, does
1319
- // the identical transformation for verification — this is that same convention, now used
1320
- // to actually PRODUCE the artifact rather than merely check one).
1321
- const bumpedText = readFileSync(pkgJsonPath, 'utf-8'); // already carries newVersion
1322
- const rewritten = JSON.parse(rewriteWorkspaceSpecs(bumpedText, pinVersions)) as Record<string, unknown>;
1323
- const scripts = rewritten['scripts'];
1324
- if (scripts !== null && typeof scripts === 'object' && !Array.isArray(scripts)) {
1325
- delete (scripts as Record<string, unknown>)['prepublishOnly'];
1326
- }
1327
- const stagedText = JSON.stringify(rewritten, null, 2) + '\n';
1328
- let tgzPath: string;
1329
- let digest: string;
1330
- writeFileSync(pkgJsonPath, stagedText);
1331
- try {
1332
- const packOptions = { cwd: pkg.dir, stdio: 'pipe' as const, encoding: 'utf-8' as const };
1333
- if (opts.exec) opts.exec(`npm pack . --pack-destination ${JSON.stringify(opts.packedTransport.packDestDir)}`, packOptions);
1334
- else execSync(`npm pack . --pack-destination ${JSON.stringify(opts.packedTransport.packDestDir)}`, packOptions);
1335
- tgzPath = join(opts.packedTransport.packDestDir, packedTarballName(pkg.name, newVersion));
1336
- digest = sha256File(tgzPath);
1337
- } finally {
1338
- // The COMMITTED tree keeps `workspace:` specs (only the version bump is meant to stick) —
1339
- // the rewrite above is packing-only and is undone here regardless of pack's outcome.
1340
- writeFileSync(pkgJsonPath, bumpedText);
1341
- }
1294
+ // Pack, inventory and digest the exact artifact later handed to npm publish.
1295
+ const { tgzPath, sha256: digest } = packArtifact({
1296
+ pkgDir: pkg.dir,
1297
+ destDir: opts.packedTransport.packDestDir,
1298
+ exec: opts.exec ?? execSync,
1299
+ pinVersions,
1300
+ });
1342
1301
  pendingPacked.push({
1343
1302
  name: pkg.name,
1344
1303
  dir: pkg.dir,