@dzhechkov/harness-core 0.8.46 → 0.8.47

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 (143) hide show
  1. package/.dz-manifest.json +175 -163
  2. package/README.md +84 -2
  3. package/dist/agentdb-index.d.ts.map +1 -1
  4. package/dist/agentdb-index.js +32 -2
  5. package/dist/agentdb-index.js.map +1 -1
  6. package/dist/agentdb-snapshot.d.ts +14 -1
  7. package/dist/agentdb-snapshot.d.ts.map +1 -1
  8. package/dist/agentdb-snapshot.js +100 -4
  9. package/dist/agentdb-snapshot.js.map +1 -1
  10. package/dist/apply-leg.d.ts +9 -1
  11. package/dist/apply-leg.d.ts.map +1 -1
  12. package/dist/apply-leg.js +26 -4
  13. package/dist/apply-leg.js.map +1 -1
  14. package/dist/architecture.d.ts +0 -5
  15. package/dist/architecture.d.ts.map +1 -1
  16. package/dist/architecture.js +25 -2
  17. package/dist/architecture.js.map +1 -1
  18. package/dist/codex-rollouts.d.ts +35 -7
  19. package/dist/codex-rollouts.d.ts.map +1 -1
  20. package/dist/codex-rollouts.js +201 -113
  21. package/dist/codex-rollouts.js.map +1 -1
  22. package/dist/cost-ledger.d.ts +42 -3
  23. package/dist/cost-ledger.d.ts.map +1 -1
  24. package/dist/cost-ledger.js +478 -15
  25. package/dist/cost-ledger.js.map +1 -1
  26. package/dist/feature-adr-routing.d.ts +16 -0
  27. package/dist/feature-adr-routing.d.ts.map +1 -1
  28. package/dist/feature-adr-routing.js +21 -0
  29. package/dist/feature-adr-routing.js.map +1 -1
  30. package/dist/guard.d.ts +7 -0
  31. package/dist/guard.d.ts.map +1 -1
  32. package/dist/guard.js +48 -0
  33. package/dist/guard.js.map +1 -1
  34. package/dist/index.d.ts +11 -6
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +10 -3
  37. package/dist/index.js.map +1 -1
  38. package/dist/loop-plan.d.ts +4 -0
  39. package/dist/loop-plan.d.ts.map +1 -1
  40. package/dist/loop-plan.js +4 -0
  41. package/dist/loop-plan.js.map +1 -1
  42. package/dist/mutation-gate.d.ts +36 -1
  43. package/dist/mutation-gate.d.ts.map +1 -1
  44. package/dist/mutation-gate.js +109 -19
  45. package/dist/mutation-gate.js.map +1 -1
  46. package/dist/npm-homepage.d.ts +64 -0
  47. package/dist/npm-homepage.d.ts.map +1 -0
  48. package/dist/npm-homepage.js +109 -0
  49. package/dist/npm-homepage.js.map +1 -0
  50. package/dist/operations.d.ts +6 -0
  51. package/dist/operations.d.ts.map +1 -1
  52. package/dist/operations.js +150 -24
  53. package/dist/operations.js.map +1 -1
  54. package/dist/pack-inventory.d.ts.map +1 -1
  55. package/dist/pack-inventory.js +6 -1
  56. package/dist/pack-inventory.js.map +1 -1
  57. package/dist/parity.d.ts.map +1 -1
  58. package/dist/parity.js +4 -1
  59. package/dist/parity.js.map +1 -1
  60. package/dist/publish-sibling-drift.d.ts +43 -1
  61. package/dist/publish-sibling-drift.d.ts.map +1 -1
  62. package/dist/publish-sibling-drift.js +269 -13
  63. package/dist/publish-sibling-drift.js.map +1 -1
  64. package/dist/publish.d.ts +5 -1
  65. package/dist/publish.d.ts.map +1 -1
  66. package/dist/publish.js +18 -1
  67. package/dist/publish.js.map +1 -1
  68. package/dist/qe-bridge.d.ts +63 -1
  69. package/dist/qe-bridge.d.ts.map +1 -1
  70. package/dist/qe-bridge.js +82 -0
  71. package/dist/qe-bridge.js.map +1 -1
  72. package/dist/release-package-audit.d.ts +86 -0
  73. package/dist/release-package-audit.d.ts.map +1 -0
  74. package/dist/release-package-audit.js +272 -0
  75. package/dist/release-package-audit.js.map +1 -0
  76. package/dist/release.d.ts +8 -1
  77. package/dist/release.d.ts.map +1 -1
  78. package/dist/release.js +58 -32
  79. package/dist/release.js.map +1 -1
  80. package/dist/round.d.ts +10 -1
  81. package/dist/round.d.ts.map +1 -1
  82. package/dist/round.js +5 -1
  83. package/dist/round.js.map +1 -1
  84. package/dist/run-records.d.ts +11 -0
  85. package/dist/run-records.d.ts.map +1 -1
  86. package/dist/run-records.js +124 -20
  87. package/dist/run-records.js.map +1 -1
  88. package/dist/stage-usage.d.ts +148 -0
  89. package/dist/stage-usage.d.ts.map +1 -0
  90. package/dist/stage-usage.js +261 -0
  91. package/dist/stage-usage.js.map +1 -0
  92. package/dist/statusline.d.ts +23 -0
  93. package/dist/statusline.d.ts.map +1 -1
  94. package/dist/statusline.js +167 -1
  95. package/dist/statusline.js.map +1 -1
  96. package/dist/workflow-run-dispatch.d.ts +21 -19
  97. package/dist/workflow-run-dispatch.d.ts.map +1 -1
  98. package/dist/workflow-run-dispatch.js +206 -95
  99. package/dist/workflow-run-dispatch.js.map +1 -1
  100. package/dist/workflow-run.d.ts +12 -3
  101. package/dist/workflow-run.d.ts.map +1 -1
  102. package/dist/workflow-run.js +29 -2
  103. package/dist/workflow-run.js.map +1 -1
  104. package/package.json +3 -3
  105. package/sbom.json +264 -234
  106. package/src/agentdb-index.ts +31 -2
  107. package/src/agentdb-snapshot.ts +105 -4
  108. package/src/apply-leg.ts +26 -4
  109. package/src/architecture.ts +15 -2
  110. package/src/codex-rollouts.ts +155 -147
  111. package/src/cost-ledger.ts +308 -19
  112. package/src/feature-adr-routing.ts +22 -0
  113. package/src/guard.ts +52 -0
  114. package/src/index.ts +15 -3
  115. package/src/loop-plan.ts +8 -0
  116. package/src/mutation-gate.ts +153 -19
  117. package/src/npm-homepage.ts +142 -0
  118. package/src/operations.ts +177 -22
  119. package/src/pack-inventory.ts +6 -1
  120. package/src/parity.ts +4 -1
  121. package/src/publish-sibling-drift.ts +214 -11
  122. package/src/publish.ts +20 -1
  123. package/src/qe-bridge.ts +86 -1
  124. package/src/release-package-audit.ts +264 -0
  125. package/src/release.ts +55 -22
  126. package/src/round.ts +8 -0
  127. package/src/run-records.ts +101 -22
  128. package/src/stage-usage.ts +191 -0
  129. package/src/statusline.ts +149 -1
  130. package/src/workflow-run-dispatch.ts +148 -89
  131. package/src/workflow-run.ts +40 -4
  132. package/dist/ledger-cost-fill.d.ts +0 -58
  133. package/dist/ledger-cost-fill.d.ts.map +0 -1
  134. package/dist/ledger-cost-fill.js +0 -78
  135. package/dist/ledger-cost-fill.js.map +0 -1
  136. package/dist/retro.d.ts +0 -131
  137. package/dist/retro.d.ts.map +0 -1
  138. package/dist/retro.js +0 -207
  139. package/dist/retro.js.map +0 -1
  140. package/dist/sbom.d.ts +0 -42
  141. package/dist/sbom.d.ts.map +0 -1
  142. package/dist/sbom.js +0 -120
  143. package/dist/sbom.js.map +0 -1
