@dzhechkov/harness-core 0.8.43 → 0.8.45

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.
Files changed (45) hide show
  1. package/.dz-manifest.json +74 -34
  2. package/README.md +57 -1
  3. package/dist/index.d.ts +4 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +3 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/lesson-payoff.d.ts.map +1 -1
  8. package/dist/lesson-payoff.js +3 -1
  9. package/dist/lesson-payoff.js.map +1 -1
  10. package/dist/memory-index-check.d.ts.map +1 -1
  11. package/dist/memory-index-check.js +17 -4
  12. package/dist/memory-index-check.js.map +1 -1
  13. package/dist/pack-artifact.d.ts +24 -0
  14. package/dist/pack-artifact.d.ts.map +1 -0
  15. package/dist/pack-artifact.js +99 -0
  16. package/dist/pack-artifact.js.map +1 -0
  17. package/dist/profile.d.ts.map +1 -1
  18. package/dist/profile.js +6 -0
  19. package/dist/profile.js.map +1 -1
  20. package/dist/publish-sibling-drift.d.ts +6 -2
  21. package/dist/publish-sibling-drift.d.ts.map +1 -1
  22. package/dist/publish-sibling-drift.js +10 -1
  23. package/dist/publish-sibling-drift.js.map +1 -1
  24. package/dist/publish.d.ts +1 -2
  25. package/dist/publish.d.ts.map +1 -1
  26. package/dist/publish.js +9 -53
  27. package/dist/publish.js.map +1 -1
  28. package/dist/setup.d.ts.map +1 -1
  29. package/dist/setup.js +3 -2
  30. package/dist/setup.js.map +1 -1
  31. package/dist/stamped-path.d.ts +15 -0
  32. package/dist/stamped-path.d.ts.map +1 -0
  33. package/dist/stamped-path.js +32 -0
  34. package/dist/stamped-path.js.map +1 -0
  35. package/package.json +12 -12
  36. package/sbom.json +133 -33
  37. package/src/index.ts +4 -1
  38. package/src/lesson-payoff.ts +3 -1
  39. package/src/memory-index-check.ts +17 -4
  40. package/src/pack-artifact.ts +109 -0
  41. package/src/profile.ts +5 -0
  42. package/src/publish-sibling-drift.ts +16 -3
  43. package/src/publish.ts +9 -50
  44. package/src/setup.ts +3 -2
  45. package/src/stamped-path.ts +41 -0
@@ -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,
package/src/setup.ts CHANGED
@@ -21,6 +21,7 @@ import { basename, dirname, isAbsolute, join, relative } from 'node:path';
21
21
  import { execSync, spawnSync } from 'node:child_process';
22
22
 
23
23
  import { mergeManagedHookEntries } from './managed-hooks.js';
24
+ import { writeUniqueStampedFile } from './stamped-path.js';
24
25
  import {
25
26
  CLAUDE_DESTRUCTIVE_HOOK_COMMAND,
26
27
  CLAUDE_DESTRUCTIVE_HOOK_MATCHER,
@@ -1115,8 +1116,8 @@ export function runSetup(opts: SetupOptions): SetupResult {
1115
1116
  if (foreign && current !== null) {
1116
1117
  // Same shape as the codex `hooks.json` backup: the original beside the original, stamped,
1117
1118
  // so `--force` is recoverable rather than merely loud.
1118
- backupPath = `${hookPath}.bak-${new Date().toISOString().replace(/[:.]/g, '-')}`;
1119
- writeFileSync(backupPath, current);
1119
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
1120
+ backupPath = writeUniqueStampedFile(`${hookPath}.bak-`, stamp, current, writeFileSync);
1120
1121
  }
1121
1122
  mkdirSync(dirname(hookPath), { recursive: true });
1122
1123
  writeFileSync(hookPath, generateClaudeDestructiveHook(), { mode: 0o755 });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Pure on purpose: harness-core's IO ratchet (test/core-boundary.test.ts) counts files that import node:fs — callers that
3
+ * already do IO pass their own `exists` / `write` (MEASURED 2026-09-25: a default `existsSync` here tripped it, 65 → 66).
4
+ */
5
+
6
+ export type StampedWrite = (path: string, data: string | Uint8Array, options: { flag: 'wx' }) => void;
7
+
8
+ /**
9
+ * Return the first unused stamped path, adding a counter suffix on collision.
10
+ * check-then-act: safe within one process or under a lock; for unlocked cross-process writers use writeUniqueStampedFile
11
+ */
12
+ export function uniqueStampedPath(
13
+ base: string,
14
+ stamp: string,
15
+ exists: (p: string) => boolean,
16
+ ): string {
17
+ let candidate = `${base}${stamp}`;
18
+ for (let n = 1; exists(candidate); n += 1) candidate = `${base}${stamp}-${n}`;
19
+ return candidate;
20
+ }
21
+
22
+ /** Atomically create a stamped file, trying at most 1000 candidates on EEXIST. */
23
+ export function writeUniqueStampedFile(
24
+ base: string,
25
+ stamp: string,
26
+ data: string | Uint8Array,
27
+ write: StampedWrite,
28
+ ): string {
29
+ for (let n = 0; n < 1000; n += 1) {
30
+ const candidate = n === 0 ? `${base}${stamp}` : `${base}${stamp}-${n}`;
31
+ try {
32
+ write(candidate, data, { flag: 'wx' });
33
+ return candidate;
34
+ } catch (error) {
35
+ if ((error as NodeJS.ErrnoException | null)?.code !== 'EEXIST') throw error;
36
+ }
37
+ }
38
+ const error = new Error(`No free stamped file after 1000 attempts: ${base}${stamp}`);
39
+ error.name = 'UniqueStampedFileExhaustedError';
40
+ throw error;
41
+ }