@deftai/directive-core 0.104.0 → 0.106.0

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 (287) hide show
  1. package/dist/authz/classify.js +1108 -67
  2. package/dist/check/cached-orchestrator.d.ts +1 -1
  3. package/dist/check/cached-orchestrator.js +2 -2
  4. package/dist/check/named-cause.d.ts +1 -1
  5. package/dist/check/named-cause.js +34 -4
  6. package/dist/codebase/default-extractor.d.ts +5 -0
  7. package/dist/codebase/default-extractor.js +7 -15
  8. package/dist/deposit/copy-tree.js +60 -0
  9. package/dist/deposit/python-free.js +26 -18
  10. package/dist/doctor/checks.d.ts +8 -0
  11. package/dist/doctor/checks.js +37 -25
  12. package/dist/doctor/doctor-state.d.ts +4 -0
  13. package/dist/doctor/doctor-state.js +7 -1
  14. package/dist/doctor/main.js +9 -19
  15. package/dist/doctor/types.d.ts +9 -0
  16. package/dist/forward-coverage/diff-coverage.d.ts +39 -0
  17. package/dist/forward-coverage/diff-coverage.js +112 -0
  18. package/dist/forward-coverage/diff-lines.d.ts +19 -0
  19. package/dist/forward-coverage/diff-lines.js +173 -0
  20. package/dist/forward-coverage/evaluate.d.ts +24 -9
  21. package/dist/forward-coverage/evaluate.js +125 -19
  22. package/dist/fs/contained-write.d.ts +115 -1
  23. package/dist/fs/contained-write.js +241 -5
  24. package/dist/fs/mutation-ledger.d.ts +54 -0
  25. package/dist/fs/mutation-ledger.js +133 -0
  26. package/dist/fs/non-product-dirs.d.ts +79 -0
  27. package/dist/fs/non-product-dirs.js +96 -0
  28. package/dist/hooks/dispatcher.d.ts +3 -1
  29. package/dist/hooks/dispatcher.js +14 -0
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +2 -0
  32. package/dist/init-deposit/agent-hooks.d.ts +1 -1
  33. package/dist/init-deposit/agent-hooks.js +18 -16
  34. package/dist/init-deposit/core-guard-pin-content.d.ts +7 -0
  35. package/dist/init-deposit/core-guard-pin-content.js +393 -0
  36. package/dist/init-deposit/gitignore.js +9 -0
  37. package/dist/init-deposit/hygiene.d.ts +47 -11
  38. package/dist/init-deposit/hygiene.js +172 -37
  39. package/dist/init-deposit/legacy-detect.js +6 -2
  40. package/dist/init-deposit/refresh.d.ts +21 -1
  41. package/dist/init-deposit/refresh.js +106 -9
  42. package/dist/init-deposit/scaffold.js +28 -279
  43. package/dist/init-deposit/slash-deposit.js +13 -7
  44. package/dist/init-deposit/xbrief-projections.d.ts +8 -1
  45. package/dist/init-deposit/xbrief-projections.js +24 -11
  46. package/dist/intake/clause-derivation.d.ts +47 -1
  47. package/dist/intake/clause-derivation.js +238 -9
  48. package/dist/intake/issue-ingest.d.ts +3 -1
  49. package/dist/intake/issue-ingest.js +35 -6
  50. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +5 -0
  51. package/dist/lifecycle/completed-tracked-on-delivery.js +85 -18
  52. package/dist/lifecycle-visible/evaluate.d.ts +98 -0
  53. package/dist/lifecycle-visible/evaluate.js +710 -0
  54. package/dist/lifecycle-visible/index.d.ts +2 -0
  55. package/dist/lifecycle-visible/index.js +2 -0
  56. package/dist/literal-acceptance/capture.d.ts +10 -0
  57. package/dist/literal-acceptance/capture.js +226 -77
  58. package/dist/literal-acceptance/evaluate.d.ts +29 -4
  59. package/dist/literal-acceptance/evaluate.js +70 -2
  60. package/dist/literal-acceptance/index.d.ts +3 -3
  61. package/dist/literal-acceptance/index.js +3 -3
  62. package/dist/literal-acceptance/safety.d.ts +41 -0
  63. package/dist/literal-acceptance/safety.js +126 -2
  64. package/dist/literal-acceptance/types.d.ts +8 -0
  65. package/dist/orchestration/judgment-policy.d.ts +13 -0
  66. package/dist/orchestration/judgment-policy.js +15 -0
  67. package/dist/orchestration/probe-session.js +13 -8
  68. package/dist/orchestration/verify-judgment-gates.d.ts +4 -0
  69. package/dist/orchestration/verify-judgment-gates.js +43 -11
  70. package/dist/orphan-active/evaluate.d.ts +7 -1
  71. package/dist/orphan-active/evaluate.js +102 -23
  72. package/dist/platform/platform-capabilities.js +2 -3
  73. package/dist/policy/base-branch.d.ts +37 -0
  74. package/dist/policy/base-branch.js +114 -0
  75. package/dist/policy/branch-sync.d.ts +80 -0
  76. package/dist/policy/branch-sync.js +211 -0
  77. package/dist/policy/ceremony-dial-escalation.js +1 -0
  78. package/dist/policy/ceremony-dial.js +11 -21
  79. package/dist/policy/delivery-branch.d.ts +5 -3
  80. package/dist/policy/delivery-branch.js +7 -3
  81. package/dist/policy/file-size-thresholds.d.ts +24 -0
  82. package/dist/policy/file-size-thresholds.js +23 -0
  83. package/dist/policy/forge-outage-retry.d.ts +47 -0
  84. package/dist/policy/forge-outage-retry.js +157 -0
  85. package/dist/policy/host-hooks.d.ts +20 -0
  86. package/dist/policy/host-hooks.js +121 -2
  87. package/dist/policy/index.d.ts +7 -0
  88. package/dist/policy/index.js +66 -7
  89. package/dist/policy/org-force-on-migration.js +2 -1
  90. package/dist/policy/plan-extensions.d.ts +1 -1
  91. package/dist/policy/plan-extensions.js +2 -0
  92. package/dist/policy/product-signal.js +6 -7
  93. package/dist/policy/project-invariants.d.ts +50 -0
  94. package/dist/policy/project-invariants.js +205 -0
  95. package/dist/policy/require-human-merge.js +8 -6
  96. package/dist/policy/resolve.d.ts +7 -2
  97. package/dist/policy/resolve.js +25 -8
  98. package/dist/policy/sync-default.d.ts +129 -0
  99. package/dist/policy/sync-default.js +452 -0
  100. package/dist/policy/sync-max-files.d.ts +93 -0
  101. package/dist/policy/sync-max-files.js +192 -0
  102. package/dist/policy/value-feedback.js +7 -8
  103. package/dist/pr-monitor/absent-required.d.ts +11 -0
  104. package/dist/pr-monitor/absent-required.js +61 -0
  105. package/dist/pr-monitor/constants.d.ts +7 -0
  106. package/dist/pr-monitor/constants.js +7 -0
  107. package/dist/pr-monitor/index.d.ts +2 -1
  108. package/dist/pr-monitor/index.js +2 -1
  109. package/dist/pr-monitor/main.js +4 -0
  110. package/dist/pr-monitor/monitor.js +24 -1
  111. package/dist/pr-wait-mergeable/classify.js +6 -0
  112. package/dist/preflight/evaluate.d.ts +4 -0
  113. package/dist/preflight/evaluate.js +35 -1
  114. package/dist/preflight/index.d.ts +4 -0
  115. package/dist/preflight/index.js +2 -0
  116. package/dist/preflight/intended-placement.d.ts +62 -0
  117. package/dist/preflight/intended-placement.js +275 -0
  118. package/dist/preflight/project-invariants-gate.d.ts +29 -0
  119. package/dist/preflight/project-invariants-gate.js +82 -0
  120. package/dist/preflight-cache/evaluate.d.ts +12 -0
  121. package/dist/preflight-cache/evaluate.js +15 -1
  122. package/dist/preflight-cache/index.d.ts +1 -1
  123. package/dist/preflight-cache/index.js +1 -1
  124. package/dist/product-first-done-gate/acceptance-resolver.d.ts +128 -0
  125. package/dist/product-first-done-gate/acceptance-resolver.js +144 -0
  126. package/dist/product-first-done-gate/acceptance.d.ts +7 -4
  127. package/dist/product-first-done-gate/acceptance.js +79 -8
  128. package/dist/product-first-done-gate/empty-resolution.d.ts +4 -1
  129. package/dist/product-first-done-gate/empty-resolution.js +12 -1
  130. package/dist/product-first-done-gate/evaluate.d.ts +16 -0
  131. package/dist/product-first-done-gate/evaluate.js +298 -34
  132. package/dist/product-first-done-gate/index.d.ts +2 -0
  133. package/dist/product-first-done-gate/index.js +1 -0
  134. package/dist/release/build-dist.d.ts +101 -0
  135. package/dist/release/build-dist.js +428 -46
  136. package/dist/release/closed-verb-gate.d.ts +12 -0
  137. package/dist/release/closed-verb-gate.js +51 -0
  138. package/dist/release/index.d.ts +1 -0
  139. package/dist/release/index.js +1 -0
  140. package/dist/release/pipeline.js +7 -0
  141. package/dist/release/types.d.ts +12 -0
  142. package/dist/render/constants.d.ts +2 -1
  143. package/dist/render/constants.js +2 -1
  144. package/dist/render/framework-commands.js +4 -0
  145. package/dist/render/spec-validate.js +23 -12
  146. package/dist/run-summary/emit.d.ts +16 -10
  147. package/dist/run-summary/emit.js +26 -15
  148. package/dist/run-summary/index.d.ts +3 -2
  149. package/dist/run-summary/index.js +2 -1
  150. package/dist/run-summary/session-id.d.ts +21 -0
  151. package/dist/run-summary/session-id.js +92 -0
  152. package/dist/run-summary/types.d.ts +33 -4
  153. package/dist/run-summary/types.js +1 -0
  154. package/dist/scope/acceptance-evidence.d.ts +19 -7
  155. package/dist/scope/acceptance-evidence.js +38 -11
  156. package/dist/scope/delivery-evidence.d.ts +3 -3
  157. package/dist/scope/delivery-evidence.js +22 -27
  158. package/dist/scope/index.d.ts +1 -0
  159. package/dist/scope/index.js +1 -0
  160. package/dist/scope/main.js +15 -5
  161. package/dist/scope/parent-lineage.js +7 -6
  162. package/dist/scope/project-invariant-coverage.d.ts +54 -0
  163. package/dist/scope/project-invariant-coverage.js +322 -0
  164. package/dist/scope/promote-from-issue.js +5 -5
  165. package/dist/scope/transition.js +1 -1
  166. package/dist/scope-provenance/compare-intent.d.ts +27 -0
  167. package/dist/scope-provenance/compare-intent.js +233 -0
  168. package/dist/scope-provenance/digest.d.ts +18 -1
  169. package/dist/scope-provenance/digest.js +16 -11
  170. package/dist/scope-provenance/evaluate.d.ts +5 -1
  171. package/dist/scope-provenance/evaluate.js +67 -7
  172. package/dist/scope-provenance/extract-intent.d.ts +43 -0
  173. package/dist/scope-provenance/extract-intent.js +262 -0
  174. package/dist/scope-provenance/index.d.ts +7 -1
  175. package/dist/scope-provenance/index.js +7 -1
  176. package/dist/scope-provenance/intent-digest.d.ts +12 -0
  177. package/dist/scope-provenance/intent-digest.js +33 -0
  178. package/dist/scope-provenance/intent-evaluate.d.ts +43 -0
  179. package/dist/scope-provenance/intent-evaluate.js +149 -0
  180. package/dist/scope-provenance/json-tokenizer.d.ts +19 -0
  181. package/dist/scope-provenance/json-tokenizer.js +225 -0
  182. package/dist/scope-provenance/known-machine.d.ts +38 -0
  183. package/dist/scope-provenance/known-machine.js +157 -0
  184. package/dist/scope-provenance/mint-artifacts.d.ts +102 -0
  185. package/dist/scope-provenance/mint-artifacts.js +425 -0
  186. package/dist/session/ac-pass-banking.d.ts +9 -36
  187. package/dist/session/ac-pass-banking.js +33 -79
  188. package/dist/session/ac-pass-reuse.d.ts +53 -0
  189. package/dist/session/ac-pass-reuse.js +95 -0
  190. package/dist/session/git.js +19 -4
  191. package/dist/session/index.d.ts +4 -0
  192. package/dist/session/index.js +4 -0
  193. package/dist/session/occupancy-stress-worker.d.ts +7 -0
  194. package/dist/session/occupancy-stress-worker.js +36 -0
  195. package/dist/session/occupancy.d.ts +97 -0
  196. package/dist/session/occupancy.js +422 -0
  197. package/dist/session/openclaw-soft-rebind-deposit.js +6 -6
  198. package/dist/session/process-cost.d.ts +32 -10
  199. package/dist/session/process-cost.js +189 -1
  200. package/dist/session/product-state-hash.d.ts +26 -0
  201. package/dist/session/product-state-hash.js +700 -0
  202. package/dist/session/ritual-entrypoint.js +16 -1
  203. package/dist/session/session-ready.d.ts +2 -0
  204. package/dist/session/session-ready.js +52 -30
  205. package/dist/session/session-start.d.ts +13 -0
  206. package/dist/session/session-start.js +103 -9
  207. package/dist/session/verify-ac-session-cache.d.ts +42 -0
  208. package/dist/session/verify-ac-session-cache.js +126 -0
  209. package/dist/session/verify-session-ritual.d.ts +28 -0
  210. package/dist/session/verify-session-ritual.js +97 -5
  211. package/dist/slash/openclaw-deposit.js +13 -9
  212. package/dist/slice/constants.d.ts +2 -2
  213. package/dist/slice/constants.js +2 -2
  214. package/dist/slice/existing.js +9 -3
  215. package/dist/slice/lock.d.ts +16 -1
  216. package/dist/slice/lock.js +137 -11
  217. package/dist/story-ready/evaluate.d.ts +2 -0
  218. package/dist/story-ready/evaluate.js +13 -0
  219. package/dist/swarm/complete-cohort.d.ts +3 -0
  220. package/dist/swarm/complete-cohort.js +59 -11
  221. package/dist/swarm/finalize-cohort-cli.d.ts +7 -2
  222. package/dist/swarm/finalize-cohort-cli.js +165 -35
  223. package/dist/swarm/finalize-cohort.d.ts +1 -0
  224. package/dist/swarm/finalize-cohort.js +47 -4
  225. package/dist/swarm/launch.d.ts +22 -0
  226. package/dist/swarm/launch.js +155 -0
  227. package/dist/swarm/routing-set-cli.d.ts +11 -1
  228. package/dist/swarm/routing-set-cli.js +15 -3
  229. package/dist/swarm/routing-verify.d.ts +13 -1
  230. package/dist/swarm/routing-verify.js +60 -21
  231. package/dist/swarm/routing.d.ts +14 -0
  232. package/dist/swarm/routing.js +53 -3
  233. package/dist/telemetry-coverage/fake-trial.js +10 -0
  234. package/dist/telemetry-coverage/kinds.js +2 -0
  235. package/dist/triage/actions/index.js +4 -1
  236. package/dist/triage/bootstrap/index.js +6 -6
  237. package/dist/triage/help/registry-data.d.ts +1 -1
  238. package/dist/triage/help/registry-data.js +1 -1
  239. package/dist/triage/welcome/writers.d.ts +1 -1
  240. package/dist/triage/welcome/writers.js +10 -5
  241. package/dist/ts-check-lane/index.d.ts +3 -0
  242. package/dist/ts-check-lane/index.js +2 -0
  243. package/dist/ts-check-lane/progress-reporter.d.ts +17 -0
  244. package/dist/ts-check-lane/progress-reporter.js +38 -0
  245. package/dist/ts-check-lane/progress.d.ts +32 -0
  246. package/dist/ts-check-lane/progress.js +57 -0
  247. package/dist/ts-check-lane/run-lane.d.ts +3 -1
  248. package/dist/ts-check-lane/run-lane.js +8 -6
  249. package/dist/validate-content/validate-links.d.ts +8 -0
  250. package/dist/validate-content/validate-links.js +9 -15
  251. package/dist/value/feedback-file.js +12 -2
  252. package/dist/value/readback.d.ts +4 -1
  253. package/dist/value/readback.js +51 -12
  254. package/dist/vbrief-build/index.d.ts +1 -0
  255. package/dist/vbrief-build/index.js +1 -0
  256. package/dist/vbrief-build/project-invariants-io.d.ts +16 -0
  257. package/dist/vbrief-build/project-invariants-io.js +41 -0
  258. package/dist/vbrief-reconcile/graph.js +18 -1
  259. package/dist/vbrief-reconcile/index.d.ts +1 -1
  260. package/dist/vbrief-reconcile/index.js +1 -1
  261. package/dist/vbrief-reconcile/types.d.ts +13 -0
  262. package/dist/vbrief-reconcile/umbrellas.d.ts +14 -0
  263. package/dist/vbrief-reconcile/umbrellas.js +261 -26
  264. package/dist/vbrief-validate/index.d.ts +1 -1
  265. package/dist/vbrief-validate/index.js +1 -1
  266. package/dist/vbrief-validate/plan-hooks.d.ts +4 -0
  267. package/dist/vbrief-validate/plan-hooks.js +68 -0
  268. package/dist/vbrief-validate/schema.d.ts +4 -0
  269. package/dist/vbrief-validate/schema.js +1 -1
  270. package/dist/verify-ac/evaluate.d.ts +7 -1
  271. package/dist/verify-ac/evaluate.js +34 -26
  272. package/dist/verify-ac/flag.d.ts +4 -0
  273. package/dist/verify-ac/flag.js +16 -0
  274. package/dist/verify-ac/index.d.ts +2 -2
  275. package/dist/verify-ac/index.js +2 -2
  276. package/dist/verify-env/agent-hook-readiness.d.ts +2 -2
  277. package/dist/verify-env/agent-hook-readiness.js +12 -8
  278. package/dist/verify-env/agent-hooks-live-probe.d.ts +5 -1
  279. package/dist/verify-env/agent-hooks-live-probe.js +26 -5
  280. package/dist/verify-env/agent-hooks.js +3 -4
  281. package/dist/verify-source/contained-writes.js +2 -0
  282. package/dist/verify-source/verify-stubs.d.ts +15 -1
  283. package/dist/verify-source/verify-stubs.js +20 -16
  284. package/dist/vitest-runner/win32-coverage-tmp-setup.d.ts +5 -6
  285. package/dist/vitest-runner/win32-coverage-tmp-setup.js +5 -6
  286. package/dist/xbrief-migrate/migrate-project.js +2 -5
  287. package/package.json +7 -3
