@deftai/directive-core 0.99.0 → 0.101.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 (166) hide show
  1. package/dist/authz/actions.d.ts +6 -0
  2. package/dist/authz/actions.js +3 -0
  3. package/dist/authz/classify.js +241 -30
  4. package/dist/authz/decompose-apply.d.ts +81 -0
  5. package/dist/authz/decompose-apply.js +377 -0
  6. package/dist/authz/index.d.ts +3 -1
  7. package/dist/authz/index.js +3 -1
  8. package/dist/authz/store.d.ts +53 -0
  9. package/dist/authz/store.js +219 -1
  10. package/dist/authz/types.d.ts +19 -2
  11. package/dist/authz/types.js +5 -1
  12. package/dist/check/cached-orchestrator.d.ts +26 -5
  13. package/dist/check/cached-orchestrator.js +228 -6
  14. package/dist/check/gate-lists.d.ts +10 -4
  15. package/dist/check/gate-lists.js +19 -5
  16. package/dist/check/index.d.ts +3 -2
  17. package/dist/check/index.js +2 -1
  18. package/dist/check/named-cause.d.ts +45 -0
  19. package/dist/check/named-cause.js +121 -0
  20. package/dist/check/orchestrator.js +6 -1
  21. package/dist/doctor/checks.d.ts +44 -0
  22. package/dist/doctor/checks.js +428 -2
  23. package/dist/doctor/help.d.ts +5 -0
  24. package/dist/doctor/help.js +31 -0
  25. package/dist/doctor/index.d.ts +2 -0
  26. package/dist/doctor/index.js +2 -0
  27. package/dist/doctor/main.d.ts +6 -7
  28. package/dist/doctor/main.js +130 -79
  29. package/dist/doctor/session-coda.d.ts +82 -0
  30. package/dist/doctor/session-coda.js +151 -0
  31. package/dist/doctor/types.d.ts +16 -0
  32. package/dist/doctor/which.d.ts +20 -1
  33. package/dist/doctor/which.js +67 -1
  34. package/dist/finish-loop/pr-finish-loop.d.ts +13 -2
  35. package/dist/finish-loop/pr-finish-loop.js +116 -5
  36. package/dist/finish-loop/types.d.ts +1 -1
  37. package/dist/hooks/dispatcher.d.ts +9 -5
  38. package/dist/hooks/dispatcher.js +23 -15
  39. package/dist/hooks/readonly.d.ts +6 -3
  40. package/dist/hooks/readonly.js +44 -25
  41. package/dist/index.d.ts +3 -0
  42. package/dist/index.js +3 -0
  43. package/dist/init-deposit/agent-hooks.d.ts +20 -0
  44. package/dist/init-deposit/agent-hooks.js +51 -31
  45. package/dist/init-deposit/gitignore.js +2 -0
  46. package/dist/intake/issue-ingest.js +36 -0
  47. package/dist/lifecycle/completed-consistency.d.ts +60 -0
  48. package/dist/lifecycle/completed-consistency.js +443 -0
  49. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +47 -0
  50. package/dist/lifecycle/completed-tracked-on-delivery.js +415 -0
  51. package/dist/lifecycle/index.d.ts +2 -0
  52. package/dist/lifecycle/index.js +2 -0
  53. package/dist/literal-acceptance/capture.d.ts +52 -0
  54. package/dist/literal-acceptance/capture.js +658 -0
  55. package/dist/literal-acceptance/evaluate.d.ts +47 -0
  56. package/dist/literal-acceptance/evaluate.js +193 -0
  57. package/dist/literal-acceptance/index.d.ts +12 -0
  58. package/dist/literal-acceptance/index.js +12 -0
  59. package/dist/literal-acceptance/run.d.ts +38 -0
  60. package/dist/literal-acceptance/run.js +216 -0
  61. package/dist/literal-acceptance/safety.d.ts +24 -0
  62. package/dist/literal-acceptance/safety.js +251 -0
  63. package/dist/literal-acceptance/types.d.ts +82 -0
  64. package/dist/literal-acceptance/types.js +26 -0
  65. package/dist/policy/ac-pass-banking.d.ts +58 -0
  66. package/dist/policy/ac-pass-banking.js +150 -0
  67. package/dist/policy/ceremony-dial.d.ts +96 -11
  68. package/dist/policy/ceremony-dial.js +250 -23
  69. package/dist/policy/index.d.ts +2 -0
  70. package/dist/policy/index.js +16 -1
  71. package/dist/policy/merge-approval-head.d.ts +129 -0
  72. package/dist/policy/merge-approval-head.js +461 -0
  73. package/dist/policy/require-human-merge.d.ts +5 -0
  74. package/dist/policy/require-human-merge.js +5 -0
  75. package/dist/pr-merge-readiness/ci-gate.d.ts +20 -3
  76. package/dist/pr-merge-readiness/ci-gate.js +65 -5
  77. package/dist/pr-merge-readiness/compute.d.ts +13 -0
  78. package/dist/pr-merge-readiness/compute.js +99 -14
  79. package/dist/pr-merge-readiness/gh.d.ts +49 -0
  80. package/dist/pr-merge-readiness/gh.js +268 -0
  81. package/dist/pr-merge-readiness/index.d.ts +2 -2
  82. package/dist/pr-merge-readiness/index.js +2 -2
  83. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +4 -0
  84. package/dist/pr-wait-mergeable/cascade.d.ts +11 -0
  85. package/dist/pr-wait-mergeable/cascade.js +115 -1
  86. package/dist/pr-wait-mergeable/main.d.ts +4 -0
  87. package/dist/pr-wait-mergeable/main.js +4 -0
  88. package/dist/pr-wait-mergeable/types.d.ts +3 -1
  89. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -1
  90. package/dist/pr-wait-mergeable/wrappers.js +7 -1
  91. package/dist/preflight/evaluate.d.ts +15 -1
  92. package/dist/preflight/evaluate.js +33 -2
  93. package/dist/preflight-cache/evaluate.d.ts +3 -0
  94. package/dist/preflight-cache/evaluate.js +3 -0
  95. package/dist/product-first-done-gate/acceptance.d.ts +25 -0
  96. package/dist/product-first-done-gate/acceptance.js +247 -0
  97. package/dist/product-first-done-gate/check-mode.d.ts +45 -0
  98. package/dist/product-first-done-gate/check-mode.js +148 -0
  99. package/dist/product-first-done-gate/evaluate.d.ts +50 -0
  100. package/dist/product-first-done-gate/evaluate.js +255 -0
  101. package/dist/product-first-done-gate/index.d.ts +11 -0
  102. package/dist/product-first-done-gate/index.js +11 -0
  103. package/dist/product-first-done-gate/types.d.ts +64 -0
  104. package/dist/product-first-done-gate/types.js +61 -0
  105. package/dist/render/framework-commands.js +4 -0
  106. package/dist/run-summary/emit.d.ts +56 -0
  107. package/dist/run-summary/emit.js +171 -0
  108. package/dist/run-summary/index.d.ts +4 -0
  109. package/dist/run-summary/index.js +4 -0
  110. package/dist/run-summary/path.d.ts +25 -0
  111. package/dist/run-summary/path.js +78 -0
  112. package/dist/run-summary/types.d.ts +76 -0
  113. package/dist/run-summary/types.js +21 -0
  114. package/dist/scope/acceptance-evidence.d.ts +110 -0
  115. package/dist/scope/acceptance-evidence.js +422 -0
  116. package/dist/scope/coverage-map.d.ts +98 -0
  117. package/dist/scope/coverage-map.js +558 -0
  118. package/dist/scope/decompose.d.ts +5 -0
  119. package/dist/scope/decompose.js +134 -11
  120. package/dist/scope/index.d.ts +3 -0
  121. package/dist/scope/index.js +3 -0
  122. package/dist/scope/parent-lineage.d.ts +125 -0
  123. package/dist/scope/parent-lineage.js +626 -0
  124. package/dist/scope/transition.d.ts +8 -0
  125. package/dist/scope/transition.js +61 -2
  126. package/dist/session/ac-pass-banking.d.ts +221 -0
  127. package/dist/session/ac-pass-banking.js +761 -0
  128. package/dist/session/active-cli.d.ts +79 -0
  129. package/dist/session/active-cli.js +382 -0
  130. package/dist/session/deposit-sha.d.ts +49 -0
  131. package/dist/session/deposit-sha.js +121 -0
  132. package/dist/session/effort-budget.d.ts +136 -0
  133. package/dist/session/effort-budget.js +352 -0
  134. package/dist/session/index.d.ts +7 -0
  135. package/dist/session/index.js +7 -0
  136. package/dist/session/orientation-compression.d.ts +127 -0
  137. package/dist/session/orientation-compression.js +425 -0
  138. package/dist/session/orientation-state.d.ts +29 -0
  139. package/dist/session/orientation-state.js +85 -0
  140. package/dist/session/session-start.d.ts +31 -0
  141. package/dist/session/session-start.js +228 -2
  142. package/dist/session/toolchain-preflight.d.ts +64 -0
  143. package/dist/session/toolchain-preflight.js +151 -0
  144. package/dist/session/verify-session-ritual.d.ts +14 -0
  145. package/dist/session/verify-session-ritual.js +33 -0
  146. package/dist/story-ready/evaluate.d.ts +11 -0
  147. package/dist/story-ready/evaluate.js +41 -3
  148. package/dist/triage/actions/candidates-log.d.ts +1 -2
  149. package/dist/triage/actions/candidates-log.js +37 -6
  150. package/dist/triage/bootstrap/gitignore.d.ts +1 -1
  151. package/dist/triage/bootstrap/gitignore.js +8 -2
  152. package/dist/triage/bootstrap/index.js +15 -3
  153. package/dist/triage/classify/index.d.ts +1 -0
  154. package/dist/triage/classify/index.js +2 -0
  155. package/dist/triage/classify/label-mirror.js +16 -0
  156. package/dist/triage/classify/mirror-discovery-tip.d.ts +95 -0
  157. package/dist/triage/classify/mirror-discovery-tip.js +234 -0
  158. package/dist/triage/reconcile/reconcile.js +28 -6
  159. package/dist/triage/summary/index.js +12 -4
  160. package/dist/triage/welcome/default-mode.js +16 -0
  161. package/dist/ts-check-lane/run-lane.js +21 -4
  162. package/dist/vbrief-validate/conformance.js +7 -0
  163. package/dist/vitest-runner/coverage-debt-teardown.js +16 -4
  164. package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
  165. package/dist/xbrief-migrate/migrate-project.js +92 -3
  166. package/package.json +15 -3
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Human-origin structural apply gate for scope:decompose (#3239 / epic #3237 Q2).
3
+ *
4
+ * Apply requires a human-origin grant bound to:
5
+ * project root identity (worktree and/or repo), parent path, draft/target path,
6
+ * SHA-256 of exact draft bytes, and op scope.decompose.apply.structural.
7
+ *
8
+ * --check stays ungated (caller responsibility). Missing / expired / mismatched /
9
+ * agent-origin grants fail closed before any child story writes.
10
+ */
11
+ import { createHash } from "node:crypto";
12
+ import { readFileSync } from "node:fs";
13
+ import { isAbsolute, relative, resolve, sep } from "node:path";
14
+ import { mintHumanOriginGrant } from "./actions.js";
15
+ import { evidenceSatisfiesImplementationApproval, isHumanOriginGrant, isRejectedOriginKind, } from "./origin.js";
16
+ import { listGrants } from "./store.js";
17
+ import { SCOPE_DECOMPOSE_APPLY_STRUCTURAL, } from "./types.js";
18
+ /** SHA-256 hex of a buffer or utf8 string. */
19
+ export function sha256Hex(data) {
20
+ return createHash("sha256").update(data).digest("hex");
21
+ }
22
+ /** SHA-256 hex of file bytes at path (exact on-disk digest). */
23
+ export function sha256FileHex(path) {
24
+ return sha256Hex(readFileSync(path));
25
+ }
26
+ /**
27
+ * Quote a CLI token for operator copy-paste when it has whitespace or shell
28
+ * metacharacters (Greptile #3291 / PR #3300). Safe bare tokens stay unquoted
29
+ * so deny-reason mint hints match the common path shape.
30
+ *
31
+ * Single quotes keep `$()`, backticks, and `!` inert. Apostrophe escape differs
32
+ * by shell: POSIX uses `'\''`; PowerShell uses `''`. Callers that need both
33
+ * pass dialect `"posix"` or `"pwsh"`. Does not rewrite path bytes (no newline
34
+ * flattening) — callers must refuse control chars before quoting.
35
+ */
36
+ function shellQuoteCliArg(value, dialect) {
37
+ // Allowlist: alnum + common path/repo chars without whitespace or shell meta.
38
+ if (value.length > 0 && /^[A-Za-z0-9_./:@+=,-]+$/.test(value)) {
39
+ return value;
40
+ }
41
+ if (dialect === "pwsh") {
42
+ // PowerShell single-quoted string: only ' is special, doubled as ''.
43
+ return `'${value.replace(/'/g, "''")}'`;
44
+ }
45
+ // POSIX single-quote; only ' needs escaping as '\''
46
+ return `'${value.replace(/'/g, `'\\''`)}'`;
47
+ }
48
+ function mintCommandWithDialect(parent, draft, repo, dialect) {
49
+ return (`deft authz:grant -- --parent ${shellQuoteCliArg(parent, dialect)} --draft ${shellQuoteCliArg(draft, dialect)}` +
50
+ (repo ? ` --repo ${shellQuoteCliArg(repo, dialect)}` : "") +
51
+ " --confirm");
52
+ }
53
+ /** True when a path/repo token cannot be represented as an exact shell mint arg. */
54
+ function hasUnsafeMintControlChars(value) {
55
+ return /[\r\n\0]/.test(value);
56
+ }
57
+ /**
58
+ * Exact operator CLI mint command for a structural decompose apply grant (#3291).
59
+ * Paths are project-relative POSIX when inside projectRoot; otherwise as provided.
60
+ * Parent / draft / repo tokens are shell-quoted when they contain whitespace or
61
+ * shell metacharacters. When POSIX and PowerShell apostrophe escapes differ,
62
+ * both forms are emitted on separate labeled lines (`bash:` / `pwsh:`) so each
63
+ * is independently copy-pasteable. Paths containing newline or NUL are refused
64
+ * (no rewritten fake path) so the operator renames before minting. Does not
65
+ * include agent/CI/TTY gates — those remain on the CLI multi-factor path.
66
+ */
67
+ export function formatDecomposeStructuralMintCommand(parentPath, draftPath, options) {
68
+ let parent = parentPath.replace(/\\/g, "/");
69
+ let draft = draftPath.replace(/\\/g, "/");
70
+ const root = options?.projectRoot;
71
+ if (root) {
72
+ parent = toProjectRelativePosix(root, parentPath) ?? parent;
73
+ draft = toProjectRelativePosix(root, draftPath) ?? draft;
74
+ }
75
+ const repo = (options?.repo ?? "").trim();
76
+ if (hasUnsafeMintControlChars(parent) ||
77
+ hasUnsafeMintControlChars(draft) ||
78
+ hasUnsafeMintControlChars(repo)) {
79
+ return ("rename parent/draft/repo to remove newline or NUL characters, then run: " +
80
+ "deft authz:grant -- --parent <parent.xbrief.json> --draft <draft.json> [--repo owner/name] --confirm");
81
+ }
82
+ const posix = mintCommandWithDialect(parent, draft, repo, "posix");
83
+ const pwsh = mintCommandWithDialect(parent, draft, repo, "pwsh");
84
+ // Labels on their own lines so the executable command is independently pasteable.
85
+ if (posix === pwsh)
86
+ return posix;
87
+ return `bash:\n${posix}\npwsh:\n${pwsh}`;
88
+ }
89
+ /**
90
+ * Normalize a path to project-relative POSIX form for grant binding compare.
91
+ * Returns null when the path escapes the project root.
92
+ */
93
+ export function toProjectRelativePosix(projectRoot, path) {
94
+ const root = resolve(projectRoot);
95
+ const abs = isAbsolute(path) ? resolve(path) : resolve(root, path);
96
+ const rel = relative(root, abs);
97
+ if (rel.startsWith("..") || rel === "..")
98
+ return null;
99
+ // Windows absolute on different drive yields absolute rel — treat as escape.
100
+ if (isAbsolute(rel))
101
+ return null;
102
+ return rel.split(sep).join("/");
103
+ }
104
+ function normalizeDigest(raw) {
105
+ if (raw === null || raw === undefined)
106
+ return null;
107
+ const t = raw.trim().toLowerCase();
108
+ if (t.length === 0)
109
+ return null;
110
+ // Accept optional sha256: prefix.
111
+ return t.startsWith("sha256:") ? t.slice("sha256:".length) : t;
112
+ }
113
+ /**
114
+ * Exact path equality for grant binding (POSIX separators only).
115
+ * Case-sensitive: case-variant paths are distinct on case-sensitive filesystems
116
+ * (#3239 Greptile P1). Windows operators mint with the same relative form
117
+ * produce via toProjectRelativePosix.
118
+ */
119
+ function pathsEqual(a, b) {
120
+ return a.replace(/\\/g, "/") === b.replace(/\\/g, "/");
121
+ }
122
+ function grantValidity(grant, now, mintHint) {
123
+ if (!isHumanOriginGrant(grant)) {
124
+ const kind = grant.origin.kind;
125
+ if (isRejectedOriginKind(kind)) {
126
+ return {
127
+ ok: false,
128
+ code: "authz-grant-origin-reject",
129
+ reason: `Directive denied scope:decompose apply: grant ${grant.id} origin.kind=${kind} is ` +
130
+ "agent/self-authored and cannot approve a decomposition draft. " +
131
+ `Human action required: \`${mintHint}\`.`,
132
+ };
133
+ }
134
+ return {
135
+ ok: false,
136
+ code: "authz-grant-origin-reject",
137
+ reason: `Directive denied scope:decompose apply: grant ${grant.id} lacks human-origin provenance. ` +
138
+ `Human action required: \`${mintHint}\`.`,
139
+ };
140
+ }
141
+ if (grant.semantics.revokedAt !== null) {
142
+ return {
143
+ ok: false,
144
+ code: "authz-grant-revoked",
145
+ reason: `Directive denied scope:decompose apply: grant ${grant.id} was revoked at ${grant.semantics.revokedAt}.`,
146
+ };
147
+ }
148
+ if (grant.semantics.singleUse && grant.semantics.usedAt !== null) {
149
+ return {
150
+ ok: false,
151
+ code: "authz-grant-single-use-spent",
152
+ reason: `Directive denied scope:decompose apply: single-use grant ${grant.id} already spent at ${grant.semantics.usedAt}. ` +
153
+ `Human action required: \`${mintHint}\`.`,
154
+ };
155
+ }
156
+ if (grant.semantics.expiresAt !== null) {
157
+ const exp = Date.parse(grant.semantics.expiresAt);
158
+ if (!Number.isNaN(exp) && exp <= now.getTime()) {
159
+ return {
160
+ ok: false,
161
+ code: "authz-grant-expired",
162
+ reason: `Directive denied scope:decompose apply: grant ${grant.id} expired at ${grant.semantics.expiresAt}. ` +
163
+ `Human action required: \`${mintHint}\`.`,
164
+ };
165
+ }
166
+ }
167
+ return { ok: true };
168
+ }
169
+ function grantHasStructuralOp(grant) {
170
+ return grant.scope.operations.includes(SCOPE_DECOMPOSE_APPLY_STRUCTURAL);
171
+ }
172
+ /**
173
+ * Evaluate whether a human-origin grant covers structural decompose apply for
174
+ * the exact draft digest + parent + target + project identity.
175
+ */
176
+ export function evaluateDecomposeStructuralApply(input) {
177
+ const now = input.now ?? new Date();
178
+ const digest = normalizeDigest(input.draftDigest) ?? "";
179
+ const parentRel = toProjectRelativePosix(input.projectRoot, input.parentPath);
180
+ const targetRel = toProjectRelativePosix(input.projectRoot, input.draftPath);
181
+ const projectAbs = resolve(input.projectRoot);
182
+ const grants = input.grants ?? listGrants(input.projectRoot);
183
+ const mintHint = formatDecomposeStructuralMintCommand(input.parentPath, input.draftPath, {
184
+ projectRoot: input.projectRoot,
185
+ repo: input.repo,
186
+ });
187
+ if (parentRel === null || targetRel === null) {
188
+ return {
189
+ allowed: false,
190
+ code: "authz-grant-scope-deny",
191
+ reason: "Directive denied scope:decompose apply: parent or draft path is outside the project root. " +
192
+ `Human action required: \`${mintHint}\` with paths inside the project root.`,
193
+ humanApprovalRef: null,
194
+ draftDigest: digest,
195
+ };
196
+ }
197
+ let lastReject = null;
198
+ for (const grant of grants) {
199
+ if (!evidenceSatisfiesImplementationApproval({ grant })) {
200
+ const validity = grantValidity(grant, now, mintHint);
201
+ if (!validity.ok) {
202
+ lastReject = { code: validity.code, reason: validity.reason, grantId: grant.id };
203
+ continue;
204
+ }
205
+ lastReject = {
206
+ code: "authz-grant-origin-reject",
207
+ reason: `Directive denied scope:decompose apply: grant ${grant.id} does not satisfy ` +
208
+ `human-origin implementation approval. Human action required: \`${mintHint}\`.`,
209
+ grantId: grant.id,
210
+ };
211
+ continue;
212
+ }
213
+ const validity = grantValidity(grant, now, mintHint);
214
+ if (!validity.ok) {
215
+ lastReject = { code: validity.code, reason: validity.reason, grantId: grant.id };
216
+ continue;
217
+ }
218
+ if (!grantHasStructuralOp(grant)) {
219
+ lastReject = {
220
+ code: "authz-grant-scope-deny",
221
+ reason: `Directive denied scope:decompose apply: grant ${grant.id} does not include operation ` +
222
+ `'${SCOPE_DECOMPOSE_APPLY_STRUCTURAL}'. Human action required: \`${mintHint}\`.`,
223
+ grantId: grant.id,
224
+ };
225
+ continue;
226
+ }
227
+ const boundDigest = normalizeDigest(grant.scope.contentDigest ?? null);
228
+ const boundParent = grant.scope.parentPath?.trim() ?? "";
229
+ const boundTarget = grant.scope.targetPath?.trim() ?? "";
230
+ const boundRepo = grant.scope.repo?.trim() ?? "";
231
+ const boundWorktree = grant.scope.worktree?.trim() ?? "";
232
+ if (boundDigest === null || boundParent.length === 0 || boundTarget.length === 0) {
233
+ lastReject = {
234
+ code: "authz-grant-binding-incomplete",
235
+ reason: `Directive denied scope:decompose apply: grant ${grant.id} is missing required ` +
236
+ "structural bindings (contentDigest, parentPath, targetPath). " +
237
+ `Human action required: \`${mintHint}\`.`,
238
+ grantId: grant.id,
239
+ };
240
+ continue;
241
+ }
242
+ if (boundRepo.length === 0 && boundWorktree.length === 0) {
243
+ lastReject = {
244
+ code: "authz-grant-binding-incomplete",
245
+ reason: `Directive denied scope:decompose apply: grant ${grant.id} does not bind project ` +
246
+ "identity (repo and/or worktree). " +
247
+ `Human action required: \`${mintHint}\`.`,
248
+ grantId: grant.id,
249
+ };
250
+ continue;
251
+ }
252
+ if (boundDigest !== digest) {
253
+ lastReject = {
254
+ code: "authz-grant-digest-mismatch",
255
+ reason: `Directive denied scope:decompose apply: grant ${grant.id} is bound to draft digest ` +
256
+ `${boundDigest.slice(0, 12)}… but the draft bytes hash to ${digest.slice(0, 12)}…. ` +
257
+ "Any content change after approval invalidates the grant. " +
258
+ `Human action required: \`${mintHint}\`.`,
259
+ grantId: grant.id,
260
+ };
261
+ continue;
262
+ }
263
+ if (!pathsEqual(boundParent, parentRel)) {
264
+ lastReject = {
265
+ code: "authz-grant-parent-mismatch",
266
+ reason: `Directive denied scope:decompose apply: grant ${grant.id} is bound to parent ` +
267
+ `'${boundParent}' but apply targets '${parentRel}'. ` +
268
+ `Human action required: \`${mintHint}\`.`,
269
+ grantId: grant.id,
270
+ };
271
+ continue;
272
+ }
273
+ if (!pathsEqual(boundTarget, targetRel)) {
274
+ lastReject = {
275
+ code: "authz-grant-target-mismatch",
276
+ reason: `Directive denied scope:decompose apply: grant ${grant.id} is bound to draft path ` +
277
+ `'${boundTarget}' but apply uses '${targetRel}'. ` +
278
+ `Human action required: \`${mintHint}\`.`,
279
+ grantId: grant.id,
280
+ };
281
+ continue;
282
+ }
283
+ // Project identity: when grant pins repo and/or worktree, attempt must match.
284
+ if (boundRepo.length > 0) {
285
+ const attemptRepo = (input.repo ?? "").trim();
286
+ if (attemptRepo.length === 0 || attemptRepo.toLowerCase() !== boundRepo.toLowerCase()) {
287
+ lastReject = {
288
+ code: "authz-grant-project-mismatch",
289
+ reason: `Directive denied scope:decompose apply: grant ${grant.id} is bound to repo ` +
290
+ `'${boundRepo}' which does not match the apply context. ` +
291
+ `Human action required: \`${mintHint}\`.`,
292
+ grantId: grant.id,
293
+ };
294
+ continue;
295
+ }
296
+ }
297
+ if (boundWorktree.length > 0) {
298
+ const grantWt = resolve(boundWorktree);
299
+ // Worktree compare is path-normalize only (resolve); keep exact string match of
300
+ // resolved absolute paths without case-folding (same rule as parent/target paths).
301
+ if (grantWt !== projectAbs) {
302
+ lastReject = {
303
+ code: "authz-grant-project-mismatch",
304
+ reason: `Directive denied scope:decompose apply: grant ${grant.id} is bound to worktree ` +
305
+ `'${boundWorktree}' which does not match project root '${projectAbs}'. ` +
306
+ `Human action required: \`${mintHint}\`.`,
307
+ grantId: grant.id,
308
+ };
309
+ continue;
310
+ }
311
+ }
312
+ return {
313
+ allowed: true,
314
+ code: "authz-allow",
315
+ reason: `Directive allowed scope:decompose apply via human-origin grant ${grant.id} ` +
316
+ `bound to draft digest ${digest.slice(0, 12)}….`,
317
+ humanApprovalRef: grant.id,
318
+ draftDigest: digest,
319
+ };
320
+ }
321
+ if (lastReject !== null) {
322
+ return {
323
+ allowed: false,
324
+ code: lastReject.code,
325
+ reason: lastReject.reason,
326
+ humanApprovalRef: lastReject.grantId,
327
+ draftDigest: digest,
328
+ };
329
+ }
330
+ return {
331
+ allowed: false,
332
+ code: "authz-grant-missing",
333
+ reason: "Directive denied scope:decompose apply: no human-origin grant covers " +
334
+ `'${SCOPE_DECOMPOSE_APPLY_STRUCTURAL}' for this draft digest. ` +
335
+ `Human action required: \`${mintHint}\` ` +
336
+ "(self-authored lifecycle/dispatch tokens do not count). --check remains ungated.",
337
+ humanApprovalRef: null,
338
+ draftDigest: digest,
339
+ };
340
+ }
341
+ /**
342
+ * Operator mint helper: human-origin grant bound to exact draft bytes + paths.
343
+ * Sole mint path remains mintHumanOriginGrant (operator-cli origin).
344
+ */
345
+ export function mintDecomposeStructuralApplyGrant(input) {
346
+ const parentRel = toProjectRelativePosix(input.projectRoot, input.parentPath);
347
+ const targetRel = toProjectRelativePosix(input.projectRoot, input.draftPath);
348
+ if (parentRel === null || targetRel === null) {
349
+ throw new Error("mintDecomposeStructuralApplyGrant: parentPath and draftPath must resolve inside projectRoot");
350
+ }
351
+ const digest = input.contentDigest !== undefined
352
+ ? (normalizeDigest(input.contentDigest) ?? "")
353
+ : sha256FileHex(isAbsolute(input.draftPath)
354
+ ? input.draftPath
355
+ : resolve(input.projectRoot, input.draftPath));
356
+ if (digest.length === 0) {
357
+ throw new Error("mintDecomposeStructuralApplyGrant: contentDigest must be non-empty");
358
+ }
359
+ const mintInput = {
360
+ projectRoot: input.projectRoot,
361
+ actor: input.actor,
362
+ operations: [SCOPE_DECOMPOSE_APPLY_STRUCTURAL],
363
+ contentDigest: digest,
364
+ parentPath: parentRel,
365
+ targetPath: targetRel,
366
+ worktree: resolve(input.projectRoot),
367
+ repo: input.repo ?? null,
368
+ expiresAt: input.expiresAt ?? null,
369
+ singleUse: input.singleUse === true,
370
+ grantId: input.grantId,
371
+ now: input.now,
372
+ pinActive: input.pinActive,
373
+ eventRef: input.eventRef ?? null,
374
+ };
375
+ return mintHumanOriginGrant(mintInput);
376
+ }
377
+ //# sourceMappingURL=decompose-apply.js.map
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Human-origin approval grants + UAT mutation lease (#2944 Wave 1)
3
- * + closed-verb release gates / AFK templates (#1095 Wave 4).
3
+ * + closed-verb release gates / AFK templates (#1095 Wave 4)
4
+ * + structural scope:decompose apply digest gate (#3239).
4
5
  */
5
6
  export * from "./actions.js";
6
7
  export * from "./classify.js";
7
8
  export * from "./closed-verb.js";
9
+ export * from "./decompose-apply.js";
8
10
  export * from "./evaluate.js";
9
11
  export * from "./origin.js";
10
12
  export * from "./paths.js";
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Human-origin approval grants + UAT mutation lease (#2944 Wave 1)
3
- * + closed-verb release gates / AFK templates (#1095 Wave 4).
3
+ * + closed-verb release gates / AFK templates (#1095 Wave 4)
4
+ * + structural scope:decompose apply digest gate (#3239).
4
5
  */
5
6
  export * from "./actions.js";
6
7
  export * from "./classify.js";
7
8
  export * from "./closed-verb.js";
9
+ export * from "./decompose-apply.js";
8
10
  export * from "./evaluate.js";
9
11
  export * from "./origin.js";
10
12
  export * from "./paths.js";
@@ -25,6 +25,59 @@ export declare function loadAuthzStateResult(projectRoot: string): AuthzStateLoa
25
25
  export declare function loadAuthzState(projectRoot: string): AuthzState;
26
26
  /** Persist single-use grant consumption after an allow decision (#2944). */
27
27
  export declare function markGrantUsed(projectRoot: string, grantId: string, now?: Date): HumanOriginGrant | null;
28
+ /** Exclusive claim lock body under `.deft/authz/locks/<id>.lock` (#3239). */
29
+ export interface GrantClaimLockRecord {
30
+ readonly pid: number;
31
+ readonly startedAt: string;
32
+ readonly token: string;
33
+ }
34
+ /**
35
+ * Whether a claim lock may be reclaimed after a crashed holder (#3239).
36
+ * Live owner PIDs are never reclaimed (no mtime-only steal of a live critical section).
37
+ * Corrupt / unreadable records are reclaimable; PID-reuse residual needs manual delete.
38
+ */
39
+ export declare function isGrantClaimLockReclaimable(rec: GrantClaimLockRecord | null): boolean;
40
+ export interface ClaimSingleUseGrantOptions {
41
+ readonly now?: Date;
42
+ /**
43
+ * Re-check grant after exclusive lock (revocation/expiry/origin/bindings).
44
+ * Called before protected apply / single-use mark so an invalidated grant cannot authorize writes.
45
+ */
46
+ readonly revalidate?: (grant: HumanOriginGrant) => {
47
+ ok: true;
48
+ } | {
49
+ ok: false;
50
+ reason: string;
51
+ };
52
+ /**
53
+ * Protected work under the exclusive claim (#3239 residual).
54
+ * Order: lock → revalidate → mark single-use usedAt → apply → release lock.
55
+ * If apply throws, usedAt is rolled back (grant reusable) and the lock is released.
56
+ * Concurrent claimants fail closed while the lock is held.
57
+ */
58
+ readonly apply?: (grant: HumanOriginGrant) => void;
59
+ }
60
+ /**
61
+ * Claim a single-use grant for structural apply (#3239).
62
+ *
63
+ * Concurrent-safe + failure-safe order when `apply` is provided:
64
+ * exclusive lock → re-load → revalidate → mark single-use usedAt → apply (writes)
65
+ * → on apply throw: rollback usedAt + release lock (grant reusable for retry).
66
+ * Multi-use grants run apply (if any) without mutating usedAt.
67
+ * Without `apply`, single-use is marked under the lock (claim-only / test path).
68
+ *
69
+ * Crash after mark + before rollback: grant stays spent (no double-apply); operator remints.
70
+ * Dead-PID / corrupt locks reclaim via **rename-away** of the old lock path (only one
71
+ * renamer wins) then exclusive create — never blind rmSync of a live winner's lock.
72
+ * Live PIDs are never reclaimed. PID-reuse residual: operator deletes the lock file.
73
+ */
74
+ export declare function claimSingleUseGrantForApply(projectRoot: string, grantId: string, options?: ClaimSingleUseGrantOptions | Date): {
75
+ ok: true;
76
+ grant: HumanOriginGrant;
77
+ } | {
78
+ ok: false;
79
+ reason: string;
80
+ };
28
81
  export declare function saveAuthzState(projectRoot: string, state: AuthzState): void;
29
82
  export declare function loadGrant(projectRoot: string, grantId: string): HumanOriginGrant | null;
30
83
  export declare function saveGrant(projectRoot: string, grant: HumanOriginGrant): void;