package/src/operations.ts CHANGED
@@ -18,12 +18,14 @@ import {
18
18
  buildCodexHookManifest,
19
19
  codexHooksPaths,
20
20
  diffCodexHooks,
21
+ looksLikeDzEntry,
21
22
  parseCodexHookManifest,
22
23
  planCodexHooks,
23
24
  removeCodexHooks,
24
25
  selectOwnHookMetadata,
25
26
  upsertTrustBlock,
26
27
  type CodexHooksPaths,
28
+ type CodexRegistry,
27
29
  type HooksListHookMetadata,
28
30
  type ManagedEntry,
29
31
  } from './codex-hooks.js';
@@ -1922,6 +1924,12 @@ export interface CodexHooksSyncOptions {
1922
1924
  readonly probeModel?: string;
1923
1925
  /** Test seam: replace the live probe. Production runs {@link runCodexVetoProbe}. */
1924
1926
  readonly probe?: (options: CodexVetoProbeOptions) => CodexVetoProbeRun;
1927
+ /**
1928
+ * Test seam (codex-hooks-remove-never-orphans, review r1 item 1): how a helper body reaches disk.
1929
+ * Production writes atomically; a test injects a failing writer (ENOSPC) to prove the section-2
1930
+ * restore fails CLOSED. Nothing else in the operation goes through it.
1931
+ */
1932
+ readonly writeHelperFile?: (path: string, body: string) => void;
1925
1933
  }
1926
1934
 
1927
1935
  export interface CodexHooksSyncReport {
@@ -2204,24 +2212,44 @@ export function runSyncCodexHooks(options: CodexHooksSyncOptions = {}): CodexHoo
2204
2212
  // 2 deleted them after the lock was released, so a concurrent installer could observe (and
2205
2213
  // rebuild against) a registry that had already been emptied — or leave a manifest describing
2206
2214
  // entries that no longer exist. One decision, one critical section.
2207
- if (planned.ok) {
2208
- for (const p of [paths.vetoHelper, paths.recallHelper, paths.manifest]) rmSync(p, { force: true });
2209
- }
2210
- return planned;
2215
+ //
2216
+ // codex-hooks-remove-never-orphans (ADR-001): the helpers go ONLY when nothing left in the
2217
+ // registry still points at them. Keyed on the OBSERVED fact — dz-like entries surviving this
2218
+ // plan — not on a flag: an installer between its two sections (no manifest yet), or a manifest
2219
+ // deleted by hand, leaves entries we cannot attribute; deleting their helpers anyway turned the
2220
+ // entry into `exit 127`, which Codex reads as ALLOW. A dead reference is worse than a stray file.
2221
+ //
2222
+ // Review r1 item 3: "nothing left points at them" is measured on the registry AS IT WILL BE
2223
+ // WRITTEN — every surviving handler, attributed-and-salvaged or not. `unattributable` counts
2224
+ // whole unattributed entries only; a handler salvaged from an attributed matcher group (another
2225
+ // interpreter sharing these helpers) would slip past it and lose its helper.
2226
+ if (!planned.ok) return { planned, references: 0 };
2227
+ const references = dzHelperReferences(planned.result.registry);
2228
+ const doomed = references > 0 ? [paths.manifest] : [paths.vetoHelper, paths.recallHelper, paths.manifest];
2229
+ for (const p of doomed) rmSync(p, { force: true });
2230
+ return { planned, references };
2211
2231
  });
2212
- if (!removal.ok) return { ...base, codexVersion, exitCode: 1, warnings, errors: [removal.error] };
2232
+ const planned = removal.planned;
2233
+ if (!planned.ok) return { ...base, codexVersion, exitCode: 1, warnings, errors: [planned.error] };
2234
+ // A remove that leaves dz references in the registry did not remove dz — it must not say it did.
2235
+ const kept = removal.references;
2213
2236
  return {
2214
2237
  ...base,
2215
2238
  codexVersion,
2216
- written: removal.result.changed,
2217
- removed: removal.result.removed,
2218
- unattributable: removal.result.unattributable,
2219
- exitCode: 0,
2220
- warnings:
2221
- removal.result.unattributable > 0
2222
- ? [`${removal.result.unattributable} entr(ies) resemble dz hooks but are not manifest-attributed — KEPT, remove them by hand if you want them gone`]
2223
- : warnings,
2224
- errors,
2239
+ written: planned.result.changed,
2240
+ removed: planned.result.removed,
2241
+ unattributable: planned.result.unattributable,
2242
+ exitCode: kept > 0 ? 1 : 0,
2243
+ warnings,
2244
+ errors:
2245
+ kept > 0
2246
+ ? [
2247
+ `remove-unattributed-entries-kept: ${kept} handler(s) in ${paths.registry} still reference the dz helpers but are ` +
2248
+ 'not manifest-attributed, so they were KEPT — and the helper files they reference were KEPT with them ' +
2249
+ '(deleting a referenced helper makes the hook exit 127, which Codex treats as ALLOW). An install may be ' +
2250
+ 'in flight: let it finish and re-run `--remove`; otherwise delete those entries by hand, then re-run.',
2251
+ ]
2252
+ : errors,
2225
2253
  writes,
2226
2254
  };
2227
2255
  }
@@ -2305,8 +2333,8 @@ export function runSyncCodexHooks(options: CodexHooksSyncOptions = {}): CodexHoo
2305
2333
  if (!plan.ok) return { plan, wrote: false as const, freshText };
2306
2334
  mkdirSync(paths.helperDir, { recursive: true, mode: 0o700 });
2307
2335
  const helpers = generateCodexHelpers();