@@ -1,32 +1,43 @@
1
- import { createWriteStream, existsSync, mkdirSync, readdirSync, statSync, unlinkSync, } from "node:fs";
1
+ import { spawnSync } from "node:child_process";
2
+ import { createReadStream, createWriteStream, existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, statSync, unlinkSync, } from "node:fs";
2
3
  import { createRequire } from "node:module";
3
4
  import { platform } from "node:os";
4
- import { dirname, join, relative, resolve } from "node:path";
5
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
+ import { NON_PRODUCT_DIRS } from "../fs/non-product-dirs.js";
7
+ import { SUBPROCESS_MAX_BUFFER } from "../subprocess/max-buffer.js";
5
8
  const { ZipArchive, TarArchive } = createRequire(import.meta.url)("archiver");
6
- export const DEFAULT_EXCLUDES = new Set([
7
- ".git",
8
- "dist",
9
- "backup",
10
- "node_modules",
11
- "__pycache__",
12
- ".venv",
13
- "htmlcov",
14
- ".pytest_cache",
15
- ".mypy_cache",
16
- ".ruff_cache",
17
- ".coverage",
18
- "coverage",
19
- // Scratch worktrees are never shippable framework content (#2953).
20
- ".deft-scratch",
21
- "swarm-worktrees",
22
- ]);
9
+ /**
10
+ * Shared "not product source" core (#3487) plus the coverage artifacts only the
11
+ * release archive names. Scratch and agent worktrees are never shippable
12
+ * framework content (#2953). The archive set is git-tracked files (#3490); this
13
+ * denylist is defence in depth, including for a tracked path under a core name.
14
+ *
15
+ * Callers may still widen this per run via `extraExcludes` / `--exclude-extra`.
16
+ */
17
+ export const DEFAULT_EXCLUDES = new Set([...NON_PRODUCT_DIRS, "htmlcov", ".coverage", "coverage"]);
23
18
  export const DEFAULT_EXCLUDED_PATH_PREFIXES = [
24
19
  "history/archive",
25
20
  "vbrief/completed",
26
21
  "vbrief/cancelled",
27
22
  ];
