@deftai/directive-core 0.95.0 → 0.97.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 (116) hide show
  1. package/dist/cache/archive.d.ts +134 -0
  2. package/dist/cache/archive.js +636 -0
  3. package/dist/cache/index.d.ts +1 -0
  4. package/dist/cache/index.js +1 -0
  5. package/dist/cache/main.js +298 -1
  6. package/dist/check/gate-lists.js +8 -0
  7. package/dist/consumer-check-contract/evaluate.d.ts +124 -0
  8. package/dist/consumer-check-contract/evaluate.js +699 -0
  9. package/dist/consumer-check-contract/index.d.ts +5 -0
  10. package/dist/consumer-check-contract/index.js +5 -0
  11. package/dist/content-contracts/skills/helpers.d.ts +1 -1
  12. package/dist/content-contracts/skills/helpers.js +1 -0
  13. package/dist/delivery-attempt/disk-begin.d.ts +51 -0
  14. package/dist/delivery-attempt/disk-begin.js +68 -0
  15. package/dist/delivery-attempt/evaluate.d.ts +26 -0
  16. package/dist/delivery-attempt/evaluate.js +443 -0
  17. package/dist/delivery-attempt/fingerprint.d.ts +32 -0
  18. package/dist/delivery-attempt/fingerprint.js +100 -0
  19. package/dist/delivery-attempt/handoff.d.ts +25 -0
  20. package/dist/delivery-attempt/handoff.js +102 -0
  21. package/dist/delivery-attempt/index.d.ts +17 -0
  22. package/dist/delivery-attempt/index.js +17 -0
  23. package/dist/delivery-attempt/ledger.d.ts +169 -0
  24. package/dist/delivery-attempt/ledger.js +758 -0
  25. package/dist/delivery-attempt/material-delta.d.ts +38 -0
  26. package/dist/delivery-attempt/material-delta.js +126 -0
  27. package/dist/delivery-attempt/types.d.ts +210 -0
  28. package/dist/delivery-attempt/types.js +77 -0
  29. package/dist/doctor/index.d.ts +1 -0
  30. package/dist/doctor/index.js +1 -0
  31. package/dist/doctor/main.js +12 -0
  32. package/dist/doctor/openclaw-soft-rebind.d.ts +26 -0
  33. package/dist/doctor/openclaw-soft-rebind.js +164 -0
  34. package/dist/hooks/dispatcher.d.ts +2 -1
  35. package/dist/hooks/dispatcher.js +63 -9
  36. package/dist/index.d.ts +5 -0
  37. package/dist/index.js +5 -0
  38. package/dist/init-deposit/gitignore.js +7 -0
  39. package/dist/init-deposit/hygiene.d.ts +1 -1
  40. package/dist/init-deposit/hygiene.js +16 -4
  41. package/dist/init-deposit/init-deposit.js +5 -0
  42. package/dist/init-deposit/refresh.js +3 -0
  43. package/dist/init-deposit/scaffold.js +6 -5
  44. package/dist/parent-turn-shape/evaluate.d.ts +84 -0
  45. package/dist/parent-turn-shape/evaluate.js +353 -0
  46. package/dist/parent-turn-shape/index.d.ts +8 -0
  47. package/dist/parent-turn-shape/index.js +8 -0
  48. package/dist/pr-merge-readiness/ci-gate.d.ts +29 -1
  49. package/dist/pr-merge-readiness/ci-gate.js +191 -24
  50. package/dist/pr-merge-readiness/compute.js +10 -1
  51. package/dist/pr-merge-readiness/index.d.ts +2 -1
  52. package/dist/pr-merge-readiness/index.js +2 -1
  53. package/dist/pr-merge-readiness/output.js +14 -0
  54. package/dist/pr-merge-readiness/platform-status.d.ts +29 -0
  55. package/dist/pr-merge-readiness/platform-status.js +49 -0
  56. package/dist/pr-watch/constants.d.ts +10 -0
  57. package/dist/pr-watch/constants.js +12 -1
  58. package/dist/pr-watch/main.js +16 -1
  59. package/dist/pr-watch/probe.js +13 -1
  60. package/dist/pr-watch/types.d.ts +3 -2
  61. package/dist/pr-watch/watch.js +14 -7
  62. package/dist/review-monitor/constants.js +3 -2
  63. package/dist/review-monitor/tier-detection.d.ts +7 -3
  64. package/dist/review-monitor/tier-detection.js +18 -1
  65. package/dist/review-monitor/verify.js +18 -0
  66. package/dist/scope/index.d.ts +2 -0
  67. package/dist/scope/index.js +2 -0
  68. package/dist/scope/main.d.ts +10 -0
  69. package/dist/scope/main.js +109 -24
  70. package/dist/scope/promote-from-issue.d.ts +49 -0
  71. package/dist/scope/promote-from-issue.js +367 -0
  72. package/dist/scope/promote-path.d.ts +39 -0
  73. package/dist/scope/promote-path.js +105 -0
  74. package/dist/scope-provenance/digest.d.ts +67 -0
  75. package/dist/scope-provenance/digest.js +188 -0
  76. package/dist/scope-provenance/evaluate.d.ts +82 -0
  77. package/dist/scope-provenance/evaluate.js +528 -0
  78. package/dist/scope-provenance/index.d.ts +6 -0
  79. package/dist/scope-provenance/index.js +6 -0
  80. package/dist/session/compact-ritual.d.ts +96 -0
  81. package/dist/session/compact-ritual.js +237 -0
  82. package/dist/session/compact-ritual.spec.d.ts +2 -0
  83. package/dist/session/compact-ritual.spec.js +21 -0
  84. package/dist/session/index.d.ts +2 -0
  85. package/dist/session/index.js +2 -0
  86. package/dist/session/openclaw-soft-rebind-deposit.d.ts +46 -0
  87. package/dist/session/openclaw-soft-rebind-deposit.js +165 -0
  88. package/dist/swarm/routing.d.ts +4 -2
  89. package/dist/swarm/routing.js +26 -4
  90. package/dist/test-boundary/evaluate.d.ts +54 -0
  91. package/dist/test-boundary/evaluate.js +368 -0
  92. package/dist/test-boundary/index.d.ts +6 -0
  93. package/dist/test-boundary/index.js +6 -0
  94. package/dist/test-boundary/policy.d.ts +52 -0
  95. package/dist/test-boundary/policy.js +182 -0
  96. package/dist/triage/actions/index.js +62 -2
  97. package/dist/triage/actions/types.d.ts +8 -1
  98. package/dist/triage/author-filter.d.ts +51 -0
  99. package/dist/triage/author-filter.js +152 -0
  100. package/dist/triage/bootstrap/gitignore.d.ts +1 -1
  101. package/dist/triage/bootstrap/gitignore.js +15 -1
  102. package/dist/triage/classify/index.d.ts +2 -2
  103. package/dist/triage/classify/index.js +2 -2
  104. package/dist/triage/classify/label-mirror.d.ts +68 -5
  105. package/dist/triage/classify/label-mirror.js +261 -31
  106. package/dist/triage/help/registry-data.d.ts +49 -38
  107. package/dist/triage/help/registry-data.js +115 -40
  108. package/dist/triage/index.d.ts +1 -0
  109. package/dist/triage/index.js +1 -0
  110. package/dist/triage/queue/index.d.ts +1 -0
  111. package/dist/triage/queue/index.js +1 -0
  112. package/dist/triage/queue/render.d.ts +2 -0
  113. package/dist/triage/queue/render.js +6 -0
  114. package/dist/vbrief-activate/activate.js +22 -6
  115. package/dist/xbrief/styles.js +33 -17
  116. package/package.json +11 -3
