@deftai/directive-core 0.81.0 → 0.83.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/hooks/cursor-hooks.d.ts +23 -0
- package/dist/hooks/cursor-hooks.js +95 -0
- package/dist/hooks/dispatcher.d.ts +8 -1
- package/dist/hooks/dispatcher.js +23 -5
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/init-deposit/agent-hooks.d.ts +4 -2
- package/dist/init-deposit/agent-hooks.js +137 -14
- package/dist/init-deposit/hygiene.js +1 -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/lifecycle/events.d.ts +1 -0
- package/dist/lifecycle/events.js +59 -9
- 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/policy/host-hooks.d.ts +21 -0
- package/dist/policy/host-hooks.js +96 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +15 -1
- 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/product-signal/consent.d.ts +20 -3
- package/dist/product-signal/consent.js +63 -6
- package/dist/product-signal/gates.d.ts +1 -1
- package/dist/product-signal/submit.d.ts +5 -1
- package/dist/product-signal/submit.js +42 -18
- 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/vbrief-validate/plan-hooks.d.ts +2 -0
- package/dist/vbrief-validate/plan-hooks.js +25 -0
- package/dist/verify-env/agent-hooks.d.ts +2 -1
- package/dist/verify-env/agent-hooks.js +3 -2
- 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
|
@@ -32,7 +32,11 @@ export declare function runProductSignalEnable(projectRoot: string | null, confi
|
|
|
32
32
|
exitCode: 0 | 1 | 2;
|
|
33
33
|
text: string;
|
|
34
34
|
};
|
|
35
|
-
export
|
|
35
|
+
export interface ProductSignalConsentRunOptions {
|
|
36
|
+
readonly action: "grant" | "revoke";
|
|
37
|
+
readonly projectRoot?: string | null;
|
|
38
|
+
}
|
|
39
|
+
export declare function runProductSignalConsent(options: ProductSignalConsentRunOptions): {
|
|
36
40
|
exitCode: 0 | 1;
|
|
37
41
|
text: string;
|
|
38
42
|
};
|
|
@@ -3,7 +3,7 @@ import { join, resolve } from "node:path";
|
|
|
3
3
|
import { enableProductSignal, formatProductSignalStatusLine, resolveProductSignal, } from "../policy/product-signal.js";
|
|
4
4
|
import { resolveProjectRoot } from "../scope/project-context.js";
|
|
5
5
|
import { resolveActorName } from "./actor-name.js";
|
|
6
|
-
import {
|
|
6
|
+
import { authorizeProductSignalSink, grantProductSignalConsent, readProductSignalConsent, revokeProductSignalConsent, } from "./consent.js";
|
|
7
7
|
import { evaluateProductSignalGates } from "./gates.js";
|
|
8
8
|
import { GitHubPrivateSinkAdapter } from "./github-private-sink-adapter.js";
|
|
9
9
|
import { collectInstallContext } from "./install-context.js";
|
|
@@ -105,15 +105,25 @@ export async function submitProductSignal(options) {
|
|
|
105
105
|
payload,
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
|
+
const policy = resolveProductSignal(root);
|
|
109
|
+
const consent = readProductSignalConsent();
|
|
110
|
+
const sinkAuth = authorizeProductSignalSink(policy.sinkRepo, consent);
|
|
111
|
+
if (!sinkAuth.authorized) {
|
|
112
|
+
return {
|
|
113
|
+
outcome: "sink-unconsented",
|
|
114
|
+
exitCode: 0,
|
|
115
|
+
message: `${sinkAuth.message}\n`,
|
|
116
|
+
payload,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
108
119
|
if (options.dryRun) {
|
|
109
120
|
return {
|
|
110
121
|
outcome: "dry-run",
|
|
111
122
|
exitCode: 0,
|
|
112
|
-
message: `[dry-run] payload valid for ${payload.surface}\n`,
|
|
123
|
+
message: `[dry-run] payload valid for ${payload.surface} (sink=${sinkAuth.configuredSink})\n`,
|
|
113
124
|
payload,
|
|
114
125
|
};
|
|
115
126
|
}
|
|
116
|
-
const policy = resolveProductSignal(root);
|
|
117
127
|
const adapter = new GitHubPrivateSinkAdapter({ sinkRepo: policy.sinkRepo });
|
|
118
128
|
const result = await adapter.submit(payload, { gapText: options.gapText });
|
|
119
129
|
if (result.outcome === "submitted") {
|
|
@@ -131,11 +141,13 @@ export async function submitProductSignal(options) {
|
|
|
131
141
|
export function runProductSignalStatus(projectRoot) {
|
|
132
142
|
const root = resolveProjectRoot(projectRoot ?? undefined) ?? process.cwd();
|
|
133
143
|
const policy = resolveProductSignal(root);
|
|
134
|
-
const
|
|
144
|
+
const consent = readProductSignalConsent();
|
|
145
|
+
const configuredSink = policy.sinkRepo;
|
|
146
|
+
const sinkAuth = authorizeProductSignalSink(configuredSink, consent);
|
|
135
147
|
const last = readLastSubmitSummary(root);
|
|
136
148
|
const lines = [
|
|
137
149
|
formatProductSignalStatusLine(policy),
|
|
138
|
-
`[deft product-signal] consented=${String(
|
|
150
|
+
`[deft product-signal] consented=${String(consent !== null)} configuredSink=${configuredSink} consentedSink=${sinkAuth.consentedSink ?? "none"} sinksMatch=${String(sinkAuth.sinksMatch)}`,
|
|
139
151
|
last ? `[deft product-signal] ${last}` : "[deft product-signal] last submit: none",
|
|
140
152
|
];
|
|
141
153
|
return { exitCode: 0, text: `${lines.join("\n")}\n` };
|
|
@@ -148,12 +160,15 @@ export function runProductSignalEnable(projectRoot, confirm) {
|
|
|
148
160
|
const result = enableProductSignal(root, { confirm });
|
|
149
161
|
return { exitCode: result.exitCode, text: result.stdout };
|
|
150
162
|
}
|
|
151
|
-
export function runProductSignalConsent(
|
|
152
|
-
if (action === "grant") {
|
|
153
|
-
const
|
|
163
|
+
export function runProductSignalConsent(options) {
|
|
164
|
+
if (options.action === "grant") {
|
|
165
|
+
const root = resolveProjectRoot(options.projectRoot ?? undefined);
|
|
166
|
+
const sinkRepo = root !== null ? resolveProductSignal(root).sinkRepo : undefined;
|
|
167
|
+
const record = grantProductSignalConsent({ sinkRepo });
|
|
154
168
|
return {
|
|
155
169
|
exitCode: 0,
|
|
156
|
-
text: `product-signal consent granted (tier=${record.tier}, version=${record.consentVersion}
|
|
170
|
+
text: `product-signal consent granted (tier=${record.tier}, version=${record.consentVersion}, ` +
|
|
171
|
+
`sinkRepo=${record.sinkRepo ?? "unknown"}).\n`,
|
|
157
172
|
};
|
|
158
173
|
}
|
|
159
174
|
const ok = revokeProductSignalConsent();
|
|
@@ -216,24 +231,29 @@ export function parseProductSignalSubmitArgs(argv) {
|
|
|
216
231
|
}
|
|
217
232
|
return { surface, dryRun, json, projectRoot, nps };
|
|
218
233
|
}
|
|
234
|
+
function parseOptionalProjectRootArg(argv, fallback) {
|
|
235
|
+
const rootIdx = argv.indexOf("--project-root");
|
|
236
|
+
if (rootIdx >= 0) {
|
|
237
|
+
return argv[rootIdx + 1] ?? fallback;
|
|
238
|
+
}
|
|
239
|
+
const eqArg = argv.find((a) => a.startsWith("--project-root="));
|
|
240
|
+
if (eqArg !== undefined) {
|
|
241
|
+
return eqArg.slice("--project-root=".length) || fallback;
|
|
242
|
+
}
|
|
243
|
+
return fallback;
|
|
244
|
+
}
|
|
219
245
|
/** CLI module entrypoint for dispatch (#2693). */
|
|
220
246
|
export async function productSignalMain(argv = process.argv.slice(2)) {
|
|
221
247
|
const sub = argv[0];
|
|
222
248
|
if (sub === "status") {
|
|
223
|
-
const
|
|
224
|
-
const root = rootIdx >= 0
|
|
225
|
-
? (argv[rootIdx + 1] ?? ".")
|
|
226
|
-
: (argv.find((a) => a.startsWith("--project-root="))?.split("=")[1] ?? ".");
|
|
249
|
+
const root = parseOptionalProjectRootArg(argv, ".") ?? ".";
|
|
227
250
|
const result = runProductSignalStatus(root);
|
|
228
251
|
process.stdout.write(result.text);
|
|
229
252
|
return result.exitCode;
|
|
230
253
|
}
|
|
231
254
|
if (sub === "enable") {
|
|
232
255
|
const confirm = argv.includes("--confirm");
|
|
233
|
-
const
|
|
234
|
-
const root = rootIdx >= 0
|
|
235
|
-
? (argv[rootIdx + 1] ?? ".")
|
|
236
|
-
: (argv.find((a) => a.startsWith("--project-root="))?.split("=")[1] ?? ".");
|
|
256
|
+
const root = parseOptionalProjectRootArg(argv, ".") ?? ".";
|
|
237
257
|
const result = runProductSignalEnable(root, confirm);
|
|
238
258
|
process.stdout.write(result.text);
|
|
239
259
|
return result.exitCode;
|
|
@@ -245,7 +265,11 @@ export async function productSignalMain(argv = process.argv.slice(2)) {
|
|
|
245
265
|
process.stderr.write("usage: product-signal consent -- --grant|--revoke\n");
|
|
246
266
|
return 1;
|
|
247
267
|
}
|
|
248
|
-
const
|
|
268
|
+
const root = parseOptionalProjectRootArg(argv, null);
|
|
269
|
+
const result = runProductSignalConsent({
|
|
270
|
+
action: grant ? "grant" : "revoke",
|
|
271
|
+
projectRoot: root,
|
|
272
|
+
});
|
|
249
273
|
process.stdout.write(result.text);
|
|
250
274
|
return result.exitCode;
|
|
251
275
|
}
|
|
@@ -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
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
4
|
-
import {
|
|
4
|
+
import { atomicWriteText } from "../cache/io.js";
|
|
5
|
+
import { formatBriefJson } from "./brief-io.js";
|
|
5
6
|
import { relativeToVbrief, resolveVbriefRef, scopeIdsForFilename } from "./vbrief-ref.js";
|
|
6
7
|
function rewriteRegistryPlanReference(ref, oldResolved, newRel, vbriefRoot) {
|
|
7
8
|
if (typeof ref !== "object" || ref === null || Array.isArray(ref)) {
|
|
@@ -88,20 +89,30 @@ function registryItemMatchesScope(item, scopeData, oldPath, newPath, vbriefRoot)
|
|
|
88
89
|
return (typeof scopeTitle === "string" && typeof itemTitle === "string" && itemTitle === scopeTitle);
|
|
89
90
|
}
|
|
90
91
|
/** Best-effort sync of a registry artifact (PROJECT-DEFINITION, specification, etc.) after a lifecycle move. */
|
|
91
|
-
export function syncRegistryArtifactAfterScopeMove(registryPath, scopeData, oldPath, newPath, vbriefRoot, targetStatus) {
|
|
92
|
+
export function syncRegistryArtifactAfterScopeMove(registryPath, scopeData, oldPath, newPath, vbriefRoot, targetStatus, hooks = {}) {
|
|
92
93
|
const newRel = relativeToVbrief(newPath, vbriefRoot);
|
|
93
94
|
if (newRel === null) {
|
|
94
95
|
return;
|
|
95
96
|
}
|
|
96
|
-
if (!existsSync(registryPath)) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
97
|
try {
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
98
|
+
let registry;
|
|
99
|
+
if (hooks.loadForMutation !== undefined) {
|
|
100
|
+
if (!existsSync(registryPath)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const [loaded] = hooks.loadForMutation();
|
|
104
|
+
registry = loaded;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
if (!existsSync(registryPath)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const parsed = JSON.parse(readFileSync(registryPath, "utf8"));
|
|
111
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
registry = parsed;
|
|
103
115
|
}
|
|
104
|
-
const registry = parsed;
|
|
105
116
|
const plan = registry.plan;
|
|
106
117
|
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
107
118
|
return;
|
|
@@ -149,11 +160,18 @@ export function syncRegistryArtifactAfterScopeMove(registryPath, scopeData, oldP
|
|
|
149
160
|
}
|
|
150
161
|
}
|
|
151
162
|
if (changed) {
|
|
152
|
-
|
|
163
|
+
if (hooks.persist !== undefined) {
|
|
164
|
+
hooks.persist(registryPath, registry);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
atomicWriteText(registryPath, formatBriefJson(registry));
|
|
168
|
+
}
|
|
153
169
|
}
|
|
154
170
|
}
|
|
155
|
-
catch {
|
|
156
|
-
|
|
171
|
+
catch (err) {
|
|
172
|
+
if (hooks.loadForMutation !== undefined || hooks.persist !== undefined) {
|
|
173
|
+
throw err;
|
|
174
|
+
}
|
|
157
175
|
}
|
|
158
176
|
}
|
|
159
177
|
//# sourceMappingURL=registry-artifact-sync.js.map
|
package/dist/scope/transition.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { existsSync, mkdirSync,
|
|
1
|
+
import { existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { InstrumentedVbriefCrud, persistCrudMetrics } from "../eval/crud-telemetry.js";
|
|
4
4
|
import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
|
|
5
5
|
import { hasArtifactSuffix } from "../layout/resolve.js";
|
|
6
6
|
import { append, canonicalLogPath, newDecisionId } from "./audit-log.js";
|
|
7
|
+
import { atomicWriteBrief, formatBriefJson, readBriefForMutation } from "./brief-io.js";
|
|
7
8
|
import { stampCompletionMetadata } from "./capacity-stamp.js";
|
|
8
9
|
import { LIFECYCLE_FOLDERS, MOVE_LABELS, STATUS_PRECONDITIONS, STAY_LABELS, TRANSITIONS, } from "./constants.js";
|
|
9
10
|
import { detectLifecycleFolder, updateDecomposedChildBackReferences, updateDecomposedParentBackReferences, } from "./decomposed-refs.js";
|
|
10
11
|
import { syncProjectDefinitionAfterScopeMove } from "./project-definition-sync.js";
|
|
11
12
|
import { syncSpecificationAfterScopeMove } from "./specification-sync.js";
|
|
12
|
-
import {
|
|
13
|
+
import { utcNowIso } from "./vbrief-json.js";
|
|
13
14
|
export function runTransition(action, filePath, now = new Date()) {
|
|
14
15
|
if (!(action in TRANSITIONS)) {
|
|
15
16
|
const valid = Object.keys(TRANSITIONS).sort().join(", ");
|
|
@@ -43,13 +44,11 @@ export function runTransition(action, filePath, now = new Date()) {
|
|
|
43
44
|
`File is in ${currentFolder}/.`,
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
catch (err) {
|
|
51
|
-
return { ok: false, message: `Invalid JSON in ${resolvedPath}: ${String(err)}` };
|
|
47
|
+
const readResult = readBriefForMutation(resolvedPath);
|
|
48
|
+
if (!readResult.ok) {
|
|
49
|
+
return { ok: false, message: readResult.message };
|
|
52
50
|
}
|
|
51
|
+
const data = readResult.data;
|
|
53
52
|
const plan = data.plan;
|
|
54
53
|
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) {
|
|
55
54
|
return { ok: false, message: `Missing or invalid 'plan' object in ${resolvedPath}` };
|
|
@@ -100,7 +99,7 @@ export function runTransition(action, filePath, now = new Date()) {
|
|
|
100
99
|
if (act === "complete") {
|
|
101
100
|
stampCompletionMetadata(planObj, projectRoot, nowIso);
|
|
102
101
|
}
|
|
103
|
-
const formatted =
|
|
102
|
+
const formatted = formatBriefJson(data);
|
|
104
103
|
const crud = new InstrumentedVbriefCrud({ now: () => now });
|
|
105
104
|
if (targetFolder !== null) {
|
|
106
105
|
const destDir = join(vbriefRoot, targetFolder);
|
|
@@ -111,10 +110,11 @@ export function runTransition(action, filePath, now = new Date()) {
|
|
|
111
110
|
}
|
|
112
111
|
// #2578: stamp terminal status at the destination path in the same write as
|
|
113
112
|
// folder placement — never leave a non-terminal status under completed/.
|
|
114
|
-
const writeResult =
|
|
113
|
+
const writeResult = atomicWriteBrief(destPath, data, vbriefRoot);
|
|
115
114
|
if (!writeResult.ok) {
|
|
116
|
-
return { ok: false, message: writeResult.
|
|
115
|
+
return { ok: false, message: writeResult.message };
|
|
117
116
|
}
|
|
117
|
+
crud.recordTrustedUpdate(destPath, formatted);
|
|
118
118
|
try {
|
|
119
119
|
unlinkSync(resolvedPath);
|
|
120
120
|
}
|
|
@@ -135,18 +135,26 @@ export function runTransition(action, filePath, now = new Date()) {
|
|
|
135
135
|
}
|
|
136
136
|
updateDecomposedParentBackReferences(data, resolvedPath, destPath, vbriefRoot);
|
|
137
137
|
updateDecomposedChildBackReferences(data, resolvedPath, destPath, vbriefRoot);
|
|
138
|
-
syncProjectDefinitionAfterScopeMove(data, resolvedPath, destPath, vbriefRoot, targetStatus);
|
|
139
|
-
syncSpecificationAfterScopeMove(data, resolvedPath, destPath, vbriefRoot, targetStatus);
|
|
140
138
|
const actionLabel = MOVE_LABELS[act] ?? act.charAt(0).toUpperCase() + act.slice(1);
|
|
139
|
+
const pdSyncError = syncProjectDefinitionAfterScopeMove(data, resolvedPath, destPath, vbriefRoot, targetStatus);
|
|
140
|
+
if (pdSyncError !== null) {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
message: `${actionLabel} ${basename}: brief moved to ${targetFolder}/ but ` +
|
|
144
|
+
`PROJECT-DEFINITION sync failed: ${pdSyncError}`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
syncSpecificationAfterScopeMove(data, resolvedPath, destPath, vbriefRoot, targetStatus);
|
|
141
148
|
return {
|
|
142
149
|
ok: true,
|
|
143
150
|
message: `${actionLabel} ${basename}: ${currentFolder}/ -> ${targetFolder}/ (status: ${targetStatus})`,
|
|
144
151
|
};
|
|
145
152
|
}
|
|
146
|
-
const writeResult =
|
|
153
|
+
const writeResult = atomicWriteBrief(resolvedPath, data, vbriefRoot);
|
|
147
154
|
if (!writeResult.ok) {
|
|
148
|
-
return { ok: false, message: writeResult.
|
|
155
|
+
return { ok: false, message: writeResult.message };
|
|
149
156
|
}
|
|
157
|
+
crud.recordTrustedUpdate(resolvedPath, formatted);
|
|
150
158
|
try {
|
|
151
159
|
persistCrudMetrics(projectRoot, crud.getMetrics());
|
|
152
160
|
}
|
package/dist/scope/undo.js
CHANGED
|
@@ -4,7 +4,7 @@ import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/project
|
|
|
4
4
|
import { resolveLifecycleRoot } from "../layout/resolve.js";
|
|
5
5
|
import { append, canonicalLogPath, newDecisionId, readAll } from "./audit-log.js";
|
|
6
6
|
import { REVERSIBLE_ACTIONS, TERMINAL_ACTIONS } from "./constants.js";
|
|
7
|
-
import {
|
|
7
|
+
import { formatBriefJson, utcNowIso } from "./vbrief-json.js";
|
|
8
8
|
function vbriefRoot(projectRoot) {
|
|
9
9
|
return resolveLifecycleRoot(projectRoot);
|
|
10
10
|
}
|
|
@@ -185,7 +185,7 @@ function moveAndFlip(srcFile, destFolder, newStatus, timestamp, projectRoot) {
|
|
|
185
185
|
const planObj = plan;
|
|
186
186
|
planObj.status = newStatus;
|
|
187
187
|
planObj.updated = timestamp;
|
|
188
|
-
writeFileSync(srcFile,
|
|
188
|
+
writeFileSync(srcFile, formatBriefJson(data), "utf8");
|
|
189
189
|
mkdirSync(destFolder, { recursive: true });
|
|
190
190
|
const destPath = join(destFolder, basename(srcFile));
|
|
191
191
|
renameSync(srcFile, destPath);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function formatVbriefJson(data: unknown): string;
|
|
1
|
+
export { formatBriefJson } from "./brief-io.js";
|
|
3
2
|
export declare function utcNowIso(now?: Date): string;
|
|
4
3
|
//# sourceMappingURL=vbrief-json.d.ts.map
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export function formatVbriefJson(data) {
|
|
3
|
-
return `${JSON.stringify(data, null, 2)}\n`;
|
|
4
|
-
}
|
|
1
|
+
export { formatBriefJson } from "./brief-io.js";
|
|
5
2
|
export function utcNowIso(now = new Date()) {
|
|
6
3
|
return now.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
7
4
|
}
|