23
+ /**
24
+ * Generated outputs that may be packed even when untracked. Empty by default:
25
+ * the release archive is a source distribution, so extra outputs must be named
26
+ * explicitly (#3490).
27
+ */
28
+ export const DEFAULT_GENERATED_ALLOWLIST = [];
28
29
  export const ARCHIVE_ROOT = "deft";
29
30
  export const CONTENT_PREFIX = "content/";
31
+ /** Raised when the resolved archive set contains an untracked surprise (#3490). */
32
+ export class UntrackedArchiveEntryError extends Error {
33
+ paths;
34
+ constructor(paths) {
35
+ const listed = paths.join(", ");
36
+ super(`build-dist: refusing to pack untracked files not on the generated-output allowlist: ${listed}`);
37
+ this.name = "UntrackedArchiveEntryError";
38
+ this.paths = paths;
39
+ }
40
+ }
30
41
  const VENDORED_TS_TEST_RE = /\.(test|spec)\.(c|m)?[jt]sx?$/i;
31
42
  function flattenContentPrefix(relPosix) {
32
43
  if (relPosix === "content")
@@ -46,6 +57,362 @@ function isVendoredTsTest(relPosix) {
46
57
  const basename = relPosix.split("/").pop() ?? "";
47
58
  return VENDORED_TS_TEST_RE.test(basename);
48
59
  }
60
+ function pathHasExcludedComponent(relPosix, excludes) {
61
+ return relPosix.split("/").some((part) => excludes.has(part));
62
+ }
63
+ function shouldSkipRel(relPosix, excludes, excludedPrefixes) {
64
+ return (pathHasExcludedComponent(relPosix, excludes) ||
65
+ matchesExcludedPrefix(relPosix, excludedPrefixes) ||
66
+ isVendoredTsTest(relPosix));
67
+ }
68
+ /** Private-use mark so archiver's string `name` can carry raw member bytes. */
69
+ export const ARCHIVE_BINARY_NAME_MARK = "\uF000";
70
+ export function flattenContentPrefixBytes(relBytes) {
71
+ const contentDir = Buffer.from("content", "utf8");
72
+ if (relBytes.equals(contentDir))
73
+ return Buffer.from(relBytes);
74
+ const prefix = Buffer.from(CONTENT_PREFIX, "utf8");
75
+ if (relBytes.length >= prefix.length && relBytes.subarray(0, prefix.length).equals(prefix)) {
76
+ return Buffer.from(relBytes.subarray(prefix.length));
77
+ }
78
+ return Buffer.from(relBytes);
79
+ }
80
+ export function isValidUtf8Buffer(buf) {
81
+ return Buffer.from(buf.toString("utf8"), "utf8").equals(buf);
82
+ }
83
+ export function archiveMemberBytes(archiveRelBytes) {
84
+ return Buffer.concat([Buffer.from(`${ARCHIVE_ROOT}/`, "utf8"), archiveRelBytes]);
85
+ }
86
+ /** Archiver `file()`/`append` name: UTF-8 text, or marked latin1 of raw bytes. */
87
+ export function archiverEntryName(memberBytes) {
88
+ if (isValidUtf8Buffer(memberBytes))
89
+ return memberBytes.toString("utf8");
90
+ return `${ARCHIVE_BINARY_NAME_MARK}${memberBytes.toString("latin1")}`;
91
+ }
92
+ function paxRawRecord(key, value) {
93
+ const keyword = Buffer.from(` ${key}=`, "utf8");
94
+ const nl = Buffer.from("\n", "utf8");
95
+ let digits = 1;
96
+ for (;;) {
97
+ const total = digits + keyword.length + value.length + nl.length;
98
+ const lenStr = String(total);
99
+ if (lenStr.length === digits) {
100
+ return Buffer.concat([Buffer.from(lenStr, "utf8"), keyword, value, nl]);
101
+ }
102
+ digits = lenStr.length;
103
+ }
104
+ }
105
+ function loadZipArchiveEntry() {
106
+ const archiverReq = createRequire(import.meta.url);
107
+ const zipReq = createRequire(archiverReq.resolve("archiver"));
108
+ const commons = createRequire(zipReq.resolve("zip-stream"))("compress-commons");
109
+ const cls = commons.ZipArchiveEntry ?? commons.default?.ZipArchiveEntry;
110
+ if (cls?.prototype?.setName)
111
+ return cls;
112
+ throw new Error("build-dist: cannot load ZipArchiveEntry for byte-preserving member names");
113
+ }
114
+ function loadTarHeaders() {
115
+ const archiverReq = createRequire(import.meta.url);
116
+ const zipReq = createRequire(archiverReq.resolve("archiver"));
117
+ const tarMain = zipReq.resolve("tar-stream");
118
+ const loaded = createRequire(tarMain)("./headers.js");
119
+ if (typeof loaded.encodePax !== "function") {
120
+ throw new Error("build-dist: cannot load tar-stream headers.encodePax");
121
+ }
122
+ return loaded;
123
+ }
124
+ /**
125
+ * Archiver only accepts a string `name` and UTF-8-encodes it. For invalid
126
+ * git path bytes we mark a latin1 string and write those bytes where
127
+ * zip-stream / tar-stream already copy a Buffer (`setName` / PAX path).
128
+ */
129
+ function installBytePreservingMemberNames() {
130
+ const ZipArchiveEntry = loadZipArchiveEntry();
131
+ const origSetName = ZipArchiveEntry.prototype.setName;
132
+ ZipArchiveEntry.prototype.setName = function (name, prependSlash = false) {
133
+ if (typeof name === "string" && name.charCodeAt(0) === ARCHIVE_BINARY_NAME_MARK.charCodeAt(0)) {
134
+ // Raw bytes, not UTF-8: leave the ZIP language-encoding flag off so
135
+ // extractors do not decode 0xFF-class names as UTF-8.
136
+ this.name = Buffer.from(name.slice(ARCHIVE_BINARY_NAME_MARK.length), "latin1");
137
+ return;
138
+ }
139
+ return origSetName.call(this, name, prependSlash);
140
+ };
141
+ const headers = loadTarHeaders();
142
+ const origEncodePax = headers.encodePax.bind(headers);
143
+ headers.encodePax = (opts) => {
144
+ if (typeof opts.name === "string" &&
145
+ opts.name.charCodeAt(0) === ARCHIVE_BINARY_NAME_MARK.charCodeAt(0)) {
146
+ const pathRec = paxRawRecord("path", Buffer.from(opts.name.slice(ARCHIVE_BINARY_NAME_MARK.length), "latin1"));
147
+ const rest = origEncodePax({ ...opts, name: undefined });
148
+ return Buffer.concat([pathRec, rest]);
149
+ }
150
+ return origEncodePax(opts);
151
+ };
152
+ return () => {
153
+ ZipArchiveEntry.prototype.setName = origSetName;
154
+ headers.encodePax = origEncodePax;
155
+ };
156
+ }
157
+ function addArchiveFile(archive, absPath, name) {
158
+ if (Buffer.isBuffer(absPath)) {
159
+ archive.append(createReadStream(absPath), { name });
160
+ return;
161
+ }
162
+ archive.file(absPath, { name });
163
+ }
164
+ const defaultArchiveFs = {
165
+ realpathSync: (path, options) => {
166
+ if (options?.encoding === "buffer") {
167
+ return realpathSync(path, { encoding: "buffer" });
168
+ }
169
+ return realpathSync(path);
170
+ },
171
+ lstatSync: (path) => lstatSync(path),
172
+ };
173
+ /** `-z` stdout is a NUL-separated byte stream; never UTF-8-decode it whole. */
174
+ export const GIT_LS_FILES_Z_ENCODING = null;
175
+ /** Decode one `ls-files -z` segment; keep non-UTF-8 bytes as latin1. */
176
+ export function decodeGitLsFilesPath(segment) {
177
+ const utf8 = segment.toString("utf8");
178
+ if (Buffer.from(utf8, "utf8").equals(segment))
179
+ return utf8;
180
+ return segment.toString("latin1");
181
+ }
182
+ /** Split a raw `git ls-files -z` Buffer on 0x00, keeping original path bytes. */
183
+ export function splitGitLsFilesZRecords(stdout) {
184
+ const paths = [];
185
+ let start = 0;
186
+ for (let i = 0; i < stdout.length; i += 1) {
187
+ if (stdout[i] === 0) {
188
+ if (i > start) {
189
+ const bytes = Buffer.from(stdout.subarray(start, i));
190
+ paths.push({ relPosix: decodeGitLsFilesPath(bytes), bytes });
191
+ }
192
+ start = i + 1;
193
+ }
194
+ }
195
+ if (start < stdout.length) {
196
+ const bytes = Buffer.from(stdout.subarray(start));
197
+ paths.push({ relPosix: decodeGitLsFilesPath(bytes), bytes });
198
+ }
199
+ return paths;
200
+ }
201
+ /** Split a raw `git ls-files -z` Buffer on 0x00, then decode each path. */
202
+ export function splitGitLsFilesZ(stdout) {
203
+ return splitGitLsFilesZRecords(stdout).map((p) => p.relPosix);
204
+ }
205
+ function spawnTextFromMaybeBuffer(value) {
206
+ if (Buffer.isBuffer(value))
207
+ return value.toString("utf8");
208
+ return typeof value === "string" ? value : "";
209
+ }
210
+ function bufferFromSpawnStdout(value) {
211
+ if (Buffer.isBuffer(value))
212
+ return value;
213
+ if (typeof value === "string")
214
+ return Buffer.from(value, "utf8");
215
+ return Buffer.alloc(0);
216
+ }
217
+ /**
218
+ * Return `git ls-files -z` paths with original bytes. Fail closed when git
219
+ * cannot enumerate tracked files -- a walk would pack host-local untracked
220
+ * state (#3490).
221
+ */
222
+ export function listGitTrackedPathRecords(root, spawn = spawnSync) {
223
+ // Pin to root/.git so a nested --root cannot walk up and pack a parent repo.
224
+ const result = spawn("git", ["-C", root, "--git-dir=.git", "--work-tree=.", "ls-files", "-z"], {
225
+ encoding: GIT_LS_FILES_Z_ENCODING,
226
+ timeout: 30_000,
227
+ maxBuffer: SUBPROCESS_MAX_BUFFER,
228
+ stdio: ["ignore", "pipe", "pipe"],
229
+ });
230
+ if (result.status !== 0 || result.error) {
231
+ const detail = spawnTextFromMaybeBuffer(result.stderr).trim() ||
232
+ spawnTextFromMaybeBuffer(result.stdout).trim() ||
233
+ result.error?.message ||
234
+ "unknown error";
235
+ throw new Error(`git ls-files failed in ${root}: ${detail}`);
236
+ }
237
+ return splitGitLsFilesZRecords(bufferFromSpawnStdout(result.stdout));
238
+ }
239
+ /** Return `git ls-files -z` paths in POSIX form. */
240
+ export function listGitTrackedFiles(root, spawn = spawnSync) {
241
+ return listGitTrackedPathRecords(root, spawn).map((p) => p.relPosix);
242
+ }
243
+ /**
244
+ * Fail closed when any resolved archive path is untracked and not on the
245
+ * generated-output allowlist (#3490).
246
+ */
247
+ export function assertArchiveEntriesTrackedOrGenerated(root, rels, generatedAllowlist = DEFAULT_GENERATED_ALLOWLIST, spawn = spawnSync) {
248
+ const tracked = new Set(listGitTrackedFiles(root, spawn));
249
+ const allowed = new Set(generatedAllowlist);
250
+ const surprise = rels.filter((rel) => !tracked.has(rel) && !allowed.has(rel));
251
+ if (surprise.length > 0) {
252
+ throw new UntrackedArchiveEntryError(surprise);
253
+ }
254
+ }
255
+ function isInsideRoot(root, absPath) {
256
+ const rel = relative(resolve(root), absPath);
257
+ return !rel.startsWith("..") && !isAbsolute(rel);
258
+ }
259
+ /** Canonicalize the supplied archive root once per resolve (#3490 review). */
260
+ function canonicalArchiveRoot(root, fs) {
261
+ try {
262
+ const real = fs.realpathSync(root, { encoding: "buffer" });
263
+ if (Buffer.isBuffer(real)) {
264
+ return { text: real.toString("utf8"), bytes: real };
265
+ }
266
+ return { text: real, bytes: Buffer.from(real, "utf8") };
267
+ }
268
+ catch {
269
+ throw new Error(`build-dist: cannot resolve archive root: ${root}`);
270
+ }
271
+ }
272
+ /** Join canonical root bytes with original git path bytes; convert POSIX `/` to OS sep. */
273
+ export function joinRootAndRelBytes(rootBytes, relBytes) {
274
+ const sepByte = sep.charCodeAt(0);
275
+ const converted = Buffer.from(relBytes);
276
+ if (sep !== "/") {
277
+ for (let i = 0; i < converted.length; i += 1) {
278
+ if (converted[i] === 0x2f)
279
+ converted[i] = sepByte;
280
+ }
281
+ }
282
+ if (converted.length === 0)
283
+ return rootBytes;
284
+ const rootEndsSep = rootBytes.length > 0 && rootBytes[rootBytes.length - 1] === sepByte;
285
+ const relStartsSep = converted[0] === sepByte;
286
+ if (rootEndsSep && relStartsSep)
287
+ return Buffer.concat([rootBytes, converted.subarray(1)]);
288
+ if (rootEndsSep || relStartsSep)
289
+ return Buffer.concat([rootBytes, converted]);
290
+ return Buffer.concat([rootBytes, Buffer.from([sepByte]), converted]);
291
+ }
292
+ /**
293
+ * String path when the `-z` segment is valid UTF-8; otherwise a Buffer that
294
+ * keeps the original filename bytes for fs lookup (#3490 residual).
295
+ */
296
+ export function fsLookupPath(root, relPosix, bytes, rootBytes) {
297
+ const contained = containedAbsPath(root, relPosix);
298
+ if (contained === null)
299
+ return null;
300
+ if (Buffer.from(relPosix, "utf8").equals(bytes))
301
+ return contained;
302
+ return joinRootAndRelBytes(rootBytes ?? Buffer.from(resolve(root), "utf8"), bytes);
303
+ }
304
+ function isInsideResolved(canonicalRoot, canonicalBytes, real) {
305
+ if (typeof real === "string")
306
+ return isInsideRoot(canonicalRoot, real);
307
+ const sepByte = sep.charCodeAt(0);
308
+ if (real.equals(canonicalBytes))
309
+ return true;
310
+ const prefix = canonicalBytes.length > 0 && canonicalBytes[canonicalBytes.length - 1] === sepByte
311
+ ? canonicalBytes
312
+ : Buffer.concat([canonicalBytes, Buffer.from([sepByte])]);
313
+ if (real.length < prefix.length)
314
+ return false;
315
+ if (real.subarray(0, prefix.length).equals(prefix))
316
+ return true;
317
+ if (process.platform === "win32") {
318
+ return (real.subarray(0, prefix.length).toString("utf8").toLowerCase() ===
319
+ prefix.toString("utf8").toLowerCase());
320
+ }
321
+ return false;
322
+ }
323
+ /** Resolve a POSIX rel path under root, or null if it escapes (#3490 review). */
324
+ export function containedAbsPath(root, relPosix) {
325
+ if (relPosix.includes("\0"))
326
+ return null;
327
+ // Git index paths are POSIX (`/` only). A literal `\` is a filename byte
328
+ // (gitglossary "path"); splitting on `\` would pack `foo\bar` as nested
329
+ // path segments and could escape the root. Node resolve still fail-closes
330
+ // OS-separator `..\` via isInsideRoot.
331
+ const parts = relPosix.split("/");
332
+ if (parts.some((part) => part === ".."))
333
+ return null;
334
+ const absPath = resolve(root, ...parts.filter((part) => part.length > 0 && part !== "."));
335
+ if (!isInsideRoot(root, absPath))
336
+ return null;
337
+ return absPath;
338
+ }
339
+ function toArchiveEntry(root, canonicalRoot, relPosix, bytes, fs) {
340
+ const lookup = fsLookupPath(root, relPosix, bytes, canonicalRoot.bytes);
341
+ if (lookup === null)
342
+ return null;
343
+ let real;
344
+ try {
345
+ // realpath resolves leaf and ancestor symlinks before the file-type check.
346
+ // Buffer lookup keeps invalid filename bytes; a string would re-encode UTF-8.
347
+ real = Buffer.isBuffer(lookup)
348
+ ? fs.realpathSync(lookup, { encoding: "buffer" })
349
+ : fs.realpathSync(lookup);
350
+ }
351
+ catch {
352
+ return null;
353
+ }
354
+ // Compare against the canonical root so a symlink *to* the checkout is
355
+ // inside, while a symlink *out of* the tree still throws.
356
+ if (!isInsideResolved(canonicalRoot.text, canonicalRoot.bytes, real)) {
357
+ throw new Error(`build-dist: path resolves outside the archive root: ${relPosix}`);
358
+ }
359
+ let st;
360
+ try {
361
+ st = fs.lstatSync(real);
362
+ }
363
+ catch {
364
+ return null;
365
+ }
366
+ if (st == null || !st.isFile())
367
+ return null;
368
+ return {
369
+ absPath: real,
370
+ archiveRel: flattenContentPrefix(relPosix),
371
+ archiveRelBytes: flattenContentPrefixBytes(bytes),
372
+ };
373
+ }
374
+ /**
375
+ * Derive the archive file set from git-tracked paths plus an explicit
376
+ * generated-output allowlist. The basename denylist remains defence in depth.
377
+ */
378
+ export function resolveArchiveEntries(root, options = {}) {
379
+ const excludes = new Set([...DEFAULT_EXCLUDES, ...(options.extraExcludes ?? [])]);
380
+ const excludedPrefixes = options.excludedPrefixes ?? DEFAULT_EXCLUDED_PATH_PREFIXES;
381
+ const generatedAllowlist = options.generatedAllowlist ?? DEFAULT_GENERATED_ALLOWLIST;
382
+ const spawn = options.spawn ?? spawnSync;
383
+ const fs = options.fs ?? defaultArchiveFs;
384
+ const canonicalRoot = canonicalArchiveRoot(root, fs);
385
+ const tracked = listGitTrackedPathRecords(root, spawn);
386
+ const entries = [];
387
+ const sourceRels = [];
388
+ const seen = new Set();
389
+ for (const { relPosix, bytes } of tracked) {
390
+ if (shouldSkipRel(relPosix, excludes, excludedPrefixes))
391
+ continue;
392
+ const entry = toArchiveEntry(root, canonicalRoot, relPosix, bytes, fs);
393
+ if (entry === null)
394
+ continue;
395
+ entries.push(entry);
396
+ sourceRels.push(relPosix);
397
+ seen.add(relPosix);
398
+ }
399
+ for (const relPosix of generatedAllowlist) {
400
+ if (seen.has(relPosix))
401
+ continue;
402
+ if (containedAbsPath(root, relPosix) === null) {
403
+ throw new Error(`build-dist: generated allowlist path escapes the archive root: ${relPosix}`);
404
+ }
405
+ const entry = toArchiveEntry(root, canonicalRoot, relPosix, Buffer.from(relPosix, "utf8"), fs);
406
+ if (entry === null)
407
+ continue;
408
+ entries.push(entry);
409
+ sourceRels.push(relPosix);
410
+ seen.add(relPosix);
411
+ }
412
+ assertArchiveEntriesTrackedOrGenerated(root, sourceRels, generatedAllowlist, spawn);
413
+ entries.sort((a, b) => a.archiveRel.localeCompare(b.archiveRel));
414
+ return entries;
415
+ }
49
416
  export function iterSourceFiles(root, excludes = DEFAULT_EXCLUDES, excludedPrefixes = DEFAULT_EXCLUDED_PATH_PREFIXES) {
50
417
  const entries = [];
51
418
  const walk = (dir) => {
@@ -121,48 +488,63 @@ function isExtraExcludesList(options) {
121
488
  function resolveBuildOptions(options = {}) {
122
489
  // Backward-compatible: 4th arg may be a bare extra-excludes array (pre-#2953).
123
490
  if (isExtraExcludesList(options)) {
124
- return { extraExcludes: options, onProgress: () => { } };
491
+ return {
492
+ extraExcludes: options,
493
+ generatedAllowlist: DEFAULT_GENERATED_ALLOWLIST,
494
+ onProgress: () => { },
495
+ spawn: undefined,
496
+ fs: undefined,
497
+ };
125
498
  }
126
499
  return {
127
500
  extraExcludes: options.extraExcludes ?? [],
501
+ generatedAllowlist: options.generatedAllowlist ?? DEFAULT_GENERATED_ALLOWLIST,
128
502
  onProgress: options.onProgress ?? (() => { }),
503
+ spawn: options.spawn,
504
+ fs: options.fs,
129
505
  };
130
506
  }
131
507
  export async function buildArchive(root, version, fmt, options = {}) {
132
- const { extraExcludes, onProgress } = resolveBuildOptions(options);
133
- const excludes = new Set([...DEFAULT_EXCLUDES, ...extraExcludes]);
508
+ const { extraExcludes, generatedAllowlist, onProgress, spawn, fs } = resolveBuildOptions(options);
134
509
  const output = outputPath(root, version, fmt);
135
510
  mkdirSync(dirname(output), { recursive: true });
136
511
  if (existsSync(output)) {
137
512
  unlinkSync(output);
138
513
  }
139
- onProgress({ stage: "scan", detail: "enumerating source files" });
140
- const entries = iterSourceFiles(root, excludes);
514
+ onProgress({ stage: "scan", detail: "enumerating tracked source files" });
515
+ const entries = resolveArchiveEntries(root, { extraExcludes, generatedAllowlist, spawn, fs });
141
516
  const total = entries.length;
142
517
  onProgress({ stage: "scan", detail: `found ${total} files`, current: total, total });
143
- await new Promise((resolvePromise, reject) => {
144
- const out = createWriteStream(output);
145
- const archive = fmt === "zip"
146
- ? new ZipArchive({ zlib: { level: 9 } })
147
- : new TarArchive({ gzip: true, gzipOptions: { level: 9 } });
148
- out.on("close", () => resolvePromise());
149
- out.on("error", (err) => reject(err));
150
- archive.on("error", (err) => reject(err));
151
- archive.pipe(out);
152
- onProgress({ stage: "pack", detail: `format=${fmt}`, current: 0, total });
153
- // Progress ticks every ~5% (or every 250 files) so long packs do not look hung.
154
- const tickEvery = Math.max(1, Math.min(250, Math.ceil(total / 20)));
155
- let i = 0;
156
- for (const { absPath, archiveRel } of entries) {
157
- archive.file(absPath, { name: `${ARCHIVE_ROOT}/${archiveRel}` });
158
- i += 1;
159
- if (i === total || i % tickEvery === 0) {
160
- onProgress({ stage: "pack", current: i, total });
518
+ const uninstallNameHook = installBytePreservingMemberNames();
519
+ try {
520
+ await new Promise((resolvePromise, reject) => {
521
+ const out = createWriteStream(output);
522
+ const archive = fmt === "zip"
523
+ ? new ZipArchive({ zlib: { level: 9 } })
524
+ : new TarArchive({ gzip: true, gzipOptions: { level: 9 } });
525
+ out.on("close", () => resolvePromise());
526
+ out.on("error", (err) => reject(err));
527
+ archive.on("error", (err) => reject(err));
528
+ archive.pipe(out);
529
+ onProgress({ stage: "pack", detail: `format=${fmt}`, current: 0, total });
530
+ // Progress ticks every ~5% (or every 250 files) so long packs do not look hung.
531
+ const tickEvery = Math.max(1, Math.min(250, Math.ceil(total / 20)));
532
+ let i = 0;
533
+ for (const { absPath, archiveRel, archiveRelBytes } of entries) {
534
+ const memberBytes = archiveMemberBytes(archiveRelBytes ?? Buffer.from(archiveRel, "utf8"));
535
+ addArchiveFile(archive, absPath, archiverEntryName(memberBytes));
536
+ i += 1;
537
+ if (i === total || i % tickEvery === 0) {
538
+ onProgress({ stage: "pack", current: i, total });
539
+ }
161
540
  }
162
- }
163
- onProgress({ stage: "finalize", detail: output });
164
- void archive.finalize();
165
- });
541
+ onProgress({ stage: "finalize", detail: output });
542
+ void archive.finalize();
543
+ });
544
+ }
545
+ finally {
546
+ uninstallNameHook();
547
+ }
166
548
  onProgress({ stage: "done", detail: output, current: total, total });
167
549
  return output;
168
550
  }
@@ -0,0 +1,12 @@
1
+ import type { ClosedVerbDecision } from "../authz/types.js";
2
+ import type { ReleaseConfig, ReleaseSeams } from "./types.js";
3
+ export declare const TAG_PUSH_CLOSED_VERB: "release-publish";
4
+ export declare function evaluateReleaseTagPushGate(version: string, projectRoot: string, seams: ReleaseSeams, repo: string): ClosedVerbDecision;
5
+ /**
6
+ * Fail closed before git tag + atomic tag push when skipTag is false.
7
+ * Dry-run and `--skip-tag` do not require a grant (no npm distribution).
8
+ * Returns EXIT_OK to continue, EXIT_VIOLATION to halt. Never spends the grant
9
+ * — draft-flip `release:publish` still owns markGrantUsed (#1095).
10
+ */
11
+ export declare function assertTagPushClosedVerb(config: ReleaseConfig, seams: ReleaseSeams): number;
12
+ //# sourceMappingURL=closed-verb-gate.d.ts.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Closed-verb gate on the tag-push / npm-publish boundary (#3527).
3
+ *
4
+ * Reuses the draft-flip evaluator (`evaluateReleasePublishGate`) so the
5
+ * distributing step carries the same authz tier. Does not delete or weaken
6
+ * the later `release:publish` draft-flip check.
7
+ */
8
+ import { closedVerbEnvBypassKey } from "../authz/closed-verb.js";
9
+ import { evaluateReleasePublishGate } from "../release-publish/pipeline.js";
10
+ import { EXIT_OK, EXIT_VIOLATION, TOTAL_STEPS } from "./constants.js";
11
+ function emitGate(label, status) {
12
+ process.stderr.write(`[10/${TOTAL_STEPS}] ${label}... ${status}\n`);
13
+ }
14
+ export const TAG_PUSH_CLOSED_VERB = "release-publish";
15
+ export function evaluateReleaseTagPushGate(version, projectRoot, seams, repo) {
16
+ return evaluateReleasePublishGate(version, projectRoot, {
17
+ grants: seams.closedVerbGrants,
18
+ env: seams.closedVerbEnv,
19
+ repo,
20
+ });
21
+ }
22
+ /**
23
+ * Fail closed before git tag + atomic tag push when skipTag is false.
24
+ * Dry-run and `--skip-tag` do not require a grant (no npm distribution).
25
+ * Returns EXIT_OK to continue, EXIT_VIOLATION to halt. Never spends the grant
26
+ * — draft-flip `release:publish` still owns markGrantUsed (#1095).
27
+ */
28
+ export function assertTagPushClosedVerb(config, seams) {
29
+ if (config.skipTag) {
30
+ return EXIT_OK;
31
+ }
32
+ const tag = `v${config.version}`;
33
+ const label = `Closed-verb gate ${TAG_PUSH_CLOSED_VERB} ${tag} (tag-push / npm-publish)`;
34
+ if (config.dryRun) {
35
+ emitGate(label, "DRYRUN (would require human-origin grant or DEFT_ALLOW_RELEASE_PUBLISH=1 before tag push)");
36
+ return EXIT_OK;
37
+ }
38
+ const gate = evaluateReleaseTagPushGate(config.version, config.projectRoot, seams, config.repo);
39
+ if (!gate.allowed) {
40
+ const bypassKey = gate.envBypassKey ??
41
+ closedVerbEnvBypassKey(TAG_PUSH_CLOSED_VERB, config.projectRoot) ??
42
+ "DEFT_ALLOW_RELEASE_PUBLISH";
43
+ emitGate(label, `FAIL (${gate.code}: ${gate.reason})`);
44
+ process.stderr.write(`[release] denied code=${gate.code} — set ${bypassKey}=1 or ` +
45
+ `mint \`deft authz:grant -- --template ${TAG_PUSH_CLOSED_VERB} --target ${config.version}\`\n`);
46
+ return EXIT_VIOLATION;
47
+ }
48
+ emitGate(label, `OK (${gate.code}${gate.humanApprovalRef !== null ? ` grant=${gate.humanApprovalRef}` : ""})`);
49
+ return EXIT_OK;
50
+ }
51
+ //# sourceMappingURL=closed-verb-gate.js.map
@@ -1,5 +1,6 @@
1
1
  export * from "./auto-hatch.js";
2
2
  export * from "./build-dist.js";
3
+ export * from "./closed-verb-gate.js";
3
4
  export * from "./constants.js";
4
5
  export * from "./coverage-debt-ledger.js";
5
6
  export * from "./flags.js";
@@ -1,6 +1,7 @@
1
1
  /* v8 ignore file -- re-export barrel; covered via main.ts */
2
2
  export * from "./auto-hatch.js";
3
3
  export * from "./build-dist.js";
4
+ export * from "./closed-verb-gate.js";
4
5
  export * from "./constants.js";
5
6
  export * from "./coverage-debt-ledger.js";
6
7
  export * from "./flags.js";
@@ -4,6 +4,7 @@ import { assertProjectionContained } from "../fs/projection-containment.js";
4
4
  import { readCoverageTotalsFromReport } from "../vitest-runner/coverage-debt.js";
5
5
  import { buildCoverageDebtIssueDraft, classifyStep5FailureWithFreshness, evaluateAutoHatch, formatAutoHatchBanner, parseExitCodeFromReason, reasonLooksLikeTimeout, } from "./auto-hatch.js";
6
6
  import { prependUpgradeBanner, promoteChangelog, sectionForVersion } from "./changelog.js";
7
+ import { assertTagPushClosedVerb } from "./closed-verb-gate.js";
7
8
  import { EXIT_CONFIG_ERROR, EXIT_OK, EXIT_VIOLATION, RELEASE_ARTIFACTS, RELEASE_CHECK_TIMEOUT_MINUTES, TOTAL_STEPS, VERIFY_DRAFT_INTERVAL_SECONDS, VERIFY_DRAFT_MAX_ATTEMPTS, } from "./constants.js";
8
9
  import { createCoverageDebtIssue, probeOpenCoverageDebtLedger } from "./coverage-debt-ledger.js";
9
10
  import { checkTagAvailable, createGithubRelease, readTextFile, verifyReleaseDraft } from "./gh.js";
@@ -373,6 +374,12 @@ export function runPipeline(config, seams = {}) {
373
374
  return EXIT_VIOLATION;
374
375
  }
375
376
  }
377
+ // Step 10–11 boundary: closed-verb gate before tag + atomic push (#3527).
378
+ // npm publish fires on tag push; draft-flip `release:publish` stays (#1095).
379
+ const tagPushGate = assertTagPushClosedVerb(config, seams);
380
+ if (tagPushGate !== EXIT_OK) {
381
+ return tagPushGate;
382
+ }
376
383
  // Step 10: git tag.
377
384
  label = `Tag v${version}`;
378
385
  if (config.skipTag) {
@@ -1,3 +1,5 @@
1
+ import type { EnvMap } from "../authz/closed-verb.js";
2
+ import type { HumanOriginGrant } from "../authz/types.js";
1
3
  export interface ReleaseConfig {
2
4
  readonly version: string;
3
5
  readonly repo: string;
@@ -77,5 +79,15 @@ export interface ReleaseSeams {
77
79
  readonly headSha?: (projectRoot: string) => string | null;
78
80
  /** #3187 — CI detector; when true suite stamp is never trusted. */
79
81
  readonly isCi?: () => boolean;
82
+ /**
83
+ * #3527 — closed-verb grants for the tag-push / npm-publish gate.
84
+ * When omitted, production loads active human-origin grants from disk.
85
+ */
86
+ readonly closedVerbGrants?: readonly HumanOriginGrant[];
87
+ /**
88
+ * #3527 — env map for DEFT_ALLOW_RELEASE_PUBLISH. When omitted, process.env.
89
+ * Tests inject `{}` so a host env bypass cannot leak into fail-closed cases.
90
+ */
91
+ readonly closedVerbEnv?: EnvMap;
80
92
  }
81
93
  //# sourceMappingURL=types.d.ts.map
@@ -1,6 +1,7 @@
1
1
  /** Canonical v0.6 status enum (matches `scripts/spec_validate.py`). */
2
2
  export declare const VALID_STATUSES: Set<string>;
3
- export declare const VALID_VBRIEF_VERSIONS: Set<string>;
3
+ export { VALID_VBRIEF_VERSIONS } from "../vbrief-validate/constants.js";
4
+ export { resolveInfoBlock } from "../vbrief-validate/schema.js";
4
5
  export declare const SPECIFICATION_NARRATIVE_KEY_ORDER: readonly ["Overview", "ProblemStatement", "Goals", "UserStories", "Requirements", "SuccessMetrics", "EdgeCases", "Architecture", "TechDecisions", "ImplementationPhases", "PreImplementationGates"];
5
6
  /** Migration capture narrative; omitted from default compact SPECIFICATION.md (#1566). */
6
7
  export declare const LEGACY_ARTIFACTS_NARRATIVE_KEY = "LegacyArtifacts";
@@ -10,7 +10,8 @@ export const VALID_STATUSES = new Set([
10
10
  "failed",
11
11
  "cancelled",
12
12
  ]);
13
- export const VALID_VBRIEF_VERSIONS = new Set(["0.6"]);
13
+ export { VALID_VBRIEF_VERSIONS } from "../vbrief-validate/constants.js";
14
+ export { resolveInfoBlock } from "../vbrief-validate/schema.js";
14
15
  export const SPECIFICATION_NARRATIVE_KEY_ORDER = [
15
16
  "Overview",
16
17
  "ProblemStatement",