@dzhechkov/harness-core 0.8.32 → 0.8.34
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 +107 -47
- package/README.md +108 -0
- package/dist/agentdb-index.d.ts +17 -6
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +61 -17
- package/dist/agentdb-index.js.map +1 -1
- package/dist/apply-leg.d.ts +68 -5
- package/dist/apply-leg.d.ts.map +1 -1
- package/dist/apply-leg.js +466 -37
- package/dist/apply-leg.js.map +1 -1
- package/dist/embed-socket-path.d.ts +65 -0
- package/dist/embed-socket-path.d.ts.map +1 -0
- package/dist/embed-socket-path.js +100 -0
- package/dist/embed-socket-path.js.map +1 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +105 -3
- package/dist/operations.js.map +1 -1
- package/dist/packed-install-smoke.d.ts +108 -0
- package/dist/packed-install-smoke.d.ts.map +1 -0
- package/dist/packed-install-smoke.js +172 -0
- package/dist/packed-install-smoke.js.map +1 -0
- package/dist/publish-sibling-drift.d.ts +139 -0
- package/dist/publish-sibling-drift.d.ts.map +1 -0
- package/dist/publish-sibling-drift.js +408 -0
- package/dist/publish-sibling-drift.js.map +1 -0
- package/dist/publish.d.ts +44 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +243 -24
- package/dist/publish.js.map +1 -1
- package/dist/qe-bridge.d.ts +16 -0
- package/dist/qe-bridge.d.ts.map +1 -1
- package/dist/qe-bridge.js +1 -0
- package/dist/qe-bridge.js.map +1 -1
- package/dist/release.d.ts +91 -0
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +317 -21
- package/dist/release.js.map +1 -1
- package/dist/setup.d.ts +36 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +96 -2
- package/dist/setup.js.map +1 -1
- package/dist/vector-tier.d.ts +27 -2
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +112 -3
- package/dist/vector-tier.js.map +1 -1
- package/package.json +23 -23
- package/sbom.json +196 -46
- package/src/agentdb-index.ts +65 -17
- package/src/apply-leg.ts +461 -37
- package/src/embed-socket-path.ts +113 -0
- package/src/index.ts +44 -3
- package/src/operations.ts +104 -3
- package/src/packed-install-smoke.ts +273 -0
- package/src/publish-sibling-drift.ts +498 -0
- package/src/publish.ts +280 -25
- package/src/qe-bridge.ts +14 -0
- package/src/release.ts +366 -19
- package/src/setup.ts +108 -2
- package/src/vector-tier.ts +147 -5
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packed-install smoke — feature `publish-sibling-drift-gate`, ADR-001 (Decision 2).
|
|
3
|
+
*
|
|
4
|
+
* `dz release`'s existing smoke gate boots a package's bin straight from the WORKSPACE — its
|
|
5
|
+
* sibling `workspace:*` deps resolve via pnpm's workspace links, never through a real install.
|
|
6
|
+
* That makes the whole class of "published tarball missing an export" incidents invisible by
|
|
7
|
+
* construction (Alternative Б1, rejected). This module plans and judges the alternative
|
|
8
|
+
* (Б2, accepted): pack every package in the batch, `npm install` the resulting tarballs together
|
|
9
|
+
* into a CLEAN directory — siblings OUTSIDE the batch resolve from the registry, exactly like a
|
|
10
|
+
* fresh user's install — then boot every bin with `--version` and require exit 0 AND non-empty
|
|
11
|
+
* stdout (the "publisher output is not a receipt" lesson: a bin that boots but prints nothing has
|
|
12
|
+
* not proven it works).
|
|
13
|
+
*
|
|
14
|
+
* Pure by construction (NFR-2): `planPackedInstallSmoke` only builds command STRINGS from
|
|
15
|
+
* injected package/bin facts and paths — it never spawns anything. `judgePackedInstallSmoke`
|
|
16
|
+
* only classifies injected execution records. The CLI (`cmdPublish`, `cmdRelease`) is the single
|
|
17
|
+
* executor, sharing this same plan/judge pair so both doors apply the identical rule.
|
|
18
|
+
*
|
|
19
|
+
* @packageDocumentation
|
|
20
|
+
*/
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
const DEFAULT_PACK_TIMEOUT_MS = 60_000;
|
|
23
|
+
const DEFAULT_INSTALL_TIMEOUT_MS = 180_000;
|
|
24
|
+
const DEFAULT_VERSION_TIMEOUT_MS = 30_000;
|
|
25
|
+
/** Mirror npm's own tarball naming: `@scope/name@1.2.3` -> `scope-name-1.2.3.tgz`. */
|
|
26
|
+
export function packedTarballName(name, version) {
|
|
27
|
+
return `${name.replace(/^@/, '').replace(/\//g, '-')}-${version}.tgz`;
|
|
28
|
+
}
|
|
29
|
+
export function planPackedInstallSmoke(opts) {
|
|
30
|
+
const packTimeoutMs = opts.packTimeoutMs ?? DEFAULT_PACK_TIMEOUT_MS;
|
|
31
|
+
const installTimeoutMs = opts.installTimeoutMs ?? DEFAULT_INSTALL_TIMEOUT_MS;
|
|
32
|
+
const versionTimeoutMs = opts.versionTimeoutMs ?? DEFAULT_VERSION_TIMEOUT_MS;
|
|
33
|
+
const steps = [];
|
|
34
|
+
const tarballs = [];
|
|
35
|
+
for (const pkg of opts.packages) {
|
|
36
|
+
const tgz = join(opts.packDir, packedTarballName(pkg.name, pkg.version));
|
|
37
|
+
tarballs.push(tgz);
|
|
38
|
+
if (opts.skipPack === true)
|
|
39
|
+
continue; // AM-1: already packed by the caller — see skipPack's doc
|
|
40
|
+
steps.push({
|
|
41
|
+
id: `pack:${pkg.name}`,
|
|
42
|
+
kind: 'pack',
|
|
43
|
+
cmd: `npm pack ${JSON.stringify(pkg.dir)} --pack-destination ${JSON.stringify(opts.packDir)}`,
|
|
44
|
+
cwd: pkg.dir,
|
|
45
|
+
timeoutMs: packTimeoutMs,
|
|
46
|
+
pkg: pkg.name,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (tarballs.length > 0) {
|
|
50
|
+
steps.push({
|
|
51
|
+
id: 'install',
|
|
52
|
+
kind: 'install',
|
|
53
|
+
cmd: `npm install ${tarballs.map((t) => JSON.stringify(t)).join(' ')} --no-audit --no-fund`,
|
|
54
|
+
cwd: opts.installDir,
|
|
55
|
+
timeoutMs: installTimeoutMs,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
for (const bin of opts.bins) {
|
|
59
|
+
const absBinPath = join(opts.installDir, 'node_modules', bin.pkg, bin.relPath);
|
|
60
|
+
// AM-8: a manifest can declare a `bin` whose target file does not exist (never built, moved,
|
|
61
|
+
// typo'd) — the OLD `cli.ts` bin-collection step silently DROPPED such a bin before this
|
|
62
|
+
// amendment, which read as "n/a: nothing to smoke" (or even skipped the whole gate when it
|
|
63
|
+
// was the batch's only bin). `test -f` is a dedicated, portable existence probe RUN AFTER THE
|
|
64
|
+
// REAL INSTALL — pass/fail here is judged into a specific, honest message
|
|
65
|
+
// ("declared bin missing after packed install") instead of being folded into whatever
|
|
66
|
+
// `node <bin> --version` happens to print for a missing file (a generic MODULE_NOT_FOUND).
|
|
67
|
+
steps.push({
|
|
68
|
+
id: `bin-exists:${bin.pkg}:${bin.binName}`,
|
|
69
|
+
kind: 'bin-exists',
|
|
70
|
+
cmd: `test -f ${JSON.stringify(absBinPath)}`,
|
|
71
|
+
cwd: opts.installDir,
|
|
72
|
+
timeoutMs: versionTimeoutMs,
|
|
73
|
+
pkg: bin.pkg,
|
|
74
|
+
binName: bin.binName,
|
|
75
|
+
});
|
|
76
|
+
steps.push({
|
|
77
|
+
id: `bin:${bin.pkg}:${bin.binName}`,
|
|
78
|
+
kind: 'bin-version',
|
|
79
|
+
cmd: `node ${JSON.stringify(absBinPath)} --version`,
|
|
80
|
+
cwd: opts.installDir,
|
|
81
|
+
timeoutMs: versionTimeoutMs,
|
|
82
|
+
pkg: bin.pkg,
|
|
83
|
+
binName: bin.binName,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return { steps, tarballs };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* First 3 lines (FR-3), EXCEPT when they are pure source context. MEASURED 2026-09-13 reproducing
|
|
90
|
+
* the exact incident this gate targets — a bin whose `import` names a missing export — Node
|
|
91
|
+
* prints the location, the offending source line and a caret BEFORE the actual
|
|
92
|
+
* `SyntaxError: … does not provide an export named …` message (identically for a plain uncaught
|
|
93
|
+
* `throw`: file:line / code / `^` / blank / `Error: …`). A literal first-3-lines slice therefore
|
|
94
|
+
* shows three lines of code and caret marks and never the reason — useless for the incident it
|
|
95
|
+
* exists to surface. When stderr contains a line that LOOKS like an error header
|
|
96
|
+
* (`SomethingError: …` or bare `Error: …`), the snippet starts there instead.
|
|
97
|
+
*/
|
|
98
|
+
function firstLines(text, n) {
|
|
99
|
+
const lines = text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
100
|
+
if (lines.length === 0)
|
|
101
|
+
return '(no output)';
|
|
102
|
+
const errorHeaderAt = lines.findIndex((l) => /^[A-Za-z][A-Za-z0-9_]*Error:|^Error\b/.test(l));
|
|
103
|
+
const start = errorHeaderAt >= 0 ? errorHeaderAt : 0;
|
|
104
|
+
return lines.slice(start, start + n).join(' | ');
|
|
105
|
+
}
|
|
106
|
+
const NO_EXECUTION_RECORD = '(no execution record — under-executed plan)';
|
|
107
|
+
function stepOk(exec) {
|
|
108
|
+
return exec !== undefined && exec.timedOut !== true && exec.exitCode === 0;
|
|
109
|
+
}
|
|
110
|
+
function stepDetail(exec) {
|
|
111
|
+
if (exec === undefined)
|
|
112
|
+
return NO_EXECUTION_RECORD;
|
|
113
|
+
if (exec.timedOut === true)
|
|
114
|
+
return 'timed out';
|
|
115
|
+
return firstLines(exec.stderr || exec.stdout, 3);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Classify a plan's executions. `plan` may be the `{ steps }` half of {@link PackedInstallPlan}.
|
|
119
|
+
* A missing execution for a planned step is a FAILURE (an under-executed plan cannot pass) —
|
|
120
|
+
* never treated as "nothing to judge, so it passed".
|
|
121
|
+
*/
|
|
122
|
+
export function judgePackedInstallSmoke(plan, executions) {
|
|
123
|
+
const byId = new Map(executions.map((e) => [e.stepId, e]));
|
|
124
|
+
let packOk = true;
|
|
125
|
+
let failureDetail;
|
|
126
|
+
for (const step of plan.steps.filter((s) => s.kind === 'pack')) {
|
|
127
|
+
const exec = byId.get(step.id);
|
|
128
|
+
if (!stepOk(exec)) {
|
|
129
|
+
packOk = false;
|
|
130
|
+
failureDetail ??= stepDetail(exec);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
let installOk = true;
|
|
134
|
+
const installStep = plan.steps.find((s) => s.kind === 'install');
|
|
135
|
+
if (installStep !== undefined) {
|
|
136
|
+
const exec = byId.get(installStep.id);
|
|
137
|
+
if (!stepOk(exec)) {
|
|
138
|
+
installOk = false;
|
|
139
|
+
failureDetail ??= stepDetail(exec);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const bins = plan.steps
|
|
143
|
+
.filter((s) => s.kind === 'bin-version')
|
|
144
|
+
.map((step) => {
|
|
145
|
+
const exec = byId.get(step.id);
|
|
146
|
+
if (!packOk || !installOk) {
|
|
147
|
+
// Pack/install already failed for the whole batch — the generic pack/install detail is
|
|
148
|
+
// more informative than a bin-specific message about a step that never had a chance to run.
|
|
149
|
+
return { pkg: step.pkg, binName: step.binName, ok: false, stdout: '', detail: stepDetail(exec) };
|
|
150
|
+
}
|
|
151
|
+
// AM-8: a declared bin missing from the REAL post-install tree is its own failure class —
|
|
152
|
+
// checked and reported BEFORE the generic stdout rule below, whose message ("empty stdout")
|
|
153
|
+
// would otherwise misdescribe a file that was never there to boot at all.
|
|
154
|
+
const existsStep = plan.steps.find((s) => s.kind === 'bin-exists' && s.pkg === step.pkg && s.binName === step.binName);
|
|
155
|
+
if (existsStep !== undefined && !stepOk(byId.get(existsStep.id))) {
|
|
156
|
+
return { pkg: step.pkg, binName: step.binName, ok: false, stdout: '', detail: 'declared bin missing after packed install' };
|
|
157
|
+
}
|
|
158
|
+
// FR-3 / lesson "publisher output is not a receipt": exit 0 alone is not enough — the
|
|
159
|
+
// version output must be non-empty, or a bin that silently no-ops would read as healthy.
|
|
160
|
+
const ok = stepOk(exec) && (exec?.stdout ?? '').trim() !== '';
|
|
161
|
+
return {
|
|
162
|
+
pkg: step.pkg,
|
|
163
|
+
binName: step.binName,
|
|
164
|
+
ok,
|
|
165
|
+
stdout: exec?.stdout ?? '',
|
|
166
|
+
...(ok ? {} : { detail: exec !== undefined && stepOk(exec) ? '(exit 0 but empty stdout)' : stepDetail(exec) }),
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
const ok = packOk && installOk && bins.every((b) => b.ok);
|
|
170
|
+
return { ok, packOk, installOk, ...(failureDetail !== undefined ? { failureDetail } : {}), bins };
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=packed-install-smoke.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"packed-install-smoke.js","sourceRoot":"","sources":["../src/packed-install-smoke.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sibling-drift gate — feature `publish-sibling-drift-gate`, ADR-001 (Decision 1).
|
|
3
|
+
*
|
|
4
|
+
* `rewriteWorkspaceSpecs` (publish.ts) pins a sibling `workspace:^`/`workspace:~`/`workspace:*`
|
|
5
|
+
* dependency to the EXACT version currently on disk. That version may be published on the
|
|
6
|
+
* registry carrying an OLDER build than the workspace — the sibling changed without a version
|
|
7
|
+
* bump. The pinned range then resolves at install time to a package that does not match the
|
|
8
|
+
* workspace's current behavior, and a fresh `npm install` reproduces whatever regressed.
|
|
9
|
+
*
|
|
10
|
+
* Detection (ADR-001, Decision 1, alternative А3 — accepted): hash every file under the
|
|
11
|
+
* published tarball's `dist/**` plus its `package.json` (with `version`/`gitHead`/`_*` fields
|
|
12
|
+
* stripped, since those legitimately differ between the registry copy and the workspace copy),
|
|
13
|
+
* and compare against the same hash of the workspace copy. Any difference is drift. A published
|
|
14
|
+
* `dist/index.js` missing an export the workspace's `dist/index.js` declares is surfaced as a
|
|
15
|
+
* SECOND, more readable signal (`missingExports`) — the exact shape of the 2026-09-13 incident
|
|
16
|
+
* ("does not provide an export named …") — but the hash comparison is the load-bearing check:
|
|
17
|
+
* it also catches behavior changes that keep every export name intact.
|
|
18
|
+
*
|
|
19
|
+
* Network access is NOT this module's concern (NFR-2: pure, no network, fixture-testable):
|
|
20
|
+
* `fetchPublished` is injected. The CLI implementation packs the sibling from the registry via
|
|
21
|
+
* `npm pack <name>@<version>` into a temp dir; tests inject a local directory. A fetch that
|
|
22
|
+
* returns `null` (offline, 404, timeout) is reported as `'unavailable'` — never silently treated
|
|
23
|
+
* as `'same'` (the "a gate that infers a pass from silence breaks on the next failure path"
|
|
24
|
+
* lesson): the caller decides whether `'unavailable'` blocks or is overridden.
|
|
25
|
+
*
|
|
26
|
+
* @packageDocumentation
|
|
27
|
+
*/
|
|
28
|
+
export type SiblingDriftStatus = 'same' | 'drift' | 'unavailable';
|
|
29
|
+
/**
|
|
30
|
+
* AM-6 (feature publish-gate-audit-durable): which mechanism produced BOTH sides' file inventory
|
|
31
|
+
* for this comparison — named on every result, never left implicit. `'npm-pack'`: the caller
|
|
32
|
+
* injected {@link DetectSiblingDriftOptions.localInventory} (the CLI's `npm pack --dry-run --json`
|
|
33
|
+
* via {@link parseNpmPackInventory}); the workspace side is exactly what npm will ship, and the
|
|
34
|
+
* published side is hashed by a FULL recursive walk of the already-unpacked tarball (AM-1 — the
|
|
35
|
+
* two sides must be symmetric: "every file npm put there" on one side, "every file npm will put
|
|
36
|
+
* there" on the other). `'readdir-approximation'`: no provider was injected — BOTH sides fall back
|
|
37
|
+
* to the pre-existing `dist`/`files`/`bin` walk ({@link shippedInventoryDirs}), which stays
|
|
38
|
+
* symmetric by construction (same function, same rules, both sides) but can miss a file
|
|
39
|
+
* `.npmignore` excludes or include one npm would never ship.
|
|
40
|
+
*/
|
|
41
|
+
export type InventorySource = 'npm-pack' | 'pnpm-pack' | 'readdir-approximation';
|
|
42
|
+
export interface SiblingDriftResult {
|
|
43
|
+
readonly name: string;
|
|
44
|
+
readonly version: string;
|
|
45
|
+
readonly status: SiblingDriftStatus;
|
|
46
|
+
/** Relative paths (dist/** or package.json) whose hash differs, or is present on only one side. */
|
|
47
|
+
readonly changedFiles: readonly string[];
|
|
48
|
+
/** Export names the workspace's dist/index.js declares that the published one lacks (А2, secondary signal). */
|
|
49
|
+
readonly missingExports: readonly string[];
|
|
50
|
+
/** Present only when status === 'unavailable'. */
|
|
51
|
+
readonly reason?: string;
|
|
52
|
+
/**
|
|
53
|
+
* AM-6: named per-result (not merely per-call) because `detectSiblingDrift` short-circuits to
|
|
54
|
+
* `'unavailable'` before ever reaching the hashing step for some entries — those still carry the
|
|
55
|
+
* source that WOULD have been used, so a reader never has to guess.
|
|
56
|
+
*/
|
|
57
|
+
readonly inventorySource: InventorySource;
|
|
58
|
+
}
|
|
59
|
+
export interface FetchedPublished {
|
|
60
|
+
/** Directory holding the extracted published tarball (contains dist/, package.json). */
|
|
61
|
+
readonly dir: string;
|
|
62
|
+
}
|
|
63
|
+
/** Fetch the published build of `name@version`. `null` = unavailable (network/404/timeout). */
|
|
64
|
+
export type FetchPublished = (name: string, version: string) => FetchedPublished | null;
|
|
65
|
+
export interface DetectSiblingDriftOptions {
|
|
66
|
+
readonly dependencies: Record<string, string> | undefined;
|
|
67
|
+
/** pnpm rewrites `workspace:` in peerDependencies too (mirrors findUnpublishedWorkspaceFloors). */
|
|
68
|
+
readonly peerDependencies?: Record<string, string> | undefined;
|
|
69
|
+
/** AM-3: ships and pins exactly like `dependencies` — checked the same way. */
|
|
70
|
+
readonly optionalDependencies?: Record<string, string> | undefined;
|
|
71
|
+
/** name -> version on DISK, for every package in the workspace. */
|
|
72
|
+
readonly workspaceVersions: ReadonlyMap<string, string>;
|
|
73
|
+
/** name -> absolute package dir on disk, for every package in the workspace. */
|
|
74
|
+
readonly workspaceDirs: ReadonlyMap<string, string>;
|
|
75
|
+
/** Names being published in THIS batch — they publish fresh, so drift cannot be measured against them. */
|
|
76
|
+
readonly batch: ReadonlySet<string>;
|
|
77
|
+
readonly fetchPublished: FetchPublished;
|
|
78
|
+
/**
|
|
79
|
+
* FR-3 (feature publish-gate-audit-durable): the LOCAL (workspace) package's shipped-file
|
|
80
|
+
* inventory, asked from npm instead of approximated by walking `dist`/`files`/`bin` by hand —
|
|
81
|
+
* `.npmignore` and nested ignore rules make the hand-rolled walk wrong in both directions (a file
|
|
82
|
+
* npm will never ship can still be read off disk, producing a false drift). No production default
|
|
83
|
+
* lives in THIS module — core stays pure (never spawns `npm`, per the core-boundary import
|
|
84
|
+
* ratchet). The CLI runs `npm pack --dry-run --json` and hands the stdout to
|
|
85
|
+
* {@link parseNpmPackInventory}, then passes the resulting closure here; a caller that injects
|
|
86
|
+
* nothing (`undefined`) makes `detectSiblingDrift` fall back to the named
|
|
87
|
+
* `'readdir-approximation'` {@link InventorySource} on BOTH sides (AM-1) — never a silent "no
|
|
88
|
+
* drift".
|
|
89
|
+
*/
|
|
90
|
+
readonly localInventory?: LocalInventory;
|
|
91
|
+
/** Label for the injected provider's source (default `'npm-pack'`); the CLI passes `'pnpm-pack'` for a packed tree. */
|
|
92
|
+
readonly localInventorySource?: InventorySource;
|
|
93
|
+
}
|
|
94
|
+
/** The exact set of relative paths `npm pack` will ship for a package — no `.npmignore` guessing. */
|
|
95
|
+
export interface PackInventory {
|
|
96
|
+
readonly paths: readonly string[];
|
|
97
|
+
}
|
|
98
|
+
/** `npm pack --dry-run --json` could not be run or answered in a shape this code cannot use. */
|
|
99
|
+
export interface PackInventoryUnavailable {
|
|
100
|
+
readonly unavailable: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Lead fix after the fix-round's live dry-run (2026-09-14 01:02): the workspace side PACKED BY THE
|
|
104
|
+
* LIVE TRANSPORT (`pnpm pack`) and unpacked into `packedDir`. pnpm synthesises a LICENSE from the
|
|
105
|
+
* workspace root into the tarball of a package whose own tree has none; `npm pack --dry-run --json`
|
|
106
|
+
* never lists that file, so a `paths` inventory read every such sibling as "LICENSE only in the
|
|
107
|
+
* published copy" — 2 false drifts (harness-presets, scout) on a tree unchanged since publication.
|
|
108
|
+
* A packed tree is hashed by the SAME full walk as the published side, symmetric by construction.
|
|
109
|
+
*/
|
|
110
|
+
export interface PackedTree {
|
|
111
|
+
readonly packedDir: string;
|
|
112
|
+
}
|
|
113
|
+
export type LocalInventoryResult = PackInventory | PackedTree | PackInventoryUnavailable;
|
|
114
|
+
/** Ask what npm would ship for the package rooted at `dir`. Injected in tests (no subprocess). */
|
|
115
|
+
export type LocalInventory = (dir: string) => LocalInventoryResult;
|
|
116
|
+
/**
|
|
117
|
+
* C-1/AM-4: `npm pack --dry-run --json` is a real subprocess call — the CLI caches its result per
|
|
118
|
+
* absolute directory for the lifetime of ONE `dz publish` run (not per package being checked), so
|
|
119
|
+
* a run that checks the same sibling from more than one dependent package packs it only once. Core
|
|
120
|
+
* itself never runs the subprocess or owns the cache (core-boundary import ratchet) — this parser
|
|
121
|
+
* is the pure half only.
|
|
122
|
+
*
|
|
123
|
+
* Parses `npm pack --dry-run --json`'s stdout (an array with one element; `files[]` holds
|
|
124
|
+
* `{path,size,mode}` per shipped path, plus `integrity`/`shasum`/`entryCount`) into the exact set of
|
|
125
|
+
* relative paths npm intends to ship, honouring `.npmignore`/`files`/default-ignore exactly the way
|
|
126
|
+
* a real `npm publish` would. A failure to run, parse, or make sense of the shape — including a
|
|
127
|
+
* malformed individual `files[]` element (AM-5: a corrupt entry is a reason to say the WHOLE
|
|
128
|
+
* inventory is untrustworthy, never a file to silently drop) — is `{ unavailable: reason }`: an
|
|
129
|
+
* input this gate cannot read is a reason to say so, never a silent "nothing to compare".
|
|
130
|
+
*/
|
|
131
|
+
export declare function parseNpmPackInventory(stdout: string): LocalInventoryResult;
|
|
132
|
+
/**
|
|
133
|
+
* For every `workspace:`-declared dependency of a package that is NOT part of `batch` (i.e. will
|
|
134
|
+
* be pinned to whatever is already on the registry, not published fresh in this run), compare the
|
|
135
|
+
* build that will be pinned against the workspace copy. Pure: all IO (fetch, fs) is either
|
|
136
|
+
* injected or scoped to reading local dist/package.json files — no network call is made here.
|
|
137
|
+
*/
|
|
138
|
+
export declare function detectSiblingDrift(opts: DetectSiblingDriftOptions): SiblingDriftResult[];
|
|
139
|
+
//# sourceMappingURL=publish-sibling-drift.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish-sibling-drift.d.ts","sourceRoot":"","sources":["../src/publish-sibling-drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAMH,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,OAAO,GAAG,aAAa,CAAC;AAElE;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,WAAW,GAAG,uBAAuB,CAAC;AAEjF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,mGAAmG;IACnG,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,+GAA+G;IAC/G,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,kDAAkD;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;CAC3C;AAED,MAAM,WAAW,gBAAgB;IAC/B,wFAAwF;IACxF,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,+FAA+F;AAC/F,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,gBAAgB,GAAG,IAAI,CAAC;AAExF,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IAC1D,mGAAmG;IACnG,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IAC/D,+EAA+E;IAC/E,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IACnE,mEAAmE;IACnE,QAAQ,CAAC,iBAAiB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxD,gFAAgF;IAChF,QAAQ,CAAC,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpD,0GAA0G;IAC1G,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC;IACzC,uHAAuH;IACvH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,eAAe,CAAC;CACjD;AAoBD,qGAAqG;AACrG,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,gGAAgG;AAChG,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,MAAM,oBAAoB,GAAG,aAAa,GAAG,UAAU,GAAG,wBAAwB,CAAC;AAEzF,kGAAkG;AAClG,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,oBAAoB,CAAC;AAEnE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,oBAAoB,CA0B1E;AAgKD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,yBAAyB,GAAG,kBAAkB,EAAE,CA6IxF"}
|