@deftai/directive-core 0.95.0 → 0.96.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.
- package/dist/cache/archive.d.ts +134 -0
- package/dist/cache/archive.js +630 -0
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.js +1 -0
- package/dist/cache/main.js +298 -1
- package/dist/content-contracts/skills/helpers.d.ts +1 -1
- package/dist/content-contracts/skills/helpers.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init-deposit/hygiene.d.ts +1 -1
- package/dist/init-deposit/hygiene.js +16 -4
- package/dist/init-deposit/scaffold.js +6 -5
- package/dist/parent-turn-shape/evaluate.d.ts +84 -0
- package/dist/parent-turn-shape/evaluate.js +353 -0
- package/dist/parent-turn-shape/index.d.ts +8 -0
- package/dist/parent-turn-shape/index.js +8 -0
- package/dist/review-monitor/constants.js +3 -2
- package/dist/review-monitor/tier-detection.d.ts +7 -3
- package/dist/review-monitor/tier-detection.js +18 -1
- package/dist/review-monitor/verify.js +18 -0
- package/dist/scope/index.d.ts +2 -0
- package/dist/scope/index.js +2 -0
- package/dist/scope/main.d.ts +10 -0
- package/dist/scope/main.js +109 -24
- package/dist/scope/promote-from-issue.d.ts +49 -0
- package/dist/scope/promote-from-issue.js +367 -0
- package/dist/scope/promote-path.d.ts +39 -0
- package/dist/scope/promote-path.js +105 -0
- package/dist/swarm/routing.d.ts +4 -2
- package/dist/swarm/routing.js +26 -4
- package/dist/triage/actions/index.js +62 -2
- package/dist/triage/actions/types.d.ts +8 -1
- package/dist/triage/author-filter.d.ts +51 -0
- package/dist/triage/author-filter.js +152 -0
- package/dist/triage/classify/index.d.ts +2 -2
- package/dist/triage/classify/index.js +2 -2
- package/dist/triage/classify/label-mirror.d.ts +68 -5
- package/dist/triage/classify/label-mirror.js +261 -31
- package/dist/triage/help/registry-data.d.ts +49 -38
- package/dist/triage/help/registry-data.js +115 -40
- package/dist/triage/index.d.ts +1 -0
- package/dist/triage/index.js +1 -0
- package/dist/triage/queue/index.d.ts +1 -0
- package/dist/triage/queue/index.js +1 -0
- package/dist/triage/queue/render.d.ts +2 -0
- package/dist/triage/queue/render.js +6 -0
- package/package.json +7 -3
package/dist/scope/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { dirname
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
3
|
import { maybeRunStalenessTickler } from "../staleness-tickler/run.js";
|
|
4
4
|
import { reconcileUmbrellas, renderUmbrellasReport } from "../vbrief-reconcile/umbrellas.js";
|
|
5
5
|
import { canonicalLogPath, readAll } from "./audit-log.js";
|
|
@@ -9,14 +9,18 @@ import { isNonDeliveryDisposition, } from "./delivery-evidence.js";
|
|
|
9
9
|
import { batchDemote, DEFAULT_OLDER_THAN_DAYS, demoteOne, resolveDemoteFilePath, resolveFilePath, resolveProjectRootStrict, } from "./demote.js";
|
|
10
10
|
import { completedPathForScopeMove, findOpenUmbrellaReferences, renderOpenUmbrellaWarning, } from "./open-umbrella-warning.js";
|
|
11
11
|
import { resolveProjectRoot } from "./project-context.js";
|
|
12
|
-
import {
|
|
12
|
+
import { promoteFromIssue } from "./promote-from-issue.js";
|
|
13
|
+
import { promotePath } from "./promote-path.js";
|
|
14
|
+
import { runTransition } from "./transition.js";
|
|
13
15
|
import { findByDecisionId, isAlreadyUndone, REVERSIBLE_ACTIONS, undoBatch, undoOne, } from "./undo.js";
|
|
14
|
-
import { checkWipCap, formatWipCapRefusal } from "./wip-cap-check.js";
|
|
15
16
|
const LIFECYCLE_USAGE_STDERR = "usage: scope_lifecycle.py [-h] [--project-root PROJECT_ROOT] [--force] [--batch]\n" +
|
|
17
|
+
" [--from-issue N] [--repo OWNER/NAME] [--strict] [--force-no-cache]\n" +
|
|
18
|
+
" [--path PATH]\n" +
|
|
16
19
|
" {activate,block,cancel,complete,fail,promote,restore,unblock}\n" +
|
|
17
20
|
" [file ...]\n" +
|
|
18
21
|
"scope_lifecycle.py: error: the following arguments are required: action, file\n" +
|
|
19
|
-
"(promote --batch may omit file and promotes all proposed/ scopes; #3011)\n"
|
|
22
|
+
"(promote --batch may omit file and promotes all proposed/ scopes; #3011)\n" +
|
|
23
|
+
"(promote --from-issue=N may omit file; #1136)\n";
|
|
20
24
|
function parseLifecycleArgv(argv) {
|
|
21
25
|
if (argv.length < 1) {
|
|
22
26
|
return { args: null, error: "usage" };
|
|
@@ -30,6 +34,10 @@ function parseLifecycleArgv(argv) {
|
|
|
30
34
|
let force = false;
|
|
31
35
|
let batch = false;
|
|
32
36
|
const batchFiles = [];
|
|
37
|
+
let fromIssue;
|
|
38
|
+
let strict = false;
|
|
39
|
+
let forceNoCache = false;
|
|
40
|
+
let pathFlag;
|
|
33
41
|
let nonDeliveryDisposition;
|
|
34
42
|
let prNumber;
|
|
35
43
|
let mergeCommit;
|
|
@@ -48,6 +56,37 @@ function parseLifecycleArgv(argv) {
|
|
|
48
56
|
else if (arg === "--batch") {
|
|
49
57
|
batch = true;
|
|
50
58
|
}
|
|
59
|
+
else if (arg === "--strict") {
|
|
60
|
+
strict = true;
|
|
61
|
+
}
|
|
62
|
+
else if (arg === "--force-no-cache") {
|
|
63
|
+
forceNoCache = true;
|
|
64
|
+
}
|
|
65
|
+
else if (arg === "--from-issue") {
|
|
66
|
+
const raw = argv[i + 1];
|
|
67
|
+
i += 1;
|
|
68
|
+
if (raw === undefined || !/^[1-9]\d*$/.test(raw)) {
|
|
69
|
+
return { args: null, error: "usage" };
|
|
70
|
+
}
|
|
71
|
+
fromIssue = Number.parseInt(raw, 10);
|
|
72
|
+
}
|
|
73
|
+
else if (arg?.startsWith("--from-issue=")) {
|
|
74
|
+
const raw = arg.slice("--from-issue=".length);
|
|
75
|
+
if (!/^[1-9]\d*$/.test(raw)) {
|
|
76
|
+
return { args: null, error: "usage" };
|
|
77
|
+
}
|
|
78
|
+
fromIssue = Number.parseInt(raw, 10);
|
|
79
|
+
}
|
|
80
|
+
else if (arg === "--path") {
|
|
81
|
+
pathFlag = argv[i + 1];
|
|
82
|
+
i += 1;
|
|
83
|
+
if (pathFlag === undefined) {
|
|
84
|
+
return { args: null, error: "usage" };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (arg?.startsWith("--path=")) {
|
|
88
|
+
pathFlag = arg.slice("--path=".length);
|
|
89
|
+
}
|
|
51
90
|
else if (arg === "--project-root") {
|
|
52
91
|
projectRoot = argv[i + 1];
|
|
53
92
|
i += 1;
|
|
@@ -127,18 +166,20 @@ function parseLifecycleArgv(argv) {
|
|
|
127
166
|
return { args: null, error: "usage" };
|
|
128
167
|
}
|
|
129
168
|
}
|
|
169
|
+
// Delivery evidence uses --repo as repository; --from-issue also uses --repo as triage slug.
|
|
170
|
+
// Prefer treating --repo as triage slug when --from-issue is set (#1136).
|
|
130
171
|
const deliveryEvidence = prNumber !== undefined ||
|
|
131
172
|
mergeCommit !== undefined ||
|
|
132
173
|
prBase !== undefined ||
|
|
133
174
|
deliveryBranch !== undefined ||
|
|
134
|
-
repo !== undefined ||
|
|
175
|
+
(repo !== undefined && fromIssue === undefined) ||
|
|
135
176
|
mergedAt !== undefined
|
|
136
177
|
? {
|
|
137
178
|
prNumber: prNumber !== undefined && Number.isFinite(prNumber) ? prNumber : null,
|
|
138
179
|
mergeCommit: mergeCommit ?? null,
|
|
139
180
|
prBase: prBase ?? null,
|
|
140
181
|
deliveryBranch: deliveryBranch ?? null,
|
|
141
|
-
repository: repo ?? null,
|
|
182
|
+
repository: fromIssue === undefined ? (repo ?? null) : null,
|
|
142
183
|
mergedAt: mergedAt ?? (mergeCommit !== undefined ? "supplied" : null),
|
|
143
184
|
verifier: "scope:complete",
|
|
144
185
|
}
|
|
@@ -158,6 +199,27 @@ function parseLifecycleArgv(argv) {
|
|
|
158
199
|
},
|
|
159
200
|
};
|
|
160
201
|
}
|
|
202
|
+
if (fromIssue !== undefined) {
|
|
203
|
+
if (action !== "promote") {
|
|
204
|
+
return { args: null, error: "usage" };
|
|
205
|
+
}
|
|
206
|
+
if (Number.isNaN(fromIssue) || fromIssue < 1) {
|
|
207
|
+
return { args: null, error: "usage" };
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
args: {
|
|
211
|
+
action,
|
|
212
|
+
file: file.length > 0 ? file : (pathFlag ?? ""),
|
|
213
|
+
projectRoot,
|
|
214
|
+
force,
|
|
215
|
+
fromIssue,
|
|
216
|
+
repo,
|
|
217
|
+
strict,
|
|
218
|
+
forceNoCache,
|
|
219
|
+
pathFlag: pathFlag ?? (file.length > 0 ? file : undefined),
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
161
223
|
if (file.length === 0) {
|
|
162
224
|
return { args: null, error: "usage" };
|
|
163
225
|
}
|
|
@@ -181,7 +243,7 @@ export function lifecycleMain(argv) {
|
|
|
181
243
|
}
|
|
182
244
|
return 2;
|
|
183
245
|
}
|
|
184
|
-
const { action, file, projectRoot, force, batch, batchFiles, nonDeliveryDisposition, deliveryEvidence, } = parsed.args;
|
|
246
|
+
const { action, file, projectRoot, force, batch, batchFiles, fromIssue, repo, strict, forceNoCache, pathFlag, nonDeliveryDisposition, deliveryEvidence, } = parsed.args;
|
|
185
247
|
if (batch === true && action === "promote") {
|
|
186
248
|
const result = batchPromote({
|
|
187
249
|
files: batchFiles && batchFiles.length > 0 ? batchFiles : undefined,
|
|
@@ -203,21 +265,54 @@ export function lifecycleMain(argv) {
|
|
|
203
265
|
}
|
|
204
266
|
return result.exitCode;
|
|
205
267
|
}
|
|
268
|
+
if (fromIssue !== undefined && action === "promote") {
|
|
269
|
+
const result = promoteFromIssue({
|
|
270
|
+
issueNumber: fromIssue,
|
|
271
|
+
repo: repo ?? null,
|
|
272
|
+
projectRoot,
|
|
273
|
+
force: force === true,
|
|
274
|
+
forceNoCache: forceNoCache === true,
|
|
275
|
+
strict: strict === true,
|
|
276
|
+
explicitPath: pathFlag,
|
|
277
|
+
});
|
|
278
|
+
for (const line of result.warnings) {
|
|
279
|
+
process.stderr.write(`${line}\n`);
|
|
280
|
+
}
|
|
281
|
+
if (result.ok) {
|
|
282
|
+
process.stdout.write(`${result.message}\n`);
|
|
283
|
+
if (result.wipCapOverride) {
|
|
284
|
+
process.stderr.write("\u26a0 WIP cap exceeded; promote allowed via --force. " +
|
|
285
|
+
"audit: scope-lifecycle.jsonl entry tagged wip_cap_override (#1124).\n");
|
|
286
|
+
}
|
|
287
|
+
return 0;
|
|
288
|
+
}
|
|
289
|
+
process.stderr.write(`Error: ${result.message}\n`);
|
|
290
|
+
return result.exitCode;
|
|
291
|
+
}
|
|
206
292
|
const [filePath, error] = resolveFilePath(file, projectRoot);
|
|
207
293
|
if (error !== null || filePath === null) {
|
|
208
294
|
process.stderr.write(`Error: ${error}\n`);
|
|
209
295
|
return 2;
|
|
210
296
|
}
|
|
211
|
-
|
|
297
|
+
// Path-based promote uses shared promotePath for WIP + optional audit consistency (#1136).
|
|
212
298
|
if (action === "promote") {
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
299
|
+
const promoteResult = promotePath(filePath, {
|
|
300
|
+
projectRoot,
|
|
301
|
+
force: force === true,
|
|
302
|
+
});
|
|
303
|
+
if (promoteResult.ok) {
|
|
304
|
+
process.stdout.write(`${promoteResult.message}\n`);
|
|
305
|
+
if (promoteResult.wipCapOverride) {
|
|
306
|
+
process.stderr.write("\u26a0 WIP cap exceeded " +
|
|
307
|
+
`(promote allowed via --force). ` +
|
|
308
|
+
"audit: scope-lifecycle.jsonl entry tagged wip_cap_override (#1124).\n");
|
|
219
309
|
}
|
|
310
|
+
return 0;
|
|
220
311
|
}
|
|
312
|
+
process.stderr.write(promoteResult.exitCode === 1 && promoteResult.message.includes("WIP")
|
|
313
|
+
? `${promoteResult.message}\n`
|
|
314
|
+
: `Error: ${promoteResult.message}\n`);
|
|
315
|
+
return promoteResult.exitCode;
|
|
221
316
|
}
|
|
222
317
|
const transitionOptions = {
|
|
223
318
|
nonDeliveryDisposition,
|
|
@@ -226,16 +321,6 @@ export function lifecycleMain(argv) {
|
|
|
226
321
|
};
|
|
227
322
|
const result = runTransition(action, filePath, new Date(), transitionOptions);
|
|
228
323
|
if (result.ok) {
|
|
229
|
-
if (action === "promote" && capCheck !== null && capCheck.forceOverride) {
|
|
230
|
-
const rootForAudit = resolveProjectRoot(projectRoot);
|
|
231
|
-
if (rootForAudit !== null) {
|
|
232
|
-
const newPath = join(rootForAudit, "vbrief", "pending", filePath.split(/[/\\]/).pop() ?? "");
|
|
233
|
-
recordWipCapOverride(newPath, rootForAudit, capCheck);
|
|
234
|
-
}
|
|
235
|
-
process.stderr.write("\u26a0 WIP cap exceeded " +
|
|
236
|
-
`(count=${capCheck.count}, cap=${capCheck.cap}); promote allowed via --force. ` +
|
|
237
|
-
"audit: vbrief/.eval/scope-lifecycle.jsonl entry tagged wip_cap_override (#1124).\n");
|
|
238
|
-
}
|
|
239
324
|
process.stdout.write(`${result.message}\n`);
|
|
240
325
|
if (action === "complete") {
|
|
241
326
|
try {
|
|
@@ -0,0 +1,49 @@
|
|
|
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 type { AuditEntry } from "../triage/actions/types.js";
|
|
9
|
+
import { type PromotePathResult } from "./promote-path.js";
|
|
10
|
+
export interface PromoteFromIssueOptions {
|
|
11
|
+
readonly issueNumber: number;
|
|
12
|
+
readonly repo?: string | null;
|
|
13
|
+
readonly projectRoot?: string;
|
|
14
|
+
/** WIP-cap override. */
|
|
15
|
+
readonly force?: boolean;
|
|
16
|
+
/** Skip reciprocity gate; still audit that force was used. */
|
|
17
|
+
readonly forceNoCache?: boolean;
|
|
18
|
+
/** Missing decision becomes hard fail (default: soft warn + proceed). */
|
|
19
|
+
readonly strict?: boolean;
|
|
20
|
+
readonly actor?: string;
|
|
21
|
+
/** Explicit path when multiple proposed artifacts match the issue. */
|
|
22
|
+
readonly explicitPath?: string;
|
|
23
|
+
readonly now?: Date;
|
|
24
|
+
/** Optional candidates-log path override (tests). */
|
|
25
|
+
readonly candidatesLogPath?: string;
|
|
26
|
+
/** Inject latest decision (tests). */
|
|
27
|
+
readonly latestDecision?: AuditEntry | null;
|
|
28
|
+
}
|
|
29
|
+
export interface PromoteFromIssueResult extends PromotePathResult {
|
|
30
|
+
readonly warnings: string[];
|
|
31
|
+
readonly repo: string | null;
|
|
32
|
+
readonly matchedPaths: string[];
|
|
33
|
+
readonly cacheStateAtPromote: string | null;
|
|
34
|
+
readonly cacheDecisionId: string | null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Locate lifecycle artifacts whose provenance owns ``issueNumber`` (optionally repo-scoped).
|
|
38
|
+
*/
|
|
39
|
+
export declare function findLifecycleArtifactsForIssue(projectRoot: string, issueNumber: number, options?: {
|
|
40
|
+
folder?: "proposed" | "pending";
|
|
41
|
+
repo?: string | null;
|
|
42
|
+
}): string[];
|
|
43
|
+
/** Locate proposed/ artifacts for issue N (repo-scoped when provided). */
|
|
44
|
+
export declare function findProposedArtifactsForIssue(projectRoot: string, issueNumber: number, repo?: string | null): string[];
|
|
45
|
+
/**
|
|
46
|
+
* Promote the proposed scope for an issue, gated on triage-cache latestDecision.
|
|
47
|
+
*/
|
|
48
|
+
export declare function promoteFromIssue(options: PromoteFromIssueOptions): PromoteFromIssueResult;
|
|
49
|
+
//# sourceMappingURL=promote-from-issue.d.ts.map
|
|
@@ -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
|