@deftai/directive-core 0.81.0 → 0.82.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/content-contracts/skills/helpers.d.ts +6 -0
- package/dist/content-contracts/skills/helpers.js +47 -3
- package/dist/content-contracts/skills/skill-external-fetch-gate.d.ts +18 -0
- package/dist/content-contracts/skills/skill-external-fetch-gate.js +81 -0
- package/dist/eval/crud-telemetry.d.ts +2 -0
- package/dist/eval/crud-telemetry.js +30 -0
- package/dist/intake/github-body-cli.js +2 -0
- package/dist/intake/github-body.d.ts +11 -0
- package/dist/intake/github-body.js +95 -24
- package/dist/integration-e2e/helpers.js +1 -1
- package/dist/issue-sync/sync-from-xbrief.js +3 -0
- package/dist/orphan-active/evaluate.d.ts +25 -0
- package/dist/orphan-active/evaluate.js +275 -0
- package/dist/orphan-active/index.d.ts +3 -0
- package/dist/orphan-active/index.js +3 -0
- package/dist/orphan-active/refs.d.ts +16 -0
- package/dist/orphan-active/refs.js +105 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +8 -0
- package/dist/pr-wait-mergeable/cascade.js +18 -0
- package/dist/pr-wait-mergeable/main.d.ts +4 -0
- package/dist/pr-wait-mergeable/main.js +44 -73
- package/dist/pr-wait-mergeable/result.d.ts +2 -1
- package/dist/pr-wait-mergeable/result.js +4 -0
- package/dist/pr-wait-mergeable/semantic-green.d.ts +27 -0
- package/dist/pr-wait-mergeable/semantic-green.js +202 -0
- package/dist/pr-wait-mergeable/types.d.ts +1 -0
- package/dist/pr-watch/constants.d.ts +4 -4
- package/dist/pr-watch/constants.js +4 -4
- package/dist/pr-watch/watch.js +5 -3
- package/dist/release/native-steps.js +11 -4
- package/dist/render/framework-commands.js +6 -0
- package/dist/scope/brief-io.d.ts +26 -0
- package/dist/scope/brief-io.js +77 -0
- package/dist/scope/decompose.js +2 -2
- package/dist/scope/decomposed-refs.js +3 -3
- package/dist/scope/demote.js +2 -2
- package/dist/scope/project-definition-sync.d.ts +2 -2
- package/dist/scope/project-definition-sync.js +15 -3
- package/dist/scope/registry-artifact-sync.d.ts +6 -1
- package/dist/scope/registry-artifact-sync.js +31 -13
- package/dist/scope/transition.js +23 -15
- package/dist/scope/undo.js +2 -2
- package/dist/scope/vbrief-json.d.ts +1 -2
- package/dist/scope/vbrief-json.js +1 -4
- package/dist/staleness-tickler/state.js +19 -2
- package/dist/triage/queue/cache.js +5 -0
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/verify-source/skill-external-fetch-gate.d.ts +21 -0
- package/dist/verify-source/skill-external-fetch-gate.js +71 -0
- package/package.json +7 -3
|
@@ -11,6 +11,9 @@ export function toResultDict(result) {
|
|
|
11
11
|
if (Object.keys(result.protectedCheck).length > 0) {
|
|
12
12
|
payload.protected_check = result.protectedCheck;
|
|
13
13
|
}
|
|
14
|
+
if (Object.keys(result.semanticGreen).length > 0) {
|
|
15
|
+
payload.semantic_green = result.semanticGreen;
|
|
16
|
+
}
|
|
14
17
|
if (result.mergeStdout.length > 0) {
|
|
15
18
|
payload.merge_stdout = result.mergeStdout;
|
|
16
19
|
}
|
|
@@ -26,6 +29,7 @@ export function makeResult(fields) {
|
|
|
26
29
|
return {
|
|
27
30
|
monitorResult: fields.monitorResult ?? {},
|
|
28
31
|
protectedCheck: fields.protectedCheck ?? {},
|
|
32
|
+
semanticGreen: fields.semanticGreen ?? {},
|
|
29
33
|
mergeStdout: fields.mergeStdout ?? "",
|
|
30
34
|
mergeStderr: fields.mergeStderr ?? "",
|
|
31
35
|
prNumber: fields.prNumber,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RunGhFn } from "../pr-merge-readiness/types.js";
|
|
2
|
+
export interface SemanticGreenOptions {
|
|
3
|
+
/** Enable merge-cascade semantic-green checks (#2385). */
|
|
4
|
+
readonly cascadeMode?: boolean;
|
|
5
|
+
/** After a prior cascade merge, require target-branch CI green at HEAD before the next merge. */
|
|
6
|
+
readonly requireMasterCiGreen?: boolean;
|
|
7
|
+
/** Target branch for stale-base comparison (defaults to PR base.ref). */
|
|
8
|
+
readonly baseBranch?: string | null;
|
|
9
|
+
readonly runGh?: RunGhFn;
|
|
10
|
+
}
|
|
11
|
+
export interface SemanticGreenPayload {
|
|
12
|
+
readonly pr_base_sha: string | null;
|
|
13
|
+
readonly target_branch: string | null;
|
|
14
|
+
readonly target_head_sha: string | null;
|
|
15
|
+
readonly master_ci: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface SemanticGreenResult {
|
|
18
|
+
readonly ok: boolean;
|
|
19
|
+
readonly outcome: string | null;
|
|
20
|
+
readonly exitCode: number | null;
|
|
21
|
+
readonly error: string | null;
|
|
22
|
+
readonly payload: SemanticGreenPayload;
|
|
23
|
+
}
|
|
24
|
+
/** Pre-merge semantic-green gate for merge cascades (#2385). */
|
|
25
|
+
export declare function evaluateSemanticGreen(prNumber: number, repo: string, options?: SemanticGreenOptions): SemanticGreenResult;
|
|
26
|
+
export type SemanticGreenFn = typeof evaluateSemanticGreen;
|
|
27
|
+
//# sourceMappingURL=semantic-green.d.ts.map
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { buildCiSummaryLine, evaluateCiGate } from "../pr-merge-readiness/ci-gate.js";
|
|
2
|
+
import { defaultRunGh, fetchCheckRunsRest } from "../pr-merge-readiness/gh.js";
|
|
3
|
+
import { EXIT_CONFIG_ERROR, EXIT_TIMEOUT_OR_ESCALATION } from "./constants.js";
|
|
4
|
+
function emptyPayload() {
|
|
5
|
+
return {
|
|
6
|
+
pr_base_sha: null,
|
|
7
|
+
target_branch: null,
|
|
8
|
+
target_head_sha: null,
|
|
9
|
+
master_ci: {},
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function fetchPrBase(prNumber, repo, runGh) {
|
|
13
|
+
const rc = runGh(["gh", "api", `repos/${repo}/pulls/${prNumber}`]);
|
|
14
|
+
if (rc.returncode !== 0) {
|
|
15
|
+
return {
|
|
16
|
+
baseRef: null,
|
|
17
|
+
baseSha: null,
|
|
18
|
+
error: `gh api /pulls/${prNumber} failed: ${rc.stderr.trim()}`,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (!rc.stdout.trim()) {
|
|
22
|
+
return { baseRef: null, baseSha: null, error: "empty body from gh api /pulls/<N>" };
|
|
23
|
+
}
|
|
24
|
+
let payload;
|
|
25
|
+
try {
|
|
26
|
+
payload = JSON.parse(rc.stdout);
|
|
27
|
+
}
|
|
28
|
+
catch (exc) {
|
|
29
|
+
const message = exc instanceof Error ? exc.message : String(exc);
|
|
30
|
+
return { baseRef: null, baseSha: null, error: `could not parse PR JSON: ${message}` };
|
|
31
|
+
}
|
|
32
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
|
|
33
|
+
return { baseRef: null, baseSha: null, error: "unexpected PR JSON shape (not a dict)" };
|
|
34
|
+
}
|
|
35
|
+
const base = payload.base;
|
|
36
|
+
if (base === null || typeof base !== "object" || Array.isArray(base)) {
|
|
37
|
+
return { baseRef: null, baseSha: null, error: "PR JSON missing base object" };
|
|
38
|
+
}
|
|
39
|
+
const baseRecord = base;
|
|
40
|
+
const baseRef = typeof baseRecord.ref === "string" && baseRecord.ref.length > 0 ? baseRecord.ref : null;
|
|
41
|
+
const baseSha = typeof baseRecord.sha === "string" && baseRecord.sha.length > 0 ? baseRecord.sha : null;
|
|
42
|
+
if (baseRef === null || baseSha === null) {
|
|
43
|
+
return { baseRef, baseSha, error: "PR JSON missing base.ref or base.sha" };
|
|
44
|
+
}
|
|
45
|
+
return { baseRef, baseSha, error: null };
|
|
46
|
+
}
|
|
47
|
+
function fetchBranchHeadSha(repo, branch, runGh) {
|
|
48
|
+
const encoded = branch.split("/").map(encodeURIComponent).join("/");
|
|
49
|
+
const rc = runGh(["gh", "api", `repos/${repo}/git/ref/heads/${encoded}`]);
|
|
50
|
+
if (rc.returncode !== 0) {
|
|
51
|
+
return {
|
|
52
|
+
sha: null,
|
|
53
|
+
error: `gh api /git/ref/heads/${branch} failed: ${rc.stderr.trim()}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (!rc.stdout.trim()) {
|
|
57
|
+
return { sha: null, error: `empty body from gh api /git/ref/heads/${branch}` };
|
|
58
|
+
}
|
|
59
|
+
let payload;
|
|
60
|
+
try {
|
|
61
|
+
payload = JSON.parse(rc.stdout);
|
|
62
|
+
}
|
|
63
|
+
catch (exc) {
|
|
64
|
+
const message = exc instanceof Error ? exc.message : String(exc);
|
|
65
|
+
return { sha: null, error: `could not parse branch ref JSON: ${message}` };
|
|
66
|
+
}
|
|
67
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
|
|
68
|
+
return { sha: null, error: "unexpected branch ref JSON shape (not a dict)" };
|
|
69
|
+
}
|
|
70
|
+
const object = payload.object;
|
|
71
|
+
if (object === null || typeof object !== "object" || Array.isArray(object)) {
|
|
72
|
+
return { sha: null, error: "branch ref JSON missing object.sha" };
|
|
73
|
+
}
|
|
74
|
+
const sha = object.sha;
|
|
75
|
+
if (typeof sha !== "string" || sha.length === 0) {
|
|
76
|
+
return { sha: null, error: "branch ref JSON missing object.sha" };
|
|
77
|
+
}
|
|
78
|
+
return { sha, error: null };
|
|
79
|
+
}
|
|
80
|
+
function evaluateTargetBranchCi(repo, headSha, runGh) {
|
|
81
|
+
const check = fetchCheckRunsRest(headSha, repo, runGh);
|
|
82
|
+
if (check.error.length > 0) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
error: "Required CI check-runs on target branch HEAD could not be fetched; " +
|
|
86
|
+
`fail closed (#2385). ${check.error}`,
|
|
87
|
+
masterCi: { ready_state: "blocked", error: check.error },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const ciResult = evaluateCiGate(check.checkRuns);
|
|
91
|
+
const masterCi = {
|
|
92
|
+
...ciResult.summary,
|
|
93
|
+
summary_line: buildCiSummaryLine(ciResult.summary),
|
|
94
|
+
};
|
|
95
|
+
if (ciResult.summary.ready_state !== "ready") {
|
|
96
|
+
const detail = ciResult.failures.length > 0
|
|
97
|
+
? ciResult.failures.join(" ")
|
|
98
|
+
: `target branch CI ready_state=${ciResult.summary.ready_state}`;
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
error: "Target branch CI is not green at current HEAD; block cascade merge until master is green (#2385). " +
|
|
102
|
+
detail,
|
|
103
|
+
masterCi,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return { ok: true, error: null, masterCi };
|
|
107
|
+
}
|
|
108
|
+
/** Pre-merge semantic-green gate for merge cascades (#2385). */
|
|
109
|
+
export function evaluateSemanticGreen(prNumber, repo, options = {}) {
|
|
110
|
+
if (options.cascadeMode !== true) {
|
|
111
|
+
return { ok: true, outcome: null, exitCode: null, error: null, payload: emptyPayload() };
|
|
112
|
+
}
|
|
113
|
+
const runGh = options.runGh ?? defaultRunGh;
|
|
114
|
+
const prBase = fetchPrBase(prNumber, repo, runGh);
|
|
115
|
+
if (prBase.error !== null) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
outcome: "config-error",
|
|
119
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
120
|
+
error: prBase.error,
|
|
121
|
+
payload: {
|
|
122
|
+
...emptyPayload(),
|
|
123
|
+
pr_base_sha: prBase.baseSha,
|
|
124
|
+
target_branch: prBase.baseRef,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const targetBranch = options.baseBranch ?? prBase.baseRef;
|
|
129
|
+
if (targetBranch === null || targetBranch.length === 0) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
outcome: "config-error",
|
|
133
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
134
|
+
error: "Could not resolve target branch for semantic-green gate (--base-branch or PR base.ref).",
|
|
135
|
+
payload: {
|
|
136
|
+
pr_base_sha: prBase.baseSha,
|
|
137
|
+
target_branch: null,
|
|
138
|
+
target_head_sha: null,
|
|
139
|
+
master_ci: {},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const branchHead = fetchBranchHeadSha(repo, targetBranch, runGh);
|
|
144
|
+
if (branchHead.error !== null) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
outcome: "config-error",
|
|
148
|
+
exitCode: EXIT_CONFIG_ERROR,
|
|
149
|
+
error: branchHead.error,
|
|
150
|
+
payload: {
|
|
151
|
+
pr_base_sha: prBase.baseSha,
|
|
152
|
+
target_branch: targetBranch,
|
|
153
|
+
target_head_sha: null,
|
|
154
|
+
master_ci: {},
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const payload = {
|
|
159
|
+
pr_base_sha: prBase.baseSha,
|
|
160
|
+
target_branch: targetBranch,
|
|
161
|
+
target_head_sha: branchHead.sha,
|
|
162
|
+
master_ci: {},
|
|
163
|
+
};
|
|
164
|
+
if (prBase.baseSha !== branchHead.sha) {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
outcome: "semantic-stale-base",
|
|
168
|
+
exitCode: EXIT_TIMEOUT_OR_ESCALATION,
|
|
169
|
+
error: "PR base SHA is behind the current target branch HEAD (merge-tree-clean but semantically stale). " +
|
|
170
|
+
`Rebase/update-branch onto ${targetBranch} at ${branchHead.sha} and wait for fresh green CI before cascade merge (#2385). ` +
|
|
171
|
+
`pr_base=${prBase.baseSha} target_head=${branchHead.sha}`,
|
|
172
|
+
payload,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (options.requireMasterCiGreen === true) {
|
|
176
|
+
const ci = evaluateTargetBranchCi(repo, branchHead.sha, runGh);
|
|
177
|
+
if (!ci.ok) {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
outcome: "master-ci-not-green",
|
|
181
|
+
exitCode: EXIT_TIMEOUT_OR_ESCALATION,
|
|
182
|
+
error: ci.error,
|
|
183
|
+
payload: {
|
|
184
|
+
...payload,
|
|
185
|
+
master_ci: ci.masterCi,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
outcome: null,
|
|
192
|
+
exitCode: null,
|
|
193
|
+
error: null,
|
|
194
|
+
payload: {
|
|
195
|
+
...payload,
|
|
196
|
+
master_ci: ci.masterCi,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return { ok: true, outcome: null, exitCode: null, error: null, payload };
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=semantic-green.js.map
|
|
@@ -5,6 +5,7 @@ export interface WaitMergeableResult {
|
|
|
5
5
|
readonly exitCode: number;
|
|
6
6
|
readonly monitorResult: Record<string, unknown>;
|
|
7
7
|
readonly protectedCheck: Record<string, unknown>;
|
|
8
|
+
readonly semanticGreen: Record<string, unknown>;
|
|
8
9
|
readonly mergeStdout: string;
|
|
9
10
|
readonly mergeStderr: string;
|
|
10
11
|
readonly error: string | null;
|
|
@@ -39,10 +39,10 @@ export declare const DEFAULT_POLL_SECONDS = 90;
|
|
|
39
39
|
*/
|
|
40
40
|
export declare const WATCH_HELP: string;
|
|
41
41
|
/**
|
|
42
|
-
* Consecutive polls
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
42
|
+
* Consecutive polls where the CLEAN gate is wedged on HEAD (!has_blocking &&
|
|
43
|
+
* !is_clean with a holdout other than sha_match) before STALL (#1039). Stale-SHA
|
|
44
|
+
* reads (sha_match holdout) do NOT advance this counter — re-review in flight
|
|
45
|
+
* waits until max-wait cap (#2313 / #1259 INCOMPLETE_BUT_RATED).
|
|
46
46
|
*/
|
|
47
47
|
export declare const DEFAULT_STALL_THRESHOLD = 3;
|
|
48
48
|
/**
|
|
@@ -61,10 +61,10 @@ export const WATCH_HELP = "usage: task pr:watch -- <pr_number> [options]\n" +
|
|
|
61
61
|
" 1 NEW_P0_P1 Blocking findings on the current (SHA-matched) review\n" +
|
|
62
62
|
" 2 ERRORED | STALL | TIMEOUT | CI_BLOCKED | RUNNER_CAPACITY_STALL | config / usage error\n";
|
|
63
63
|
/**
|
|
64
|
-
* Consecutive polls
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
64
|
+
* Consecutive polls where the CLEAN gate is wedged on HEAD (!has_blocking &&
|
|
65
|
+
* !is_clean with a holdout other than sha_match) before STALL (#1039). Stale-SHA
|
|
66
|
+
* reads (sha_match holdout) do NOT advance this counter — re-review in flight
|
|
67
|
+
* waits until max-wait cap (#2313 / #1259 INCOMPLETE_BUT_RATED).
|
|
68
68
|
*/
|
|
69
69
|
export const DEFAULT_STALL_THRESHOLD = 3;
|
|
70
70
|
/**
|
package/dist/pr-watch/watch.js
CHANGED
|
@@ -99,9 +99,11 @@ export function watch(prNumber, repo, options = {}) {
|
|
|
99
99
|
if (!oneShot && ciBlockedStreak >= DEFAULT_CI_BLOCKED_THRESHOLD) {
|
|
100
100
|
return build(VERDICT_CI_BLOCKED, EXIT_TERMINAL_ERROR, probe, poll);
|
|
101
101
|
}
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
102
|
+
// STALL (#1039): wedged CLEAN-gate on HEAD — !has_blocking && !is_clean for N
|
|
103
|
+
// consecutive polls with a holdout OTHER than sha_match. Stale-SHA reads
|
|
104
|
+
// (clean_gate_holdout=sha_match) are INCOMPLETE for HEAD per #1259 / #2313 —
|
|
105
|
+
// keep polling until cap, not early STALL while re-review is in flight.
|
|
106
|
+
if (!probe.hasBlocking && !probe.isClean && probe.cleanGateHoldout !== "sha_match") {
|
|
105
107
|
stallStreak += 1;
|
|
106
108
|
}
|
|
107
109
|
else {
|
|
@@ -6,7 +6,7 @@ import { spawnSync } from "node:child_process";
|
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { isTerminalLifecyclePath,
|
|
9
|
+
import { buildLifecycleReport, isTerminalLifecyclePath, scanLifecycleAnchors, } from "../intake/reconcile-issues.js";
|
|
10
10
|
import { LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, resolveLifecycleFolder, resolveLifecycleRoot, } from "../layout/resolve.js";
|
|
11
11
|
import { renderRoadmap } from "../render/roadmap-render.js";
|
|
12
12
|
import { fetchIssueStatesForRelease } from "./issue-state-fetch.js";
|
|
@@ -35,15 +35,22 @@ export function checkVbriefLifecycleSyncNative(projectRoot, repo) {
|
|
|
35
35
|
vbriefDir = join(projectRoot, MIGRATED_ARTIFACT_DIR);
|
|
36
36
|
}
|
|
37
37
|
try {
|
|
38
|
-
const
|
|
39
|
-
const
|
|
38
|
+
const anchors = scanLifecycleAnchors(vbriefDir);
|
|
39
|
+
const anchorIssueNumbers = new Set();
|
|
40
|
+
for (const anchor of anchors) {
|
|
41
|
+
const num = anchor.issue_number;
|
|
42
|
+
if (num !== null) {
|
|
43
|
+
anchorIssueNumbers.add(num);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const fetchResult = fetchIssueStatesForRelease(repo, anchorIssueNumbers, {
|
|
40
47
|
cwd: projectRoot,
|
|
41
48
|
});
|
|
42
49
|
if (!fetchResult.ok) {
|
|
43
50
|
return [false, -1, fetchResult.reason];
|
|
44
51
|
}
|
|
45
52
|
const issueStateMap = fetchResult.states;
|
|
46
|
-
const report =
|
|
53
|
+
const report = buildLifecycleReport(anchors, issueStateMap, false);
|
|
47
54
|
const mismatches = [];
|
|
48
55
|
for (const entry of report.no_open_issue) {
|
|
49
56
|
const files = (entry.vbrief_files ?? []);
|
|
@@ -82,6 +82,9 @@ export const COMMANDS = {
|
|
|
82
82
|
"verify:wip-cap": spec("verify:wip-cap", "preflight_wip_cap:main", {
|
|
83
83
|
projectRootArg: "--project-root",
|
|
84
84
|
}),
|
|
85
|
+
"verify:orphan-active": spec("verify:orphan-active", "verify_orphan_active:main", {
|
|
86
|
+
projectRootArg: "--project-root",
|
|
87
|
+
}),
|
|
85
88
|
"verify:pack-drift": spec("verify:pack-drift", "pack_render:main", {
|
|
86
89
|
defaultArgs: ["--check"],
|
|
87
90
|
cwd: "framework",
|
|
@@ -102,6 +105,7 @@ export const COMMANDS = {
|
|
|
102
105
|
"verify:branch",
|
|
103
106
|
"verify:cache-fresh",
|
|
104
107
|
"verify:wip-cap",
|
|
108
|
+
"verify:orphan-active",
|
|
105
109
|
"vbrief:validate",
|
|
106
110
|
"verify-strategy-output",
|
|
107
111
|
]),
|
|
@@ -120,6 +124,7 @@ export const COMMANDS = {
|
|
|
120
124
|
"verify:cache-fresh",
|
|
121
125
|
"verify:pack-drift",
|
|
122
126
|
"verify:wip-cap",
|
|
127
|
+
"verify:orphan-active",
|
|
123
128
|
"vbrief:validate",
|
|
124
129
|
"verify-strategy-output",
|
|
125
130
|
]),
|
|
@@ -293,6 +298,7 @@ const ENTRYPOINT_VERB = {
|
|
|
293
298
|
"verify_no_task_runtime:main": "verify-no-task-runtime",
|
|
294
299
|
"preflight_cache:main": "preflight-cache",
|
|
295
300
|
"preflight_wip_cap:main": "verify-wip-cap",
|
|
301
|
+
"verify_orphan_active:main": "verify-orphan-active",
|
|
296
302
|
"pack_render:main": "pack-render",
|
|
297
303
|
"validate_strategy_output:main": "validate-strategy-output",
|
|
298
304
|
"vbrief_validate:main": "vbrief-validate",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { JsonObject } from "../vbrief-build/types.js";
|
|
2
|
+
export declare class BriefIOError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/** Canonical brief serialization (single source of truth with vbrief-build). */
|
|
6
|
+
export declare function formatBriefJson(data: unknown): string;
|
|
7
|
+
export type ReadBriefResult = {
|
|
8
|
+
readonly ok: true;
|
|
9
|
+
readonly data: JsonObject;
|
|
10
|
+
} | {
|
|
11
|
+
readonly ok: false;
|
|
12
|
+
readonly message: string;
|
|
13
|
+
};
|
|
14
|
+
/** Read a scope brief for in-memory mutation (parse only; no validation gate on read). */
|
|
15
|
+
export declare function readBriefForMutation(filePath: string): ReadBriefResult;
|
|
16
|
+
/** Fail-closed schema + folder/status validation before persist. */
|
|
17
|
+
export declare function validateBriefForPersist(filePath: string, data: JsonObject, vbriefRoot: string): string | null;
|
|
18
|
+
export type AtomicWriteBriefResult = {
|
|
19
|
+
readonly ok: true;
|
|
20
|
+
} | {
|
|
21
|
+
readonly ok: false;
|
|
22
|
+
readonly message: string;
|
|
23
|
+
};
|
|
24
|
+
/** Validate then atomically persist a brief (temp file + rename). */
|
|
25
|
+
export declare function atomicWriteBrief(filePath: string, data: JsonObject, vbriefRoot: string): AtomicWriteBriefResult;
|
|
26
|
+
//# sourceMappingURL=brief-io.d.ts.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { atomicWriteText } from "../cache/io.js";
|
|
4
|
+
import { hasArtifactSuffix } from "../layout/resolve.js";
|
|
5
|
+
import { pythonJsonPretty } from "../vbrief-build/json.js";
|
|
6
|
+
import { validateFolderStatus } from "../vbrief-validate/folder-status.js";
|
|
7
|
+
import { validateVbriefSchema } from "../vbrief-validate/schema.js";
|
|
8
|
+
export class BriefIOError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "BriefIOError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Canonical brief serialization (single source of truth with vbrief-build). */
|
|
15
|
+
export function formatBriefJson(data) {
|
|
16
|
+
return pythonJsonPretty(data);
|
|
17
|
+
}
|
|
18
|
+
/** Read a scope brief for in-memory mutation (parse only; no validation gate on read). */
|
|
19
|
+
export function readBriefForMutation(filePath) {
|
|
20
|
+
const resolvedPath = resolve(filePath);
|
|
21
|
+
if (!existsSync(resolvedPath)) {
|
|
22
|
+
return { ok: false, message: `File not found: ${resolvedPath}` };
|
|
23
|
+
}
|
|
24
|
+
const basename = resolvedPath.split(/[/\\]/).pop() ?? "";
|
|
25
|
+
if (!hasArtifactSuffix(basename)) {
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
message: `Not a vBRIEF file (expected .vbrief.json or .xbrief.json): ${basename}`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
let raw;
|
|
32
|
+
try {
|
|
33
|
+
raw = readFileSync(resolvedPath, "utf8");
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
37
|
+
return { ok: false, message: `Could not read ${resolvedPath}: ${msg}` };
|
|
38
|
+
}
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(raw);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
return { ok: false, message: `Invalid JSON in ${resolvedPath}: ${String(err)}` };
|
|
45
|
+
}
|
|
46
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
47
|
+
return { ok: false, message: `Top-level value in ${resolvedPath} is not a JSON object` };
|
|
48
|
+
}
|
|
49
|
+
return { ok: true, data: parsed };
|
|
50
|
+
}
|
|
51
|
+
/** Fail-closed schema + folder/status validation before persist. */
|
|
52
|
+
export function validateBriefForPersist(filePath, data, vbriefRoot) {
|
|
53
|
+
const errors = [
|
|
54
|
+
...validateVbriefSchema(data, filePath),
|
|
55
|
+
...validateFolderStatus(filePath, data, vbriefRoot),
|
|
56
|
+
];
|
|
57
|
+
if (errors.length === 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return errors.join("; ");
|
|
61
|
+
}
|
|
62
|
+
/** Validate then atomically persist a brief (temp file + rename). */
|
|
63
|
+
export function atomicWriteBrief(filePath, data, vbriefRoot) {
|
|
64
|
+
const validationError = validateBriefForPersist(filePath, data, vbriefRoot);
|
|
65
|
+
if (validationError !== null) {
|
|
66
|
+
return { ok: false, message: validationError };
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
atomicWriteText(filePath, formatBriefJson(data));
|
|
70
|
+
return { ok: true };
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
74
|
+
return { ok: false, message: `Failed to write ${filePath}: ${msg}` };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=brief-io.js.map
|
package/dist/scope/decompose.js
CHANGED
|
@@ -12,7 +12,7 @@ import { referenceTypeMatches } from "@deftai/directive-types";
|
|
|
12
12
|
import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, resolveLifecycleRoot, } from "../layout/resolve.js";
|
|
13
13
|
import { referenceWithDefaultTrust, slugify } from "../vbrief-build/build.js";
|
|
14
14
|
import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
|
|
15
|
-
import {
|
|
15
|
+
import { formatBriefJson } from "./vbrief-json.js";
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
// Constants
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
@@ -54,7 +54,7 @@ function loadJson(path) {
|
|
|
54
54
|
}
|
|
55
55
|
function writeJson(path, data) {
|
|
56
56
|
mkdirSync(dirname(path), { recursive: true });
|
|
57
|
-
writeFileSync(path,
|
|
57
|
+
writeFileSync(path, formatBriefJson(data), "utf8");
|
|
58
58
|
}
|
|
59
59
|
// ---------------------------------------------------------------------------
|
|
60
60
|
// Story quality helpers (ported from _vbrief_story_quality.py)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, relative, resolve } from "node:path";
|
|
3
3
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
4
|
-
import {
|
|
4
|
+
import { formatBriefJson } from "./vbrief-json.js";
|
|
5
5
|
import { collectChildUris, collectPlanRefs, resolveVbriefRef } from "./vbrief-ref.js";
|
|
6
6
|
function rewriteOnePlanRef(value, oldParentResolved, newParentRel, vbriefDir) {
|
|
7
7
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -53,7 +53,7 @@ function rewriteParentChildReference(parentPath, oldChildResolved, newChildRel,
|
|
|
53
53
|
}
|
|
54
54
|
if (changed) {
|
|
55
55
|
try {
|
|
56
|
-
writeFileSync(parentPath,
|
|
56
|
+
writeFileSync(parentPath, formatBriefJson(parentData), "utf8");
|
|
57
57
|
}
|
|
58
58
|
catch {
|
|
59
59
|
return false;
|
|
@@ -127,7 +127,7 @@ function rewriteChildParentReference(childPath, oldParentResolved, newParentRel,
|
|
|
127
127
|
}
|
|
128
128
|
if (changed) {
|
|
129
129
|
try {
|
|
130
|
-
writeFileSync(childPath,
|
|
130
|
+
writeFileSync(childPath, formatBriefJson(childData), "utf8");
|
|
131
131
|
}
|
|
132
132
|
catch {
|
|
133
133
|
return false;
|
package/dist/scope/demote.js
CHANGED
|
@@ -5,7 +5,7 @@ import { hasArtifactSuffix, resolveLifecycleFolder } from "../layout/resolve.js"
|
|
|
5
5
|
import { stripTrailingPathSeparators } from "../text/redos-safe.js";
|
|
6
6
|
import { append, canonicalLogPath, latestForPath, newDecisionId } from "./audit-log.js";
|
|
7
7
|
import { resolveProjectRoot } from "./project-context.js";
|
|
8
|
-
import {
|
|
8
|
+
import { formatBriefJson, utcNowIso } from "./vbrief-json.js";
|
|
9
9
|
import { canonicalRelpath } from "./vbrief-ref.js";
|
|
10
10
|
export const DEFAULT_OLDER_THAN_DAYS = 45;
|
|
11
11
|
export const SOURCE_FOLDER = "pending";
|
|
@@ -85,7 +85,7 @@ export function demoteOne(filePath, projectRoot, reason, options = {}) {
|
|
|
85
85
|
const timestamp = utcNowIso(now);
|
|
86
86
|
planObj.status = TARGET_STATUS;
|
|
87
87
|
planObj.updated = timestamp;
|
|
88
|
-
writeFileSync(resolved,
|
|
88
|
+
writeFileSync(resolved, formatBriefJson(data), "utf8");
|
|
89
89
|
const vbriefRoot = dirname(dirname(resolved));
|
|
90
90
|
const targetDir = join(vbriefRoot, TARGET_FOLDER);
|
|
91
91
|
mkdirSync(targetDir, { recursive: true });
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export declare function syncProjectDefinitionAfterScopeMove(scopeData: Record<string, unknown>, oldPath: string, newPath: string, vbriefRoot: string, targetStatus: string):
|
|
1
|
+
/** Fail-closed sync of PROJECT-DEFINITION after a lifecycle move (#1527 / #2131). */
|
|
2
|
+
export declare function syncProjectDefinitionAfterScopeMove(scopeData: Record<string, unknown>, oldPath: string, newPath: string, vbriefRoot: string, targetStatus: string): string | null;
|
|
3
3
|
//# sourceMappingURL=project-definition-sync.d.ts.map
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { dirname, resolve } from "node:path";
|
|
2
2
|
import { resolveProjectDefinitionPath } from "../layout/resolve.js";
|
|
3
|
+
import { atomicWriteProjectDefinition, loadProjectDefinitionForMutation, projectDefinitionMutationLock, } from "../vbrief-build/project-definition-io.js";
|
|
3
4
|
import { syncRegistryArtifactAfterScopeMove } from "./registry-artifact-sync.js";
|
|
4
|
-
/**
|
|
5
|
+
/** Fail-closed sync of PROJECT-DEFINITION after a lifecycle move (#1527 / #2131). */
|
|
5
6
|
export function syncProjectDefinitionAfterScopeMove(scopeData, oldPath, newPath, vbriefRoot, targetStatus) {
|
|
6
7
|
const projectRoot = dirname(resolve(vbriefRoot));
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
try {
|
|
9
|
+
projectDefinitionMutationLock(projectRoot, () => {
|
|
10
|
+
const projectDefPath = resolveProjectDefinitionPath(projectRoot);
|
|
11
|
+
syncRegistryArtifactAfterScopeMove(projectDefPath, scopeData, oldPath, newPath, vbriefRoot, targetStatus, {
|
|
12
|
+
loadForMutation: () => loadProjectDefinitionForMutation(projectRoot),
|
|
13
|
+
persist: (path, data) => atomicWriteProjectDefinition(path, data),
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
return err instanceof Error ? err.message : String(err);
|
|
20
|
+
}
|
|
9
21
|
}
|
|
10
22
|
//# sourceMappingURL=project-definition-sync.js.map
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import type { JsonObject } from "../vbrief-build/types.js";
|
|
2
|
+
export interface RegistryArtifactPersistHooks {
|
|
3
|
+
readonly loadForMutation?: () => [JsonObject, string];
|
|
4
|
+
readonly persist?: (path: string, data: JsonObject) => void;
|
|
5
|
+
}
|
|
1
6
|
/** Best-effort sync of a registry artifact (PROJECT-DEFINITION, specification, etc.) after a lifecycle move. */
|
|
2
|
-
export declare function syncRegistryArtifactAfterScopeMove(registryPath: string, scopeData: Record<string, unknown>, oldPath: string, newPath: string, vbriefRoot: string, targetStatus: string): void;
|
|
7
|
+
export declare function syncRegistryArtifactAfterScopeMove(registryPath: string, scopeData: Record<string, unknown>, oldPath: string, newPath: string, vbriefRoot: string, targetStatus: string, hooks?: RegistryArtifactPersistHooks): void;
|
|
3
8
|
//# sourceMappingURL=registry-artifact-sync.d.ts.map
|