@@ -0,0 +1,367 @@
1
+ /**
2
+ * scope:promote --from-issue reciprocity gate (#1136 / D18).
3
+ *
4
+ * Gates promote of a proposed scope on the latest triage-cache decision for
5
+ * the issue, locates the proposed artifact via provenance scan, and runs the
6
+ * shared promotePath path (WIP rules unchanged).
7
+ */
8
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
9
+ import { isAbsolute, join, resolve } from "node:path";
10
+ import { provenanceIssueNumber, scanProvenanceRefs } from "../intake/issue-ingest.js";
11
+ import { hasArtifactSuffix, resolveLifecycleRoot } from "../layout/resolve.js";
12
+ import { createCandidatesLog, resolveAuditLogPath } from "../triage/actions/candidates-log.js";
13
+ import { resolveRepo } from "../triage/queue/repo.js";
14
+ import { resolveProjectRoot } from "./project-context.js";
15
+ import { promotePath } from "./promote-path.js";
16
+ /** Collapse CR/LF in operator-facing messages (SLizard CWE-116). */
17
+ function sanitizeMsg(value) {
18
+ return value.replace(/\r?\n/g, " ");
19
+ }
20
+ /** True when brief references or Origin point at owner/name issue N. */
21
+ function briefMatchesIssueAndRepo(data, issueNumber, repo) {
22
+ if (provenanceIssueNumber(data) !== issueNumber) {
23
+ // Also accept plan.references github-issue URIs without Origin text.
24
+ const plan = data.plan;
25
+ if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
26
+ return false;
27
+ }
28
+ const refs = plan.references;
29
+ if (!Array.isArray(refs)) {
30
+ return false;
31
+ }
32
+ let hit = false;
33
+ for (const ref of refs) {
34
+ if (typeof ref !== "object" || ref === null || Array.isArray(ref))
35
+ continue;
36
+ const uri = String(ref.uri ?? "");
37
+ if (uri.includes(`/issues/${issueNumber}`)) {
38
+ hit = true;
39
+ if (repo === null ||
40
+ uri.includes(`github.com/${repo}/`) ||
41
+ uri.includes(`${repo}/issues/`)) {
42
+ return true;
43
+ }
44
+ }
45
+ }
46
+ return hit && repo === null;
47
+ }
48
+ if (repo === null) {
49
+ return true;
50
+ }
51
+ const plan = data.plan;
52
+ if (typeof plan === "object" && plan !== null && !Array.isArray(plan)) {
53
+ const refs = plan.references;
54
+ if (Array.isArray(refs)) {
55
+ for (const ref of refs) {
56
+ if (typeof ref !== "object" || ref === null || Array.isArray(ref))
57
+ continue;
58
+ const uri = String(ref.uri ?? "");
59
+ if (uri.includes(`/issues/${issueNumber}`) &&
60
+ (uri.includes(`github.com/${repo}/`) || uri.includes(`${repo}/issues/`))) {
61
+ return true;
62
+ }
63
+ }
64
+ }
65
+ const narratives = plan.narratives;
66
+ if (typeof narratives === "object" && narratives !== null && !Array.isArray(narratives)) {
67
+ const origin = String(narratives.Origin ?? "");
68
+ if (origin.includes(`github.com/${repo}/issues/${issueNumber}`)) {
69
+ return true;
70
+ }
71
+ }
72
+ }
73
+ // Provenance matched issue number but no repo-scoped URI — refuse when repo known.
74
+ return false;
75
+ }
76
+ /**
77
+ * Locate lifecycle artifacts whose provenance owns ``issueNumber`` (optionally repo-scoped).
78
+ */
79
+ export function findLifecycleArtifactsForIssue(projectRoot, issueNumber, options = {}) {
80
+ const folder = options.folder ?? "proposed";
81
+ const repo = options.repo ?? null;
82
+ const root = resolve(projectRoot);
83
+ let lifecycleRoot;
84
+ try {
85
+ lifecycleRoot = resolveLifecycleRoot(root);
86
+ }
87
+ catch {
88
+ return [];
89
+ }
90
+ const byIssue = scanProvenanceRefs(lifecycleRoot);
91
+ const rels = (byIssue.get(issueNumber) ?? []).filter((rel) => {
92
+ const f = rel.split(/[/\\]/)[0];
93
+ return f === folder;
94
+ });
95
+ const candidates = rels.map((rel) => join(lifecycleRoot, rel));
96
+ // Always also scan the folder for Origin-only scaffolds.
97
+ const folderDir = join(lifecycleRoot, folder);
98
+ if (existsSync(folderDir)) {
99
+ for (const name of readdirSync(folderDir).filter((f) => hasArtifactSuffix(f))) {
100
+ candidates.push(join(folderDir, name));
101
+ }
102
+ }
103
+ const out = [];
104
+ for (const abs of uniqueExisting(candidates)) {
105
+ try {
106
+ const data = JSON.parse(readFileSync(abs, "utf8"));
107
+ if (briefMatchesIssueAndRepo(data, issueNumber, repo)) {
108
+ out.push(abs);
109
+ }
110
+ }
111
+ catch {
112
+ /* skip */
113
+ }
114
+ }
115
+ return uniqueExisting(out);
116
+ }
117
+ /** Locate proposed/ artifacts for issue N (repo-scoped when provided). */
118
+ export function findProposedArtifactsForIssue(projectRoot, issueNumber, repo = null) {
119
+ return findLifecycleArtifactsForIssue(projectRoot, issueNumber, { folder: "proposed", repo });
120
+ }
121
+ function uniqueExisting(paths) {
122
+ const seen = new Set();
123
+ const out = [];
124
+ for (const p of paths) {
125
+ const key = resolve(p);
126
+ if (seen.has(key))
127
+ continue;
128
+ seen.add(key);
129
+ if (existsSync(key)) {
130
+ out.push(key);
131
+ }
132
+ }
133
+ return out.sort();
134
+ }
135
+ function resolveLatestDecision(options, projectRoot, repo) {
136
+ if (options.latestDecision !== undefined) {
137
+ return options.latestDecision;
138
+ }
139
+ const log = createCandidatesLog(projectRoot);
140
+ const path = options.candidatesLogPath ?? resolveAuditLogPath(projectRoot);
141
+ return log.latestDecision(options.issueNumber, repo, { path });
142
+ }
143
+ /**
144
+ * Promote the proposed scope for an issue, gated on triage-cache latestDecision.
145
+ */
146
+ export function promoteFromIssue(options) {
147
+ const warnings = [];
148
+ const projectRoot = resolveProjectRoot(options.projectRoot);
149
+ if (projectRoot === null) {
150
+ return {
151
+ ok: false,
152
+ message: "Cannot determine project root. Pass --project-root PATH, set $DEFT_PROJECT_ROOT, or run from inside a directory tree that contains vbrief/ or .git/ (#535).",
153
+ exitCode: 2,
154
+ warnings,
155
+ repo: null,
156
+ matchedPaths: [],
157
+ cacheStateAtPromote: null,
158
+ cacheDecisionId: null,
159
+ };
160
+ }
161
+ const repo = resolveRepo(options.repo, projectRoot);
162
+ if (repo === null) {
163
+ return {
164
+ ok: false,
165
+ message: "scope:promote --from-issue requires --repo OWNER/NAME (or $DEFT_TRIAGE_REPO / git remote origin).",
166
+ exitCode: 2,
167
+ warnings,
168
+ repo: null,
169
+ matchedPaths: [],
170
+ cacheStateAtPromote: null,
171
+ cacheDecisionId: null,
172
+ };
173
+ }
174
+ const repoSafe = sanitizeMsg(repo);
175
+ const n = options.issueNumber;
176
+ if (!Number.isInteger(n) || n < 1) {
177
+ return {
178
+ ok: false,
179
+ message: `Invalid --from-issue value: ${String(options.issueNumber)} (expected positive integer).`,
180
+ exitCode: 2,
181
+ warnings,
182
+ repo,
183
+ matchedPaths: [],
184
+ cacheStateAtPromote: null,
185
+ cacheDecisionId: null,
186
+ };
187
+ }
188
+ const latest = resolveLatestDecision(options, projectRoot, repo);
189
+ const cacheState = latest?.decision ?? null;
190
+ const cacheDecisionId = latest?.decision_id ?? null;
191
+ const forceNoCache = options.forceNoCache === true;
192
+ if (!forceNoCache) {
193
+ if (latest === null) {
194
+ if (options.strict === true) {
195
+ return {
196
+ ok: false,
197
+ message: `No triage-cache decision for #${n} (${repoSafe}). ` +
198
+ `Accept first: task triage:accept -- --issue ${n} --repo ${repoSafe} ` +
199
+ `(or omit --strict to soft-warn and proceed; --force-no-cache skips the gate).`,
200
+ exitCode: 1,
201
+ warnings,
202
+ repo,
203
+ matchedPaths: [],
204
+ cacheStateAtPromote: null,
205
+ cacheDecisionId: null,
206
+ };
207
+ }
208
+ warnings.push(`[scope:promote --from-issue] no triage-cache decision for #${n} (${repoSafe}); proceeding (soft warn). Use --strict to fail.`);
209
+ }
210
+ else if (latest.decision !== "accept") {
211
+ return {
212
+ ok: false,
213
+ message: `Refusing promote for #${n} (${repoSafe}): latest triage decision is '${sanitizeMsg(latest.decision)}' ` +
214
+ `(decision_id=${latest.decision_id}). ` +
215
+ `Accept first: task triage:accept -- --issue ${n} --repo ${repoSafe} ` +
216
+ `or override with --force-no-cache.`,
217
+ exitCode: 1,
218
+ warnings,
219
+ repo,
220
+ matchedPaths: [],
221
+ cacheStateAtPromote: cacheState,
222
+ cacheDecisionId,
223
+ };
224
+ }
225
+ }
226
+ else {
227
+ warnings.push(`[scope:promote --from-issue] --force-no-cache: skipped reciprocity gate for #${n} ` +
228
+ `(cache decision was ${cacheState === null ? "absent" : `'${sanitizeMsg(cacheState)}'`}).`);
229
+ }
230
+ // Already in pending/ for this issue → idempotent success (auto-promote re-entry).
231
+ const alreadyPending = findLifecycleArtifactsForIssue(projectRoot, n, {
232
+ folder: "pending",
233
+ repo,
234
+ });
235
+ if (alreadyPending.length === 1 && options.explicitPath === undefined) {
236
+ warnings.push(`[scope:promote --from-issue] #${n} already pending (${sanitizeMsg(alreadyPending[0] ?? "")}); no-op.`);
237
+ return {
238
+ ok: true,
239
+ message: `No-op: issue #${n} already has pending scope ${alreadyPending[0]}`,
240
+ exitCode: 0,
241
+ warnings,
242
+ repo,
243
+ matchedPaths: alreadyPending,
244
+ cacheStateAtPromote: cacheState,
245
+ cacheDecisionId: forceNoCache && cacheState !== "accept" ? null : cacheDecisionId,
246
+ destPath: alreadyPending[0],
247
+ };
248
+ }
249
+ let matched = findProposedArtifactsForIssue(projectRoot, n, repo);
250
+ if (options.explicitPath !== undefined && options.explicitPath.trim().length > 0) {
251
+ const raw = options.explicitPath.trim();
252
+ const explicit = isAbsolute(raw) ? resolve(raw) : resolve(projectRoot, raw);
253
+ if (!existsSync(explicit)) {
254
+ return {
255
+ ok: false,
256
+ message: `Explicit path not found: ${sanitizeMsg(explicit)}`,
257
+ exitCode: 2,
258
+ warnings,
259
+ repo,
260
+ matchedPaths: matched,
261
+ cacheStateAtPromote: cacheState,
262
+ cacheDecisionId,
263
+ };
264
+ }
265
+ // Never accept an unrelated path — must match issue provenance (and repo).
266
+ try {
267
+ const data = JSON.parse(readFileSync(explicit, "utf8"));
268
+ if (!briefMatchesIssueAndRepo(data, n, repo)) {
269
+ return {
270
+ ok: false,
271
+ message: `Explicit path is not a provenance match for #${n} (${repoSafe}): ${sanitizeMsg(explicit)}.`,
272
+ exitCode: 2,
273
+ warnings,
274
+ repo,
275
+ matchedPaths: matched,
276
+ cacheStateAtPromote: cacheState,
277
+ cacheDecisionId,
278
+ };
279
+ }
280
+ }
281
+ catch (err) {
282
+ return {
283
+ ok: false,
284
+ message: `Explicit path is not readable JSON: ${sanitizeMsg(explicit)} (${String(err)})`,
285
+ exitCode: 2,
286
+ warnings,
287
+ repo,
288
+ matchedPaths: matched,
289
+ cacheStateAtPromote: cacheState,
290
+ cacheDecisionId,
291
+ };
292
+ }
293
+ if (matched.length > 0) {
294
+ const hit = matched.find((p) => resolve(p) === explicit);
295
+ if (hit === undefined) {
296
+ return {
297
+ ok: false,
298
+ message: `Explicit path is not among proposed matches for #${n}: ${sanitizeMsg(explicit)}. ` +
299
+ `Matched: ${matched.map(sanitizeMsg).join(", ")}`,
300
+ exitCode: 2,
301
+ warnings,
302
+ repo,
303
+ matchedPaths: matched,
304
+ cacheStateAtPromote: cacheState,
305
+ cacheDecisionId,
306
+ };
307
+ }
308
+ matched = [hit];
309
+ }
310
+ else {
311
+ matched = [explicit];
312
+ }
313
+ }
314
+ if (matched.length === 0) {
315
+ return {
316
+ ok: false,
317
+ message: `No proposed/ scope artifact found for issue #${n} (${repoSafe}). ` +
318
+ `Ingest first (task triage:accept -- --issue ${n} --repo ${repoSafe}) ` +
319
+ `or pass an explicit path: task scope:promote -- <path>.`,
320
+ exitCode: 1,
321
+ warnings,
322
+ repo,
323
+ matchedPaths: [],
324
+ cacheStateAtPromote: cacheState,
325
+ cacheDecisionId,
326
+ };
327
+ }
328
+ if (matched.length > 1) {
329
+ const list = matched.map((p) => ` - ${sanitizeMsg(p)}`).join("\n");
330
+ return {
331
+ ok: false,
332
+ message: `Multiple proposed/ artifacts match issue #${n}; refuse to guess.\n${list}\n` +
333
+ `Promote one path explicitly: task scope:promote -- <path> ` +
334
+ `or re-run with --path <one-of-the-above>.`,
335
+ exitCode: 1,
336
+ warnings,
337
+ repo,
338
+ matchedPaths: matched,
339
+ cacheStateAtPromote: cacheState,
340
+ cacheDecisionId,
341
+ };
342
+ }
343
+ const filePath = matched[0];
344
+ const promoteResult = promotePath(filePath, {
345
+ projectRoot,
346
+ force: options.force,
347
+ actor: options.actor,
348
+ now: options.now,
349
+ fromIssue: n,
350
+ cacheDecisionId: forceNoCache && cacheState !== "accept" ? null : cacheDecisionId,
351
+ cacheStateAtPromote: cacheState,
352
+ forceNoCache,
353
+ requireAudit: true,
354
+ });
355
+ warnings.push(`[scope:promote --from-issue] cache decision for #${n} was ` +
356
+ `${cacheState === null ? "absent" : `'${sanitizeMsg(cacheState)}'`}` +
357
+ (promoteResult.ok ? " (proceeded)" : " (promote failed)"));
358
+ return {
359
+ ...promoteResult,
360
+ warnings,
361
+ repo,
362
+ matchedPaths: matched,
363
+ cacheStateAtPromote: cacheState,
364
+ cacheDecisionId: forceNoCache && cacheState !== "accept" ? null : cacheDecisionId,
365
+ };
366
+ }
367
+ //# sourceMappingURL=promote-from-issue.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shared single-path promote (proposed/ → pending/) with optional triage audit linkage (#1136).
3
+ */
4
+ export interface PromotePathOptions {
5
+ readonly projectRoot?: string;
6
+ /** WIP-cap override (--force). */
7
+ readonly force?: boolean;
8
+ readonly actor?: string;
9
+ readonly now?: Date;
10
+ /** Issue number that triggered this promote (from-issue / auto-promote). */
11
+ readonly fromIssue?: number;
12
+ /** Accept (or other) decision_id from candidates.jsonl, when known. */
13
+ readonly cacheDecisionId?: string | null;
14
+ /** Latest cache decision string at promote time (accept / defer / null). */
15
+ readonly cacheStateAtPromote?: string | null;
16
+ /** True when reciprocity gate was skipped via --force-no-cache. */
17
+ readonly forceNoCache?: boolean;
18
+ /** Write promote audit entry even without from-issue linkage (default: only when linkage present). */
19
+ readonly alwaysAudit?: boolean;
20
+ /**
21
+ * When true, audit append failure is a hard error (from-issue / auto-promote
22
+ * require from_issue + cache fields). Default false for plain path promote.
23
+ */
24
+ readonly requireAudit?: boolean;
25
+ }
26
+ export interface PromotePathResult {
27
+ readonly ok: boolean;
28
+ readonly message: string;
29
+ readonly exitCode: number;
30
+ readonly destPath?: string;
31
+ readonly auditEntry?: Record<string, unknown> | null;
32
+ readonly wipCapOverride?: boolean;
33
+ }
34
+ /**
35
+ * Promote a single proposed-scope path to pending/, enforcing WIP and
36
+ * optionally recording from_issue / cache_decision_id on the scope audit log.
37
+ */
38
+ export declare function promotePath(filePath: string, options?: PromotePathOptions): PromotePathResult;
39
+ //# sourceMappingURL=promote-path.d.ts.map
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Shared single-path promote (proposed/ → pending/) with optional triage audit linkage (#1136).
3
+ */
4
+ import { existsSync } from "node:fs";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { append, canonicalLogPath, newDecisionId } from "./audit-log.js";
7
+ import { resolveProjectRoot } from "./project-context.js";
8
+ import { recordWipCapOverride, runTransition } from "./transition.js";
9
+ import { utcNowIso } from "./vbrief-json.js";
10
+ import { canonicalRelpath } from "./vbrief-ref.js";
11
+ import { checkWipCap, formatWipCapRefusal } from "./wip-cap-check.js";
12
+ /**
13
+ * Promote a single proposed-scope path to pending/, enforcing WIP and
14
+ * optionally recording from_issue / cache_decision_id on the scope audit log.
15
+ */
16
+ export function promotePath(filePath, options = {}) {
17
+ const root = resolveProjectRoot(options.projectRoot);
18
+ if (root === null) {
19
+ return {
20
+ ok: false,
21
+ message: "Cannot determine project root. Pass --project-root PATH, set $DEFT_PROJECT_ROOT, or run from inside a directory tree that contains vbrief/ or .git/ (#535).",
22
+ exitCode: 2,
23
+ };
24
+ }
25
+ const resolved = resolve(filePath);
26
+ if (!existsSync(resolved)) {
27
+ return { ok: false, message: `File not found: ${resolved}`, exitCode: 2 };
28
+ }
29
+ const capCheck = checkWipCap(root, options.force === true);
30
+ if (!capCheck.allowed) {
31
+ return {
32
+ ok: false,
33
+ message: formatWipCapRefusal(capCheck),
34
+ exitCode: 1,
35
+ };
36
+ }
37
+ const now = options.now ?? new Date();
38
+ const result = runTransition("promote", resolved, now);
39
+ if (!result.ok) {
40
+ return { ok: false, message: result.message, exitCode: 1 };
41
+ }
42
+ const basename = resolved.split(/[/\\]/).pop() ?? "";
43
+ // Destination after promote is sibling pending/ under the same lifecycle root.
44
+ const lifecycleRoot = dirname(dirname(resolved));
45
+ const destPath = join(lifecycleRoot, "pending", basename);
46
+ let auditEntry = null;
47
+ const shouldAudit = options.alwaysAudit === true ||
48
+ options.fromIssue !== undefined ||
49
+ options.forceNoCache === true ||
50
+ options.cacheDecisionId !== undefined ||
51
+ options.cacheStateAtPromote !== undefined;
52
+ if (shouldAudit) {
53
+ try {
54
+ const entry = {
55
+ decision_id: newDecisionId(),
56
+ timestamp: utcNowIso(now),
57
+ action: "promote",
58
+ vbrief_path: canonicalRelpath(destPath, root),
59
+ from_status: "proposed",
60
+ to_status: "pending",
61
+ actor: options.actor ?? "operator",
62
+ };
63
+ if (options.fromIssue !== undefined) {
64
+ entry.from_issue = options.fromIssue;
65
+ }
66
+ if (options.cacheDecisionId !== undefined) {
67
+ entry.cache_decision_id = options.cacheDecisionId;
68
+ }
69
+ if (options.cacheStateAtPromote !== undefined) {
70
+ entry.cache_state_at_promote = options.cacheStateAtPromote;
71
+ }
72
+ if (options.forceNoCache === true) {
73
+ entry.force_no_cache = true;
74
+ }
75
+ append(entry, canonicalLogPath(root));
76
+ auditEntry = entry;
77
+ }
78
+ catch (err) {
79
+ if (options.requireAudit === true) {
80
+ return {
81
+ ok: false,
82
+ message: `Promoted ${basename} to pending/ but required scope audit failed: ${String(err)}. ` +
83
+ `Artifact is in pending/; re-run promote will no-op once audit is writable.`,
84
+ exitCode: 1,
85
+ destPath,
86
+ auditEntry: null,
87
+ wipCapOverride: capCheck.forceOverride,
88
+ };
89
+ }
90
+ /* best-effort audit for plain path promote */
91
+ }
92
+ }
93
+ if (capCheck.forceOverride) {
94
+ recordWipCapOverride(destPath, root, capCheck, now);
95
+ }
96
+ return {
97
+ ok: true,
98
+ message: result.message,
99
+ exitCode: 0,
100
+ destPath,
101
+ auditEntry,
102
+ wipCapOverride: capCheck.forceOverride,
103
+ };
104
+ }
105
+ //# sourceMappingURL=promote-path.js.map
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Approved-scope digest helpers (#3145).
3
+ *
4
+ * At activation/approval time, record an immutable digest of the human-approved
5
+ * file_scope. PR verification compares the live active xBRIEF against this
6
+ * baseline so same-PR xBRIEF edits cannot self-authorize new paths.
7
+ */
8
+ /** Immutable approved-scope record written under `.deft/approved-scope/`. */
9
+ export interface ApprovedScopeRecord {
10
+ readonly schemaVersion: 1;
11
+ /** Repo-relative path of the active xBRIEF at approval time. */
12
+ readonly xbriefRelPath: string;
13
+ readonly planId: string;
14
+ /** ISO-8601 UTC timestamp of approval / activation. */
15
+ readonly approvedAt: string;
16
+ /** Sorted unique file_scope paths frozen at approval. */
17
+ readonly fileScope: readonly string[];
18
+ /** sha256 hex of normalized file_scope lines. */
19
+ readonly fileScopeDigest: string;
20
+ /**
21
+ * Optional human-origin stamp (refs #2944). When present on renewal,
22
+ * re-approval is accepted. Agent-only stamps are ignored by evaluate.
23
+ */
24
+ readonly humanApproval?: {
25
+ readonly kind: string;
26
+ readonly actor: string;
27
+ readonly mintedAt: string;
28
+ readonly mintedVia?: string;
29
+ };
30
+ /** Digest of the full xBRIEF canonical JSON body (optional stronger pin). */
31
+ readonly xbriefBodyDigest?: string;
32
+ }
33
+ export declare const APPROVED_SCOPE_DIR = ".deft/approved-scope";
34
+ /** Normalize and sort scope paths for stable digests. */
35
+ export declare function normalizeFileScope(paths: readonly string[]): string[];
36
+ /** sha256 hex of normalized file_scope (one path per line). */
37
+ export declare function computeFileScopeDigest(paths: readonly string[]): string;
38
+ /** sha256 hex of raw UTF-8 text. */
39
+ export declare function computeTextDigest(text: string): string;
40
+ /** Extract plan.metadata.swarm.file_scope from an xBRIEF payload. */
41
+ export declare function extractFileScope(payload: unknown): string[];
42
+ /** Extract plan.id from an xBRIEF payload. */
43
+ export declare function extractPlanId(payload: unknown): string | null;
44
+ export declare function approvedScopeDir(projectRoot: string): string;
45
+ export declare function approvedScopeRecordPath(projectRoot: string, planId: string): string;
46
+ /** Build an ApprovedScopeRecord from xBRIEF payload + metadata. */
47
+ export declare function buildApprovedScopeRecord(input: {
48
+ readonly xbriefRelPath: string;
49
+ readonly payload: unknown;
50
+ readonly approvedAt?: string;
51
+ readonly humanApproval?: ApprovedScopeRecord["humanApproval"];
52
+ readonly xbriefRawText?: string;
53
+ }): ApprovedScopeRecord;
54
+ /** Persist approved-scope record (activation / renewed human approval). */
55
+ export declare function writeApprovedScopeRecord(projectRoot: string, record: ApprovedScopeRecord): string;
56
+ /** Load one approved-scope record by plan id, or null if missing. */
57
+ export declare function readApprovedScopeRecord(projectRoot: string, planId: string): ApprovedScopeRecord | null;
58
+ /** Load all approved-scope records under `.deft/approved-scope/`. */
59
+ export declare function listApprovedScopeRecords(projectRoot: string): ApprovedScopeRecord[];
60
+ /**
61
+ * Paths present in `current` but not in `approved` (normalized).
62
+ * These are the expansion set that cannot self-authorize.
63
+ */
64
+ export declare function scopeExpansion(approved: readonly string[], current: readonly string[]): string[];
65
+ /** True when actor/kind look like human provenance (mirrors authz subset). */
66
+ export declare function isHumanApprovalStamp(stamp: ApprovedScopeRecord["humanApproval"] | null | undefined): boolean;
67
+ //# sourceMappingURL=digest.d.ts.map