2308
- writeHelperIfChanged(paths.vetoHelper, helpers.veto, writes);
2309
- writeHelperIfChanged(paths.recallHelper, helpers.recall, writes);
2336
+ writeHelperIfChanged(paths.vetoHelper, helpers.veto, writes, options.writeHelperFile);
2337
+ writeHelperIfChanged(paths.recallHelper, helpers.recall, writes, options.writeHelperFile);
2310
2338
  if (plan.plan.changed) {
2311
2339
  backupRegistry(paths, freshText, now, writes);
2312
2340
  atomicWrite(paths.registry, plan.plan.text);
@@ -2368,13 +2396,58 @@ export function runSyncCodexHooks(options: CodexHooksSyncOptions = {}): CodexHoo
2368
2396
  const nowText = existsSync(paths.registry) ? readFileSync(paths.registry, 'utf8') : undefined;
2369
2397
  const nowDrift = diffCodexHooks(nowText, entries, undefined);
2370
2398
  if (!nowDrift.installed) {
2371
- return { wrote: false, drift: nowDrift };
2399
+ return { wrote: false, drift: nowDrift, restored: false, restoreFailure: null };
2372
2400
  }
2401
+ // codex-hooks-remove-never-orphans (ADR-001): the entries are ours and present — so are their
2402
+ // helpers, or the registry points into nothing (exit 127 = ALLOW). Something that ignores the lock
2403
+ // (an older dz's `--remove`, a hand) may have deleted or altered them since section one. Re-assert
2404
+ // them HERE, under the lock, before the manifest calls this install complete. Two small writes,
2405
+ // no probe: the section stays a short transaction.
2406
+ const before = writes.length;
2407
+ try {
2408
+ mkdirSync(paths.helperDir, { recursive: true, mode: 0o700 });
2409
+ const helpers = generateCodexHelpers();
2410
+ writeHelperIfChanged(paths.vetoHelper, helpers.veto, writes, options.writeHelperFile);
2411
+ writeHelperIfChanged(paths.recallHelper, helpers.recall, writes, options.writeHelperFile);
2412
+ } catch (err) {
2413
+ // Review r1 item 1: the restore FAILED (ENOSPC, a file where the helper dir should be, …). The
2414
+ // entries this install wrote now point at helpers that may not exist — fail CLOSED on the
2415
+ // invariant: take OUR entries back out of the registry (same lock, same transaction), write no
2416
+ // manifest, and let the report say why. Foreign entries are untouched by construction:
2417
+ // `removeCodexHooks` attributes by the hashes of the commands this very run emitted.
2418
+ return {
2419
+ wrote: false,
2420
+ drift: nowDrift,
2421
+ restored: false,
2422
+ restoreFailure: rollBackOwnEntries(paths, entries, nowText, manifestText, `${now}-rollback`, writes, String((err as Error)?.message ?? err)),
2423
+ };
2424
+ }
2425
+ const restored = writes.length > before;
2373
2426
  atomicWrite(paths.manifest, manifestText);
2374
2427
  writes.push(paths.manifest);
2375
- return { wrote: true, drift: nowDrift };
2428
+ return { wrote: true, drift: nowDrift, restored, restoreFailure: null };
2376
2429
  });
