@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.
- package/.dz-manifest.json +74 -34
- package/README.md +57 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/lesson-payoff.d.ts.map +1 -1
- package/dist/lesson-payoff.js +3 -1
- package/dist/lesson-payoff.js.map +1 -1
- package/dist/memory-index-check.d.ts.map +1 -1
- package/dist/memory-index-check.js +17 -4
- package/dist/memory-index-check.js.map +1 -1
- package/dist/pack-artifact.d.ts +24 -0
- package/dist/pack-artifact.d.ts.map +1 -0
- package/dist/pack-artifact.js +99 -0
- package/dist/pack-artifact.js.map +1 -0
- package/dist/profile.d.ts.map +1 -1
- package/dist/profile.js +6 -0
- package/dist/profile.js.map +1 -1
- package/dist/publish-sibling-drift.d.ts +6 -2
- package/dist/publish-sibling-drift.d.ts.map +1 -1
- package/dist/publish-sibling-drift.js +10 -1
- package/dist/publish-sibling-drift.js.map +1 -1
- package/dist/publish.d.ts +1 -2
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +9 -53
- package/dist/publish.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +3 -2
- package/dist/setup.js.map +1 -1
- package/dist/stamped-path.d.ts +15 -0
- package/dist/stamped-path.d.ts.map +1 -0
- package/dist/stamped-path.js +32 -0
- package/dist/stamped-path.js.map +1 -0
- package/package.json +12 -12
- package/sbom.json +133 -33
- package/src/index.ts +4 -1
- package/src/lesson-payoff.ts +3 -1
- package/src/memory-index-check.ts +17 -4
- package/src/pack-artifact.ts +109 -0
- package/src/profile.ts +5 -0
- package/src/publish-sibling-drift.ts +16 -3
- package/src/publish.ts +9 -50
- package/src/setup.ts +3 -2
- 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
|
|
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
|
-
?
|
|
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 {
|
|
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
|
|
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
|
-
//
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
-
|
|
1119
|
-
|
|
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
|
+
}
|