2377
- if (!manifestOutcome.wrote) {
2430
+ if (manifestOutcome.restoreFailure !== null) {
2431
+ const f = manifestOutcome.restoreFailure;
2432
+ errors.push(
2433
+ `helpers-restore-failed: the helper files were missing or altered after the registry transaction and could not be ` +
2434
+ `rewritten (${f.cause}). ` +
2435
+ (f.rolledBack
2436
+ ? 'This install\u2019s own entries were REMOVED from the registry again, so none of THEM references a missing helper (exit 127 = ALLOW); no manifest was written.'
2437
+ : `Rolling this install\u2019s entries back ALSO failed (${f.rollbackError ?? 'unknown'}) — ${paths.registry} may reference a missing helper: fix the disk and re-run, or run \`--remove\`.`) +
2438
+ (f.missing.length > 0
2439
+ ? ` Missing helper path(s): ${f.missing.join(', ')}. Handlers not written by this run that reference these paths were left untouched and will exit 127 (Codex reads that as ALLOW) — run \`dz hooks-sync --target codex --check\` and repair.`
2440
+ : ''),
2441
+ );
2442
+ }
2443
+ if (manifestOutcome.restored) {
2444
+ warnings.push(
2445
+ 'helpers-restored: the helper files were missing or altered between the registry transaction and the manifest ' +
2446
+ 'write (an older dz `--remove`, or a process that ignores the codex-hooks lock) — they were rewritten under the lock so the ' +
2447
+ 'registry never references a helper that does not exist.',
2448
+ );
2449
+ }
2450
+ if (!manifestOutcome.wrote && manifestOutcome.restoreFailure === null) {
2378
2451
  warnings.push(
2379
2452
  'the registry no longer carries this install\u2019s entries — a concurrent remover won the window between the ' +
2380
2453
  'registry transaction and the manifest write, so NO manifest was written (a manifest describing entries that are ' +
@@ -2382,7 +2455,7 @@ export function runSyncCodexHooks(options: CodexHooksSyncOptions = {}): CodexHoo
2382
2455
  );
2383
2456
  }
2384
2457
 
2385
- const drift = manifestOutcome.drift;
2458
+ const drift = manifestOutcome.restoreFailure !== null ? manifestOutcome.restoreFailure.drift : manifestOutcome.drift;
2386
2459
  const installed = drift.installed;
2387
2460
  const armedState = installed && executable && trustResult.trust === 'trusted';
2388
2461
  // The install is not finished when the file is written — it is finished when the guard has been
@@ -2615,13 +2688,95 @@ function atomicWrite(path: string, text: string): void {
2615
2688
  }
2616
2689
 
2617
2690
  /** 0600: the helpers are invoked as `node <path>`, never executed directly (AM-35c). */
2618
- function writeHelperIfChanged(path: string, body: string, writes: string[]): void {
2691
+ function writeHelperIfChanged(
2692
+ path: string,
2693
+ body: string,
2694
+ writes: string[],
2695
+ write: (path: string, body: string) => void = atomicWrite,
2696
+ ): void {
2619
2697
  const current = existsSync(path) ? readFileSync(path, 'utf8') : undefined;
2620
2698
  if (current === body) return; // byte-idempotence: no change ⇒ no write ⇒ trust survives (FR-2)
2621
- atomicWrite(path, body);
2699
+ write(path, body);
2622
2700
  writes.push(path);
2623
2701
  }
2624
2702
 
2703
+ /**
2704
+ * Handlers in a registry that reference the dz helper files (codex-hooks-remove-never-orphans).
2705
+ * Counted per HANDLER, over every surviving entry — so a foreign-looking handler salvaged from an
2706
+ * attributed matcher group still counts as a reference.
2707
+ */
2708
+ function dzHelperReferences(registry: CodexRegistry): number {
2709
+ let n = 0;
2710
+ for (const list of Object.values(registry.hooks)) {
2711
+ for (const entry of list) {
2712
+ const e = entry as { hooks?: unknown };
2713
+ const handlers = Array.isArray(e?.hooks) ? (e.hooks as unknown[]) : [entry];
2714
+ for (const h of handlers) if (looksLikeDzEntry(h)) n += 1;
2715
+ }
2716
+ }
2717
+ return n;
2718
+ }
2719
+
2720
+ interface HelperRestoreFailure {
2721
+ readonly cause: string;
2722
+ readonly rolledBack: boolean;
2723
+ readonly rollbackError: string | null;
2724
+ /** Helper paths of this home that are absent on disk after the failed restore. */
2725
+ readonly missing: readonly string[];
2726
+ readonly drift: ReturnType<typeof diffCodexHooks>;
2727
+ }
2728
+
2729
+ /**
2730
+ * Section-2 fail-closed path: remove exactly the entries THIS run emitted (attributed by the hashes
2731
+ * in the manifest it was about to write) and any stale manifest, under the caller's lock.
2732
+ */
2733
+ function rollBackOwnEntries(
2734
+ paths: CodexHooksPaths,
2735
+ entries: readonly ManagedEntry[],
2736
+ registryText: string | undefined,
2737
+ manifestText: string,
2738
+ backupStamp: string,
2739
+ writes: string[],
2740
+ cause: string,
2741
+ ): HelperRestoreFailure {
2742
+ const ours = parseCodexHookManifest(manifestText);
2743
+ const planned = removeCodexHooks(registryText, ours);
2744
+ let rolledBack = false;
2745
+ let rollbackError: string | null = null;
2746
+ let finalText = registryText;
2747
+ if (!planned.ok) {
2748
+ rollbackError = planned.error;
2749
+ } else {
2750
+ try {
2751
+ if (planned.result.changed) {
2752
+ backupRegistry(paths, registryText, backupStamp, writes);
2753
+ atomicWrite(paths.registry, planned.result.text);
2754
+ writes.push(paths.registry);
2755
+ }
2756
+ finalText = planned.result.text;
2757
+ rolledBack = true;
2758
+ } catch (err) {
2759
+ rollbackError = String((err as Error)?.message ?? err);
2760
+ }
2761
+ }
2762
+ // A stale manifest from an earlier install would now describe entries that are gone.
2763
+ try {
2764
+ rmSync(paths.manifest, { force: true });
2765
+ } catch { /* the helper dir may itself be the obstacle (a file) — nothing to describe there */ }
2766
+ // Named, never pruned (AM-6, review r3): a handler this run did not write is left untouched even if
2767
+ // it references a missing helper. Deciding "does this shell command execute our helper?" needs a
2768
+ // shell parser; a substring match misfired BOTH ways (removed a valid foreign guard that merely
2769
+ // mentioned the path; missed an escaped path). Detection is `--check`'s job.
2770
+ const missing = [paths.vetoHelper, paths.recallHelper].filter((p) => !existsSync(p));
2771
+ return {
2772
+ cause,
2773
+ rolledBack,
2774
+ rollbackError,
2775
+ missing,
2776
+ drift: diffCodexHooks(finalText, entries, undefined),
2777
+ };
2778
+ }
2779
+
2625
2780
  function backupRegistry(paths: CodexHooksPaths, currentText: string | undefined, now: string, writes: string[]): void {
2626
2781
  if (currentText === undefined) return;
2627
2782
  const backup = `${paths.registry}.bak-${now.replace(/[:.]/g, '-')}`;
@@ -253,7 +253,12 @@ export function listPublishInventory(packageRoot: string, opts: ListPublishInven
253
253
  packageRoot = resolve(packageRoot);
254
254
  const packer = opts.packer ?? 'npm';
255
255
  const exec = opts.exec ?? defaultExec;
256
- const timeoutMs = opts.timeoutMs ?? 15_000;
256
+ // Default packer ceiling = 3 × the max `npm pack --dry-run --json` measured UNDER LOAD, rounded up to 1 000 ms.
257
+ // MEASURED 2026-09-26 (features/secret-scan-tails/07_code_changes/packer-timing-under-load.md): two sweeps over
258
+ // all 56 non-private packages/@dzhechkov/* while `npx vitest run` of harness-core ran (load1 2.38–7.73, 8 CPUs):
259
+ // max 5 358 ms (@dzhechkov/harness-core), p95 2 692 ms over 112 calls ⇒ 3 × 5 358 = 16 074 > 15 000 ⇒ 17 000.
260
+ // Supersedes the UNLOADED 15 000 ms (max 3 994 ms, parent AM-2). Reproducer: that file's measure script.
261
+ const timeoutMs = opts.timeoutMs ?? 17_000;
257
262
  let version: string | null = null;
258
263
  const fallback = (reason: string): PublishInventory => {
259
264
  const files = listPackFiles(packageRoot);
package/src/parity.ts CHANGED
@@ -397,7 +397,10 @@ export const PARITY_FEATURES: readonly ParityFeature[] = [
397
397
  {
398
398
  id: 'statusline-gates',
399
399
  title: 'Live 🚦 gates status line',
400
- forms: [{ form: 'statusline integration', requires: ['statusline'], level: 'full' }],
400
+ forms: [
401
+ { form: 'statusline integration', requires: ['statusline'], level: 'full' },
402
+ { form: 'dz statusline --watch (adjacent terminal)', requires: ['shell'], level: 'manual' },
403
+ ],
401
404
  },
402
405
  {
403
406
  id: 'project-guards',
@@ -26,9 +26,12 @@
26
26
  * @packageDocumentation
27
27
  */
28
28
 
29
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
29
+ import { existsSync, readFileSync, readdirSync, statSync, lstatSync } from 'node:fs';
30
30
  import { createHash } from 'node:crypto';
31
31
  import { join, relative, isAbsolute } from 'node:path';
32
+ import { judgeReleaseCohortAudit } from './release-package-audit.js';
33
+ import { rewriteWorkspaceSpecs } from './pack-artifact.js';
34
+ import { hashPackBytes, verifyManifest, isSafeManifestPath, type SignedManifest } from './sign.js';
32
35
 
33
36
  export type SiblingDriftStatus = 'same' | 'drift' | 'unavailable';
34
37
 
@@ -54,7 +57,7 @@ export interface SiblingDriftResult {
54
57
  readonly changedFiles: readonly string[];
55
58
  /** Export names the workspace's dist/index.js declares that the published one lacks (А2, secondary signal). */
56
59
  readonly missingExports: readonly string[];
57
- /** Present only when status === 'unavailable'. */
60
+ /** Unavailable reason or diagnostic for a verified/declined owner-branding comparison. */
58
61
  readonly reason?: string;
59
62
  /**
60
63
  * AM-6: named per-result (not merely per-call) because `detectSiblingDrift` short-circuits to
@@ -62,6 +65,8 @@ export interface SiblingDriftResult {
62
65
  * source that WOULD have been used, so a reader never has to guess.
63
66
  */
64
67
  readonly inventorySource: InventorySource;
68
+ /** Verified directional policy class; SAME here does not mean byte-identical. */
69
+ readonly classification?: 'owner-branding-only' | 'retained-registered-binding';
65
70
  }
66
71
 
67
72
  export interface FetchedPublished {
@@ -100,18 +105,46 @@ export interface DetectSiblingDriftOptions {
100
105
  readonly localInventory?: LocalInventory;
101
106
  /** Label for the injected provider (default 'npm-pack'); CLI uses 'pack-artifact'. */
102
107
  readonly localInventorySource?: InventorySource;
108
+ /** Repository trust root, supplied externally; never read from either artifact. */
109
+ readonly trustedPublicKeyPem?: string;
110
+ /** Structural validation seam only; normal CLI additionally requires its completed invocation-owned object. */
111
+ readonly retainedBindingProof?: {
112
+ readonly version: 1; readonly phase: 'preview' | 'final'; readonly batch: readonly string[];
113
+ readonly head: string; readonly refs: readonly { ref: string; oid: string }[];
114
+ readonly workspaceVersions: readonly (readonly [string, string])[];
115
+ readonly adapters: Readonly<Record<string, {
116
+ sourceSha256: string; localTreeSha256: string; registryTreeSha256: string; registryIntegrity: string;
117
+ version: string; localFloor: string; registeredFloor: string; creation: string;
118
+ history: readonly { oid: string; blob: string | null; literal: string | null; boundary: boolean }[];
119
+ }>>;
120
+ readonly baseline: { roots: Parameters<typeof judgeReleaseCohortAudit>[0]; evidence: unknown };
121
+ readonly candidate: { roots: Parameters<typeof judgeReleaseCohortAudit>[0]; evidence: unknown };
122
+ readonly digest: string;
123
+ };
124
+
103
125
  }
104
126
 
105
- function listFilesRecursive(root: string, dir: string): string[] {
106
- if (!existsSync(dir)) return [];
107
- const out: string[] = [];
108
- for (const entry of readdirSync(dir).sort()) {
109
- const abs = join(dir, entry);
110
- const st = statSync(abs);
111
- if (st.isDirectory()) out.push(...listFilesRecursive(root, abs));
112
- else out.push(relative(root, abs));
127
+ function listFilesRecursive(root: string, dir: string, strictPacked = false): string[] {
128
+ try {
129
+ if (!existsSync(dir)) { if (strictPacked) throw new InventoryListingError('missing packed tree'); return []; }
130
+ if (strictPacked && !lstatSync(dir).isDirectory()) throw new InventoryListingError('packed directory is a symlink or unsupported entry');
131
+ const out: string[] = []; const seen = new Set<string>();
132
+ for (const entry of readdirSync(dir).sort()) {
133
+ const abs = join(dir, entry); const path = relative(root, abs);
134
+ const st = strictPacked ? lstatSync(abs) : statSync(abs);
135
+ if (strictPacked) {
136
+ if (!isSafeManifestPath(path) || seen.has(path.toLowerCase())) throw new InventoryListingError('noncanonical or aliased packed path: ' + path);
137
+ seen.add(path.toLowerCase());
138
+ if (!st.isDirectory() && !st.isFile()) throw new InventoryListingError('symlink or special packed entry: ' + path);
139
+ }
140
+ if (st.isDirectory()) out.push(...listFilesRecursive(root, abs, strictPacked));
141
+ else out.push(path);
142
+ }
143
+ return out;
144
+ } catch (error) {
145
+ if (strictPacked && !(error instanceof InventoryListingError)) throw new InventoryListingError('physical packed inventory unreadable: ' + String(error));
146
+ throw error;
113
147
  }
114
- return out;
115
148
  }
116
149
 
117
150
  function sha256(data: string | Buffer): string {
@@ -251,6 +284,140 @@ function readManifest(dir: string): Record<string, unknown> | null {
251
284
  }
252
285
  }
253
286
 
287
+ /** Readonly exception for two original, fully verified artifact trees. Every other delta refuses. */
288
+ function classifyOwnerBrandingDelta(
289
+ localDir: string, registryDir: string, name: string, version: string,
290
+ publicKey: string | undefined, declaredPaths: readonly string[] | undefined, retained?: { localFloor: string; registeredFloor: string },
291
+ ): { accepted: boolean; coverageVerified: boolean; changedFiles: string[]; reason: string } {
292
+ let coverageVerified = false;
293
+ const decline = (detail: string) => ({ accepted: false, coverageVerified, changedFiles: [], reason: 'owner-branding not proven: ' + detail });
294
+ if (!publicKey) return decline('no trusted external public key');
295
+ try {
296
+ for (const dir of [localDir, registryDir]) {
297
+ for (const path of ['.dz-manifest.json', 'sbom.json', 'package.json', 'README.md']) {
298
+ if (!lstatSync(join(dir, path)).isFile()) return decline('original metadata is not a regular file');
299
+ }
300
+ }
301
+ const localSignedBytes = readFileSync(join(localDir, '.dz-manifest.json'), 'utf8');
302
+ const registrySignedBytes = readFileSync(join(registryDir, '.dz-manifest.json'), 'utf8');
303
+ const localSigned = JSON.parse(localSignedBytes) as SignedManifest;
304
+ const registrySigned = JSON.parse(registrySignedBytes) as SignedManifest;
305
+ // No projected/re-signed object reaches the verifier. Both untouched inventories and SBOMs first.
306
+ if (!verifyManifest(localDir, localSigned, publicKey).ok || !verifyManifest(registryDir, registrySigned, publicKey).ok) return decline('original signature, SBOM or full signed inventory failed verification');
307
+ for (const [signed, bytes] of [[localSigned, localSignedBytes], [registrySigned, registrySignedBytes]] as const) {
308
+ const serializations = [JSON.stringify(signed), JSON.stringify(signed, null, 2)];
309
+ if (!serializations.some(text => bytes === text || bytes === text + '\n')) return decline('manifest serialization has unexplained or ambiguous bytes');
310
+ if (Buffer.from(signed.signature, 'base64').toString('base64') !== signed.signature) return decline('signature encoding is not canonical');
311
+ }
312
+ const paths = listFilesRecursive(localDir, localDir, true).sort();
313
+ const registryPaths = listFilesRecursive(registryDir, registryDir, true).sort();
314
+ if (JSON.stringify(paths) !== JSON.stringify(registryPaths)) return decline('artifact file sets differ');
315
+ if (declaredPaths !== undefined && JSON.stringify([...declaredPaths].sort()) !== JSON.stringify(paths)) return decline('declared packed inventory is incomplete or duplicated');
316
+ // Installed/source verifier exclusions are not packed-artifact signing coverage.
317
+ // Every physical regular leaf is authenticated; only the two validated ROOT metadata files self-exempt.
318
+ for (const [signed, physical] of [[localSigned, paths], [registrySigned, registryPaths]] as const) {
319
+ const signedPaths = signed.manifest.files.map(entry => entry.path).sort();
320
+ if (signedPaths.some(path => !isSafeManifestPath(path) || path === '.dz-manifest.json' || path === 'sbom.json')) return decline('invalid signed leaf or generated-root self-reference');
321
+ const expected = physical.filter(path => path !== '.dz-manifest.json' && path !== 'sbom.json');
322
+ if (JSON.stringify(signedPaths) !== JSON.stringify(expected)) return decline('physical packed leaves are not fully covered by the signed inventory');
323
+ }
324
+ coverageVerified = true;
325
+ const localPkgBytes = readFileSync(join(localDir, 'package.json'));
326
+ const registryPkgBytes = readFileSync(join(registryDir, 'package.json'));
327
+ const localPkg = JSON.parse(localPkgBytes.toString('utf8')) as Record<string, unknown>;
328
+ const registryPkg = JSON.parse(registryPkgBytes.toString('utf8')) as Record<string, unknown>;
329
+ if (localPkg['name'] !== name || registryPkg['name'] !== name || localPkg['version'] !== version || registryPkg['version'] !== version) return decline('package identity/version does not bind both artifacts');
330
+ const source = 'https://github.com/djd1m/dz-harness/tree/main/packages/' + name;
331
+ if (localPkg['homepage'] !== 'https://aicoding.space' || !['https://aicoding.space', source + '#readme'].includes(String(registryPkg['homepage']))) return decline('homepage is not the exact directional owner policy');
332
+ const projectedPkg: Record<string, unknown> = { ...localPkg, homepage: registryPkg['homepage'] };
333
+ if (retained !== undefined) {
334
+ const left = localPkg['dependencies'] as Record<string, unknown> | undefined;
335
+ const right = registryPkg['dependencies'] as Record<string, unknown> | undefined;
336
+ if (!left || !right || left['@dzhechkov/core'] !== retained.localFloor || right['@dzhechkov/core'] !== retained.registeredFloor) return decline('retained exact dependency binding contradicts originals');
337
+ projectedPkg['dependencies'] = { ...left, '@dzhechkov/core': right['@dzhechkov/core'] };
338
+ }
339
+
340
+ // Existing v3 canonical handling preserves order-sensitive exports/imports; no dependency values omitted.
341
+ if (hashPackBytes('package.json', Buffer.from(JSON.stringify(projectedPkg))) !== hashPackBytes('package.json', registryPkgBytes)) return decline('packed metadata differs beyond homepage (including dependency values)');
342
+ const stamp = 'Site: https://aicoding.space · Source: ' + source;
343
+ const stripTopStamp = (bytes: Buffer, required: boolean): Buffer | undefined => {
344
+ const text = bytes.toString('utf8'); if (!Buffer.from(text).equals(bytes)) return undefined;
345
+ const lines = text.split('\n');
346
+ const stamps = lines.filter(line => line.startsWith('Site: ') || line.includes(' · Source: '));
347
+ if (stamps.length === 0) return required ? undefined : bytes;
348
+ if (stamps.length !== 1 || !/^# [^\r\n]+$/.test(lines[0] ?? '') || lines[1] !== '' || lines[2] !== stamp || lines[3] !== '') return undefined;
349
+ // Remove only the inserted blank/stamp framing; retain every other byte including trailing newlines.
350
+ return Buffer.from(lines[0] + '\n' + lines.slice(3).join('\n'));
351
+ };
352
+ const localReadme = readFileSync(join(localDir, 'README.md'));
353
+ const registryReadme = readFileSync(join(registryDir, 'README.md'));
354
+ const localRest = stripTopStamp(localReadme, true), registryRest = stripTopStamp(registryReadme, false);
355
+ if (!localRest || !registryRest || !localRest.equals(registryRest)) return decline('README differs beyond the one exact top owner stamp');
356
+ const admitted = new Set<string>();
357
+ if (!localReadme.equals(registryReadme)) admitted.add('README.md');
358
+ if (hashPackBytes('package.json', localPkgBytes, localSigned.manifest.version) !== hashPackBytes('package.json', registryPkgBytes, registrySigned.manifest.version)) admitted.add('package.json');
359
+ if (admitted.size === 0) return decline('no admitted branding leaf changed');
360
+ const canonical = (value: unknown): string => Array.isArray(value) ? '[' + value.map(canonical).join(',') + ']' : value !== null && typeof value === 'object' ? '{' + Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => JSON.stringify(k) + ':' + canonical(v)).join(',') + '}' : JSON.stringify(value);
361
+ const projectManifest = (signed: SignedManifest) => ({ ...signed, signature: '<verified-original>', manifest: { ...signed.manifest, files: [...signed.manifest.files].sort((a, b) => a.path.localeCompare(b.path)).map(entry => ({ ...entry, sha256: admitted.has(entry.path) ? '<admitted-digest>' : entry.sha256 })) } });
362
+ if (canonical(projectManifest(localSigned)) !== canonical(projectManifest(registrySigned))) return decline('unexplained signed-manifest field or digest change');
363
+ const projectSbom = (dir: string, signed: SignedManifest) => {
364
+ const sbom = JSON.parse(readFileSync(join(dir, 'sbom.json'), 'utf8'));
365
+ for (const component of sbom.components) {
366
+ if (!admitted.has(component.name)) continue;
367
+ const digest = signed.manifest.files.find(entry => entry.path === component.name)?.sha256;
368
+ for (const hash of component.hashes ?? []) if (hash.alg === 'SHA-256' && hash.content === digest) hash.content = '<admitted-digest>';
369
+ for (const prop of component.properties ?? []) if (['dz:canonical-json-sha256-v1', 'dz:canonical-json-sha256-v2'].includes(prop.name) && prop.value === digest) prop.value = '<admitted-digest>';
370
+ }
371
+ return sbom;
372
+ };
373
+ if (canonical(projectSbom(localDir, localSigned)) !== canonical(projectSbom(registryDir, registrySigned))) return decline('unexplained SBOM field or digest change');
374
+ const changedFiles: string[] = [];
375
+ for (const path of paths) {
376
+ const left = readFileSync(join(localDir, path)), right = readFileSync(join(registryDir, path));
377
+ if (left.equals(right)) continue;
378
+ if (!admitted.has(path) && !['.dz-manifest.json', 'sbom.json'].includes(path)) return decline('other shipped bytes changed: ' + path);
379
+ changedFiles.push(path);
380
+ }
381
+ return { accepted: true, coverageVerified, changedFiles, reason: 'owner-branding-only: both original signatures/full inventories verified; exact owner leaves and corresponding manifest/SBOM digests only' };
382
+ } catch { return decline('artifact proof is missing, unreadable or malformed'); }
383
+ }
384
+
385
+ function classifyRetainedBindingDelta(opts: DetectSiblingDriftOptions, localDir: string, registryDir: string, sourceDir: string, name: string, version: string, paths: readonly string[] | undefined): ReturnType<typeof classifyOwnerBrandingDelta> | undefined {
386
+ const proof = opts.retainedBindingProof;
387
+ if (!proof) return undefined;
388
+ const decline = (reason: string) => ({ accepted: false, coverageVerified: true, changedFiles: [], reason: 'retained registered binding not proven: ' + reason });
389
+ try {
390
+ const roots = ['harness-core', 'harness-cli', 'skills-meta', 'keysarium', 'skills-feature-adr'].map(n => '@dzhechkov/' + n).sort();
391
+ const adapters = ['adapter-agents-md', 'adapter-claude', 'adapter-codex', 'adapter-copilot', 'adapter-cursor', 'adapter-gemini', 'adapter-hermes', 'adapter-openclaude', 'adapter-opencode', 'adapter-windsurf'].map(n => '@dzhechkov/' + n).sort();
392
+ if (proof.version !== 1 || !['preview', 'final'].includes(proof.phase) || JSON.stringify([...opts.batch].sort()) !== JSON.stringify(roots) || JSON.stringify([...proof.batch].sort()) !== JSON.stringify(roots) || !adapters.includes(name) || opts.batch.has(name) || JSON.stringify(Object.keys(proof.adapters).sort()) !== JSON.stringify(adapters)) return decline('batch or literal adapter identity differs');
393
+ const canonical = (v: unknown): string => Array.isArray(v) ? '[' + v.map(canonical).join(',') + ']' : v !== null && typeof v === 'object' ? '{' + Object.entries(v).sort(([a], [b]) => a.localeCompare(b)).map(([k, value]) => JSON.stringify(k) + ':' + canonical(value)).join(',') + '}' : JSON.stringify(v);
394
+ const { digest, ...payload } = proof; if (sha256(canonical(payload)) !== digest || !/^[a-f0-9]{40,64}$/.test(proof.head) || proof.refs.length === 0 || proof.refs.some(r => !/^[a-f0-9]{40,64}$/.test(r.oid))) return decline('snapshot/digest incomplete');
395
+ const baselineExecution = proof.baseline.evidence as Record<string, unknown>, candidateExecution = proof.candidate.evidence as Record<string, unknown>;
396
+ if (canonical(baselineExecution.platform) !== canonical(candidateExecution.platform) || baselineExecution.npmVersion !== candidateExecution.npmVersion) return decline('host/npm identity differs across cohorts');
397
+ const baseline = judgeReleaseCohortAudit(proof.baseline.roots, proof.baseline.evidence), candidate = judgeReleaseCohortAudit(proof.candidate.roots, proof.candidate.evidence);
398
+ if (baseline.status !== 'clean' || candidate.status !== 'clean' || !baseline.graph || !candidate.graph) return decline('fresh five-root cohorts are not both clean and complete');
399
+ for (const adapter of adapters) { const fact = proof.adapters[adapter]; if (!fact || ![baseline.graph, candidate.graph].every(graph => graph.some(node => node.name === adapter && !node.selectedRoot && node.installed && node.version === fact.version && node.integrity === fact.registryIntegrity && node.declarations.dependencies?.['@dzhechkov/core'] === fact.registeredFloor && node.edges.some((edge: any) => edge.table === 'dependencies' && edge.name === '@dzhechkov/core' && graph.some(target => target.path === edge.target && !target.selectedRoot && target.version === fact.registeredFloor))))) return decline('all ten registered adapter bindings must occur in both complete graphs'); }
400
+ const project = (graph: readonly Record<string, any>[]) => graph.map(node => {
401
+ if (!node.selectedRoot) return node;
402
+ const expectedPath = 'node_modules/' + node.name; if (node.path !== expectedPath || !roots.includes(node.name)) throw new Error('selected projection is not a root occurrence');
403
+ const declarations = Object.fromEntries(Object.entries(node.declarations).map(([table, values]) => [table, Object.fromEntries(Object.entries(values as Record<string, string>).map(([dep, spec]) => [dep, node.edges.some((edge: any) => edge.table === table && edge.name === dep && edge.target === 'node_modules/' + dep && roots.includes(dep)) ? '<selected-root-step>' : spec]))]));
404
+ return { ...node, version: '<provided-root>', resolved: '<provided-root>', integrity: '<provided-root>', declarations, edges: node.edges.map((edge: any) => ({ ...edge, spec: roots.includes(edge.name) && edge.target === 'node_modules/' + edge.name ? '<selected-root-step>' : edge.spec })) };
405
+ });
406
+ if (canonical(project(baseline.graph)) !== canonical(project(candidate.graph)) || canonical(baseline.optionalAbsences) !== canonical(candidate.optionalAbsences)) return decline('unselected graph, occurrence, edge or omission differs');
407
+ const record = proof.adapters[name]; if (!record) return decline('adapter record missing'); const sourceBytes = readFileSync(join(sourceDir, 'package.json'));
408
+ hashPackBytes('package.json', sourceBytes); const source = JSON.parse(sourceBytes.toString('utf8'));
409
+ if (sha256(sourceBytes) !== record.sourceSha256 || source.name !== name || source.version !== version || source.dependencies?.['@dzhechkov/core'] !== 'workspace:*' || record.version !== version || !/^[a-f0-9]{40,64}$/.test(record.creation) || !record.history.some(row => row.oid === record.creation && !row.boundary) || record.history.length === 0 || record.history.length > 2048 || record.history.some(row => !/^[a-f0-9]{40,64}$/.test(row.oid) || (row.boundary ? row.literal !== null || row.blob !== null : row.literal !== 'workspace:*' || !/^[a-f0-9]{40,64}$/.test(row.blob ?? '')))) return decline('current source or complete declaration history differs');
410
+ if (JSON.stringify([...opts.workspaceVersions].sort()) !== JSON.stringify([...proof.workspaceVersions].sort())) return decline('captured workspace materialization map changed');
411
+ const expected = JSON.parse(rewriteWorkspaceSpecs(sourceBytes.toString('utf8'), new Map(proof.workspaceVersions))); if (expected.scripts) delete expected.scripts.prepublishOnly;
412
+ const actual = readFileSync(join(localDir, 'package.json')); if (hashPackBytes('package.json', Buffer.from(JSON.stringify(expected))) !== hashPackBytes('package.json', actual)) return decline('source-to-packed materialization differs');
413
+ const treeHash = (dir: string) => sha256(canonical(listFilesRecursive(dir, dir, true).sort().map(path => [path, sha256(readFileSync(join(dir, path)))])));
414
+ if (treeHash(localDir) !== record.localTreeSha256 || treeHash(registryDir) !== record.registryTreeSha256 || !/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(record.registryIntegrity) || !/^\d+\.\d+\.\d+$/.test(record.registeredFloor) || record.localFloor === record.registeredFloor || expected.dependencies?.['@dzhechkov/core'] !== record.localFloor) return decline('artifact/SRI/exact-floor binding differs');
415
+ const verified = classifyOwnerBrandingDelta(localDir, registryDir, name, version, opts.trustedPublicKeyPem, paths, { localFloor: record.localFloor, registeredFloor: record.registeredFloor });
416
+ if (!verified.accepted) return verified;
417
+ return { ...verified, reason: `retained-registered-binding: registered core ${record.registeredFloor} retained; workspace materializes ${record.localFloor}; fresh cohort/source/artifact proof ${proof.digest}; historical release gitHead not established` };
418
+ } catch { return decline('source/artifact/graph proof unreadable or inconsistent'); }
419
+ }
420
+
254
421
  /** package.json normalized for comparison: strip fields that legitimately differ (version, gitHead, npm-internal `_*`). */
255
422
  function normalizedPackageJsonText(raw: Record<string, unknown>): string {
256
423
  // Lead edit after the live dry-run on the hub (2026-09-13 10:40): the packer strips
@@ -409,6 +576,16 @@ export function detectSiblingDrift(opts: DetectSiblingDriftOptions): SiblingDrif
409
576
  // AM-3: a missing/unparseable package.json on EITHER side must not silently drop out of the
410
577
  // comparison (the old `hashTree` simply omitted the key, which — with an empty/matching
411
578
  // `dist/**` on both sides — could report `same` about an input that was never actually read).
579
+ if (opts.localInventory !== undefined && inventorySource === 'pack-artifact') {
580
+ try {
581
+ listFilesRecursive(fetched.dir, fetched.dir, true);
582
+ if (!lstatSync(workspaceDir).isDirectory()) throw new InventoryListingError('workspace artifact root is a symlink or unsupported entry');
583
+ if (!lstatSync(join(fetched.dir, 'package.json')).isFile() || !lstatSync(join(workspaceDir, 'package.json')).isFile()) throw new InventoryListingError('package metadata is not a regular file');
584
+ } catch (error) {
585
+ results.push({ name: dep, version, status: 'unavailable', changedFiles: [], missingExports: [], reason: 'physical packed admission failed: ' + String(error), inventorySource });
586
+ continue;
587
+ }
588
+ }
412
589
  const publishedManifest = readManifest(fetched.dir);
413
590
  const workspaceManifest = readManifest(workspaceDir);
414
591
  if (publishedManifest === null || workspaceManifest === null) {
@@ -437,6 +614,8 @@ export function detectSiblingDrift(opts: DetectSiblingDriftOptions): SiblingDrif
437
614
  // pack`) correctly includes them, reading as a false "only in workspace" drift. WITHOUT a
438
615
  // provider, core has no way to ask npm on either side, so it degrades to the SAME approximation
439
616
  // on BOTH sides (symmetry preserved, just cruder) — a named approximation, never a subprocess.
617
+ let localPackedDir: string | undefined;
618
+ let localPackedPaths: readonly string[] | undefined;
440
619
  let workspaceHashes: Map<string, string>;
441
620
  let publishedHashes: Map<string, string>;
442
621
  if (opts.localInventory !== undefined) {
@@ -453,7 +632,12 @@ export function detectSiblingDrift(opts: DetectSiblingDriftOptions): SiblingDrif
453
632
  });
454
633
  continue;
455
634
  }
635
+ if ('packedDir' in localResult) { localPackedDir = localResult.packedDir; localPackedPaths = localResult.paths; }
456
636
  try {
637
+ if ('packedDir' in localResult && inventorySource === 'pack-artifact') {
638
+ const physical = listFilesRecursive(localResult.packedDir, localResult.packedDir, true).sort();
639
+ if (localResult.paths !== undefined && (!Array.isArray(localResult.paths) || localResult.paths.some(path => !isSafeManifestPath(path)) || JSON.stringify([...localResult.paths].sort()) !== JSON.stringify(physical))) throw new InventoryListingError('declared packed inventory is not the complete canonical physical leaf set');
640
+ }
457
641
  workspaceHashes = 'packedDir' in localResult
458
642
  ? localResult.paths !== undefined
459
643
  ? hashTreeFromPaths(localResult.packedDir, localResult.paths, workspaceManifest)
@@ -485,6 +669,24 @@ export function detectSiblingDrift(opts: DetectSiblingDriftOptions): SiblingDrif
485
669
  }
486
670
  changed.sort();
487
671
 
672
+ const branding = (changed.length > 0 || opts.trustedPublicKeyPem !== undefined) && localPackedDir !== undefined && inventorySource === 'pack-artifact'
673
+ ? classifyOwnerBrandingDelta(localPackedDir, fetched.dir, dep, version, opts.trustedPublicKeyPem, localPackedPaths)
674
+ : undefined;
675
+ const retained = !branding?.accepted && localPackedDir !== undefined && inventorySource === 'pack-artifact'
676
+ ? classifyRetainedBindingDelta(opts, localPackedDir, fetched.dir, workspaceDir, dep, version, localPackedPaths) : undefined;
677
+ if (retained?.accepted) {
678
+ results.push({ name: dep, version, status: 'same', classification: 'retained-registered-binding', changedFiles: retained.changedFiles, missingExports: [], reason: retained.reason, inventorySource });
679
+ continue;
680
+ }
681
+ if (opts.trustedPublicKeyPem !== undefined && branding !== undefined && !branding.coverageVerified) {
682
+ results.push({ name: dep, version, status: 'unavailable', changedFiles: changed, missingExports: [], reason: branding.reason, inventorySource });
683
+ continue;
684
+ }
685
+ if (branding?.accepted) {
686
+ results.push({ name: dep, version, status: 'same', classification: 'owner-branding-only', changedFiles: branding.changedFiles, missingExports: [], reason: branding.reason, inventorySource });
687
+ continue;
688
+ }
689
+
488
690
  if (changed.length === 0) {
489
691
  results.push({ name: dep, version, status: 'same', changedFiles: [], missingExports: [], inventorySource });
490
692
  } else {
@@ -493,6 +695,7 @@ export function detectSiblingDrift(opts: DetectSiblingDriftOptions): SiblingDrif
493
695
  version,
494
696
  status: 'drift',
495
697
  changedFiles: changed,
698
+ ...(branding !== undefined ? { reason: branding.reason } : {}),
496
699
  missingExports: missingExportNames(fetched.dir, workspaceDir),
497
700
  inventorySource,
498
701
  });
package/src/publish.ts CHANGED
@@ -415,12 +415,30 @@ export function publishArgv(mode: ProvenanceMode, env: NodeJS.ProcessEnv): strin
415
415
  return decideProvenance(mode, env).useProvenance ? base + ' --provenance' : base;
416
416
  }
417
417
 
418
- /** Match substrings against package identity and path without including the checkout root. */
418
+ /** Validate exact identities against the unfiltered discovery set before any publish effects. */
419
+ export function validatePublishFilters(
420
+ filters: readonly string[] | undefined,
421
+ packages: readonly { name: string }[],
422
+ ): void {
423
+ for (const filter of filters ?? []) {
424
+ if (!filter.startsWith('=')) continue;
425
+ const name = filter.slice(1);
426
+ if (name.length === 0 || /[\s\p{Cc}\p{Cf}=,]/u.test(name)) {
427
+ throw new Error(`malformed exact --filter token ${JSON.stringify(filter)}: expected =NAME without whitespace, controls, equals or comma`);
428
+ }
429
+ if (!packages.some(pkg => pkg.name === name)) {
430
+ throw new Error(`unknown exact --filter token ${JSON.stringify(filter)}: no discovered package has that name`);
431
+ }
432
+ }
433
+ }
434
+
435
+ /** Match =NAME by literal package identity; other filters retain checkout-relative substrings. */
419
436
  export function matchesPublishFilter(
420
437
  pkg: { name: string; dir: string },
421
438
  filter: string,
422
439
  monorepoRoot: string,
423
440
  ): boolean {
441
+ if (filter.startsWith('=')) return pkg.name === filter.slice(1);
424
442
  const normalizedFilter = filter.replace(/\\/g, '/');
425
443
  const relativeDir = pathRelative(pathResolve(monorepoRoot), pathResolve(pkg.dir));
426
444
  // A foreign path must not reintroduce checkout ancestors into the match.
@@ -941,6 +959,7 @@ export function publishPackages(
941
959
  }
942
960
 
943
961
  const packages = discoverPackages(monorepoRoot);
962
+ validatePublishFilters(opts.filter, packages);
944
963
  const results: PublishResult[] = [];
945
964
  const registryProbes = new Map<string, RegistryProbe>();
946
965
  const matching = opts.filter && opts.filter.length > 0