@deftai/directive-core 0.87.0 → 0.89.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/authz/actions.d.ts +55 -0
- package/dist/authz/actions.js +125 -0
- package/dist/authz/classify.d.ts +23 -0
- package/dist/authz/classify.js +217 -0
- package/dist/authz/closed-verb.d.ts +42 -0
- package/dist/authz/closed-verb.js +279 -0
- package/dist/authz/evaluate.d.ts +41 -0
- package/dist/authz/evaluate.js +298 -0
- package/dist/authz/index.d.ts +15 -0
- package/dist/authz/index.js +15 -0
- package/dist/authz/origin.d.ts +28 -0
- package/dist/authz/origin.js +64 -0
- package/dist/authz/paths.d.ts +6 -0
- package/dist/authz/paths.js +19 -0
- package/dist/authz/store.d.ts +40 -0
- package/dist/authz/store.js +317 -0
- package/dist/authz/templates.d.ts +139 -0
- package/dist/authz/templates.js +241 -0
- package/dist/authz/types.d.ts +111 -0
- package/dist/authz/types.js +41 -0
- package/dist/authz/verb-classification.d.ts +44 -0
- package/dist/authz/verb-classification.js +237 -0
- package/dist/branch/evaluate.js +6 -1
- package/dist/cache/fetch.js +10 -5
- package/dist/cache/io.d.ts +9 -2
- package/dist/cache/io.js +30 -10
- package/dist/cache/scanner.d.ts +11 -1
- package/dist/cache/scanner.js +29 -4
- package/dist/cache/task-cache/store.js +11 -7
- package/dist/capacity/backfill.js +10 -4
- package/dist/category-b-namespace/index.js +10 -4
- package/dist/check/gate-lists.js +1 -0
- package/dist/codebase/default-extractor.js +3 -0
- package/dist/codebase/map.js +11 -4
- package/dist/content-contracts/skills/helpers.d.ts +10 -0
- package/dist/content-contracts/skills/helpers.js +35 -0
- package/dist/deposit/copy-tree.d.ts +19 -1
- package/dist/deposit/copy-tree.js +134 -5
- package/dist/doctor/doctor-state.js +28 -4
- package/dist/doctor/main.d.ts +10 -0
- package/dist/doctor/main.js +208 -0
- package/dist/doctor/types.d.ts +11 -0
- package/dist/escalation/actions.d.ts +63 -0
- package/dist/escalation/actions.js +137 -0
- package/dist/escalation/index.d.ts +8 -0
- package/dist/escalation/index.js +8 -0
- package/dist/escalation/paths.d.ts +3 -0
- package/dist/escalation/paths.js +10 -0
- package/dist/escalation/store.d.ts +17 -0
- package/dist/escalation/store.js +172 -0
- package/dist/escalation/types.d.ts +73 -0
- package/dist/escalation/types.js +43 -0
- package/dist/eval/crud-telemetry.js +30 -10
- package/dist/eval/health.js +22 -7
- package/dist/eval/readback.js +11 -10
- package/dist/eval/run.js +32 -16
- package/dist/events/attribution-enrichment.js +10 -4
- package/dist/finish-loop/directive-finish-loop.d.ts +37 -0
- package/dist/finish-loop/directive-finish-loop.js +239 -0
- package/dist/finish-loop/grant-gate.d.ts +31 -0
- package/dist/finish-loop/grant-gate.js +169 -0
- package/dist/finish-loop/index.d.ts +11 -0
- package/dist/finish-loop/index.js +11 -0
- package/dist/finish-loop/main.d.ts +35 -0
- package/dist/finish-loop/main.js +365 -0
- package/dist/finish-loop/pr-finish-loop.d.ts +34 -0
- package/dist/finish-loop/pr-finish-loop.js +217 -0
- package/dist/finish-loop/progress.d.ts +17 -0
- package/dist/finish-loop/progress.js +45 -0
- package/dist/finish-loop/queue.d.ts +16 -0
- package/dist/finish-loop/queue.js +65 -0
- package/dist/finish-loop/types.d.ts +52 -0
- package/dist/finish-loop/types.js +10 -0
- package/dist/fs/contained-write.d.ts +101 -0
- package/dist/fs/contained-write.js +281 -0
- package/dist/fs/projection-containment.d.ts +18 -0
- package/dist/fs/projection-containment.js +40 -0
- package/dist/hooks/classify/classify.d.ts +10 -0
- package/dist/hooks/classify/classify.js +37 -0
- package/dist/hooks/classify/index.d.ts +14 -0
- package/dist/hooks/classify/index.js +13 -0
- package/dist/hooks/classify/paths.d.ts +22 -0
- package/dist/hooks/classify/paths.js +72 -0
- package/dist/hooks/classify/payload.d.ts +16 -0
- package/dist/hooks/classify/payload.js +45 -0
- package/dist/hooks/classify/stdin.d.ts +12 -0
- package/dist/hooks/classify/stdin.js +63 -0
- package/dist/hooks/classify/tool-name.d.ts +18 -0
- package/dist/hooks/classify/tool-name.js +85 -0
- package/dist/hooks/classify/types.d.ts +41 -0
- package/dist/hooks/classify/types.js +7 -0
- package/dist/hooks/dispatcher.d.ts +30 -15
- package/dist/hooks/dispatcher.js +326 -134
- package/dist/hooks/fixtures/cases.d.ts +44 -0
- package/dist/hooks/fixtures/cases.js +595 -0
- package/dist/hooks/fixtures/index.d.ts +2 -0
- package/dist/hooks/fixtures/index.js +2 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/tools.d.ts +31 -0
- package/dist/hooks/tools.js +74 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/init-deposit/agent-hooks.d.ts +1 -1
- package/dist/init-deposit/agent-hooks.js +65 -10
- package/dist/init-deposit/gitignore.js +11 -3
- package/dist/init-deposit/headless-manifest.js +12 -4
- package/dist/init-deposit/hygiene.d.ts +16 -0
- package/dist/init-deposit/hygiene.js +26 -0
- package/dist/init-deposit/init-dispatch.js +28 -0
- package/dist/init-deposit/migrate.d.ts +1 -1
- package/dist/init-deposit/migrate.js +13 -3
- package/dist/init-deposit/prettierignore.js +12 -5
- package/dist/init-deposit/refresh.js +38 -7
- package/dist/init-deposit/scaffold.js +42 -29
- package/dist/init-deposit/xbrief-projections.js +17 -10
- package/dist/intake/candidates-log.js +46 -35
- package/dist/intake/github-body-cli.d.ts +6 -0
- package/dist/intake/github-body-cli.js +17 -0
- package/dist/intake/github-body.d.ts +46 -3
- package/dist/intake/github-body.js +191 -20
- package/dist/intake/issue-emit.d.ts +1 -0
- package/dist/intake/issue-emit.js +41 -23
- package/dist/intake/issue-ingest.js +11 -2
- package/dist/lifecycle/event-detect.js +12 -4
- package/dist/lifecycle/events.js +29 -20
- package/dist/lifecycle/lifecycle-hygiene.js +9 -3
- package/dist/packs/pack-render.d.ts +33 -0
- package/dist/packs/pack-render.js +172 -13
- package/dist/packs/quarantine-ext.d.ts +10 -0
- package/dist/packs/quarantine-ext.js +26 -2
- package/dist/plan-sequence/store.js +11 -4
- package/dist/policy/hotfix-criteria.d.ts +79 -0
- package/dist/policy/hotfix-criteria.js +197 -0
- package/dist/policy/index.d.ts +5 -0
- package/dist/policy/index.js +32 -1
- package/dist/policy/intent-ceiling.d.ts +79 -0
- package/dist/policy/intent-ceiling.js +181 -0
- package/dist/policy/no-deft-directive.d.ts +59 -0
- package/dist/policy/no-deft-directive.js +110 -0
- package/dist/policy/org-force-on-migration.d.ts +52 -0
- package/dist/policy/org-force-on-migration.js +269 -27
- package/dist/policy/require-human-merge.d.ts +75 -0
- package/dist/policy/require-human-merge.js +324 -0
- package/dist/policy/resolve.js +17 -6
- package/dist/policy/runtime-authority.d.ts +56 -2
- package/dist/policy/runtime-authority.js +297 -3
- package/dist/policy/write-fence.d.ts +78 -0
- package/dist/policy/write-fence.js +164 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +10 -0
- package/dist/pr-wait-mergeable/cascade.js +28 -0
- package/dist/preflight/evaluate.js +10 -0
- package/dist/product-signal/consent.js +21 -5
- package/dist/product-signal/submit.js +22 -12
- package/dist/release/build-dist-runner.js +5 -2
- package/dist/release/build-dist.d.ts +19 -1
- package/dist/release/build-dist.js +50 -2
- package/dist/release/native-steps.js +7 -2
- package/dist/release-publish/pipeline.d.ts +13 -0
- package/dist/release-publish/pipeline.js +46 -1
- package/dist/scope/audit-log.js +19 -24
- package/dist/scope/decompose.js +10 -5
- package/dist/scope/decomposed-refs.js +18 -3
- package/dist/scope/demote.js +9 -2
- package/dist/scope/undo.js +9 -2
- package/dist/session/release-availability.js +26 -6
- package/dist/session/ritual-sentinel.js +19 -12
- package/dist/session/session-start-hook.d.ts +3 -0
- package/dist/session/session-start-hook.js +15 -0
- package/dist/session/session-start.js +37 -0
- package/dist/staleness-tickler/state.js +26 -6
- package/dist/swarm/launch.js +13 -3
- package/dist/swarm/routing.js +9 -3
- package/dist/task-surface/index.js +24 -9
- package/dist/triage/bootstrap/gitignore.js +53 -10
- package/dist/triage/cache-path.js +10 -3
- package/dist/triage/welcome/summary.js +11 -4
- package/dist/triage/welcome/writers.js +45 -16
- package/dist/validate-content/validate-links.js +5 -0
- package/dist/value/readback.js +11 -6
- package/dist/vbrief-activate/activate.js +12 -4
- package/dist/vbrief-build/project-definition-io.js +14 -6
- package/dist/verify-source/contained-writes.d.ts +59 -0
- package/dist/verify-source/contained-writes.js +272 -0
- package/dist/verify-source/cursor-tier1.js +7 -2
- package/dist/verify-source/index.d.ts +1 -0
- package/dist/verify-source/index.js +1 -0
- package/dist/verify-source/openclaw-tier1.js +19 -1
- package/dist/verify-source/verify-stubs.js +3 -0
- package/dist/xbrief-migrate/migrate-project.js +26 -12
- package/dist/xbrief-migrate/transforms.d.ts +4 -2
- package/dist/xbrief-migrate/transforms.js +37 -14
- package/package.json +16 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
4
5
|
import { DEFAULT_PRODUCT_SIGNAL_SINK_REPO } from "../policy/product-signal.js";
|
|
5
6
|
import { platformUserConfigDir } from "../user-config/resolve-user-md.js";
|
|
6
7
|
export const PRODUCT_SIGNAL_CONSENT_FILENAME = "product-signal-consent.json";
|
|
@@ -142,8 +143,15 @@ export function grantProductSignalConsent(options = {}) {
|
|
|
142
143
|
sinkRepo,
|
|
143
144
|
};
|
|
144
145
|
const path = resolveProductSignalConsentPath(options);
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
// #2980 wave D: product write sink routes through containedWrite.
|
|
147
|
+
const dir = dirname(path);
|
|
148
|
+
mkdirSync(dir, { recursive: true });
|
|
149
|
+
containedWrite({
|
|
150
|
+
root: resolve(dir),
|
|
151
|
+
target: basename(path),
|
|
152
|
+
data: `${JSON.stringify(record, null, 2)}\n`,
|
|
153
|
+
mode: "replace",
|
|
154
|
+
});
|
|
147
155
|
return record;
|
|
148
156
|
}
|
|
149
157
|
/** Revoke consent by setting revokedAt (#2693 D2). */
|
|
@@ -167,7 +175,15 @@ export function revokeProductSignalConsent(options = {}) {
|
|
|
167
175
|
...existing,
|
|
168
176
|
revokedAt: now.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
169
177
|
};
|
|
170
|
-
|
|
178
|
+
// #2980 wave D: product write sink routes through containedWrite.
|
|
179
|
+
const dir = dirname(path);
|
|
180
|
+
mkdirSync(dir, { recursive: true });
|
|
181
|
+
containedWrite({
|
|
182
|
+
root: resolve(dir),
|
|
183
|
+
target: basename(path),
|
|
184
|
+
data: `${JSON.stringify(revoked, null, 2)}\n`,
|
|
185
|
+
mode: "replace",
|
|
186
|
+
});
|
|
171
187
|
return true;
|
|
172
188
|
}
|
|
173
189
|
//# sourceMappingURL=consent.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { ContainedWriteError, containedWrite } from "../fs/contained-write.js";
|
|
4
|
+
import { ProjectionContainmentError } from "../fs/projection-containment.js";
|
|
4
5
|
import { enableProductSignal, formatProductSignalStatusLine, resolveProductSignal, } from "../policy/product-signal.js";
|
|
5
6
|
import { resolveProjectRoot } from "../scope/project-context.js";
|
|
6
7
|
import { resolveActorName } from "./actor-name.js";
|
|
@@ -15,17 +16,26 @@ import { bootstrapProductSignalSink } from "./sink-bootstrap.js";
|
|
|
15
16
|
export const PRODUCT_SIGNAL_LAST_SUBMIT_REL = join(".deft-cache", "product-signal-last-submit.json");
|
|
16
17
|
function recordLastSubmit(projectRoot, outcome, url) {
|
|
17
18
|
const path = resolve(projectRoot, PRODUCT_SIGNAL_LAST_SUBMIT_REL);
|
|
18
|
-
assertWriteTargetSafe(projectRoot, path);
|
|
19
19
|
try {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
// #2980 wave D: product write sink routes through containedWrite.
|
|
21
|
+
const root = resolve(projectRoot);
|
|
22
|
+
containedWrite({
|
|
23
|
+
root,
|
|
24
|
+
target: path,
|
|
25
|
+
data: `${JSON.stringify({
|
|
26
|
+
outcome,
|
|
27
|
+
issueUrl: url,
|
|
28
|
+
at: new Date().toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
29
|
+
})}\n`,
|
|
30
|
+
mode: "append",
|
|
31
|
+
});
|
|
26
32
|
}
|
|
27
|
-
catch {
|
|
28
|
-
//
|
|
33
|
+
catch (err) {
|
|
34
|
+
// Containment refusals must fail closed (tests + product path) (#2807 / #2980).
|
|
35
|
+
if (err instanceof ContainedWriteError || err instanceof ProjectionContainmentError) {
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
// other observability I/O failures only
|
|
29
39
|
}
|
|
30
40
|
}
|
|
31
41
|
function readLastSubmitSummary(projectRoot) {
|
|
@@ -134,7 +144,7 @@ export async function submitProductSignal(options) {
|
|
|
134
144
|
recordLastSubmit(root, result.outcome, result.issueUrl ?? null);
|
|
135
145
|
}
|
|
136
146
|
catch (err) {
|
|
137
|
-
if (err instanceof ProjectionContainmentError) {
|
|
147
|
+
if (err instanceof ProjectionContainmentError || err instanceof ContainedWriteError) {
|
|
138
148
|
return {
|
|
139
149
|
outcome: "error-config",
|
|
140
150
|
exitCode: 2,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Subprocess entry for sync callers (release pipeline, task build).
|
|
3
3
|
* Top-level await keeps the process alive until the archive is written.
|
|
4
|
+
* Progress ticks go to stderr so pipeline callers can still capture the
|
|
5
|
+
* archive path on stdout (#2953).
|
|
4
6
|
*/
|
|
5
|
-
import { buildArchive, selectFormat } from "./build-dist.js";
|
|
7
|
+
import { buildArchive, emitBuildProgress, selectFormat } from "./build-dist.js";
|
|
6
8
|
const version = process.argv[2];
|
|
7
9
|
const root = process.argv[3];
|
|
8
10
|
if (!version || !root) {
|
|
@@ -11,7 +13,8 @@ if (!version || !root) {
|
|
|
11
13
|
}
|
|
12
14
|
const fmt = selectFormat(process.env.DEFT_BUILD_FORMAT);
|
|
13
15
|
try {
|
|
14
|
-
|
|
16
|
+
process.stderr.write(`build-dist-runner: start version=${version} format=${fmt}\n`);
|
|
17
|
+
const out = await buildArchive(root, version, fmt, { onProgress: emitBuildProgress });
|
|
15
18
|
process.stdout.write(`${out}\n`);
|
|
16
19
|
}
|
|
17
20
|
catch (err) {
|
|
@@ -8,7 +8,25 @@ export declare function iterSourceFiles(root: string, excludes?: ReadonlySet<str
|
|
|
8
8
|
}>;
|
|
9
9
|
export declare function selectFormat(arg: string | null | undefined): "tar" | "zip";
|
|
10
10
|
export declare function outputPath(root: string, version: string, fmt: "tar" | "zip"): string;
|
|
11
|
-
export
|
|
11
|
+
export type BuildProgress = {
|
|
12
|
+
readonly stage: string;
|
|
13
|
+
readonly detail?: string;
|
|
14
|
+
readonly current?: number;
|
|
15
|
+
readonly total?: number;
|
|
16
|
+
};
|
|
17
|
+
export type BuildArchiveOptions = {
|
|
18
|
+
readonly extraExcludes?: readonly string[];
|
|
19
|
+
/** Progress sink; default silent so unit tests stay quiet (#2953). */
|
|
20
|
+
readonly onProgress?: (p: BuildProgress) => void;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Emit human-readable stage progress for long release packaging runs (#2953).
|
|
24
|
+
* Writes to stderr so stdout stays the archive path for pipeline capture.
|
|
25
|
+
*/
|
|
26
|
+
export declare function emitBuildProgress(progress: BuildProgress, stream?: {
|
|
27
|
+
write: (chunk: string) => unknown;
|
|
28
|
+
}): void;
|
|
29
|
+
export declare function buildArchive(root: string, version: string, fmt: "tar" | "zip", options?: readonly string[] | BuildArchiveOptions): Promise<string>;
|
|
12
30
|
export declare function parseExtraExcludes(raw: string): string[];
|
|
13
31
|
export declare function main(argv: readonly string[]): Promise<number>;
|
|
14
32
|
//# sourceMappingURL=build-dist.d.ts.map
|
|
@@ -16,6 +16,9 @@ export const DEFAULT_EXCLUDES = new Set([
|
|
|
16
16
|
".ruff_cache",
|
|
17
17
|
".coverage",
|
|
18
18
|
"coverage",
|
|
19
|
+
// Scratch worktrees are never shippable framework content (#2953).
|
|
20
|
+
".deft-scratch",
|
|
21
|
+
"swarm-worktrees",
|
|
19
22
|
]);
|
|
20
23
|
export const DEFAULT_EXCLUDED_PATH_PREFIXES = [
|
|
21
24
|
"history/archive",
|
|
@@ -97,14 +100,46 @@ export function outputPath(root, version, fmt) {
|
|
|
97
100
|
const suffix = fmt === "zip" ? "zip" : "tar.gz";
|
|
98
101
|
return join(root, "dist", `deft-${version}.${suffix}`);
|
|
99
102
|
}
|
|
100
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Emit human-readable stage progress for long release packaging runs (#2953).
|
|
105
|
+
* Writes to stderr so stdout stays the archive path for pipeline capture.
|
|
106
|
+
*/
|
|
107
|
+
export function emitBuildProgress(progress, stream = process.stderr) {
|
|
108
|
+
const { stage, detail, current, total } = progress;
|
|
109
|
+
let line = `build-dist: ${stage}`;
|
|
110
|
+
if (typeof current === "number" && typeof total === "number" && total > 0) {
|
|
111
|
+
const pct = Math.min(100, Math.floor((current / total) * 100));
|
|
112
|
+
line += ` ${current}/${total} (${pct}%)`;
|
|
113
|
+
}
|
|
114
|
+
if (detail)
|
|
115
|
+
line += ` - ${detail}`;
|
|
116
|
+
stream.write(`${line}\n`);
|
|
117
|
+
}
|
|
118
|
+
function isExtraExcludesList(options) {
|
|
119
|
+
return Array.isArray(options);
|
|
120
|
+
}
|
|
121
|
+
function resolveBuildOptions(options = {}) {
|
|
122
|
+
// Backward-compatible: 4th arg may be a bare extra-excludes array (pre-#2953).
|
|
123
|
+
if (isExtraExcludesList(options)) {
|
|
124
|
+
return { extraExcludes: options, onProgress: () => { } };
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
extraExcludes: options.extraExcludes ?? [],
|
|
128
|
+
onProgress: options.onProgress ?? (() => { }),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export async function buildArchive(root, version, fmt, options = {}) {
|
|
132
|
+
const { extraExcludes, onProgress } = resolveBuildOptions(options);
|
|
101
133
|
const excludes = new Set([...DEFAULT_EXCLUDES, ...extraExcludes]);
|
|
102
134
|
const output = outputPath(root, version, fmt);
|
|
103
135
|
mkdirSync(dirname(output), { recursive: true });
|
|
104
136
|
if (existsSync(output)) {
|
|
105
137
|
unlinkSync(output);
|
|
106
138
|
}
|
|
139
|
+
onProgress({ stage: "scan", detail: "enumerating source files" });
|
|
107
140
|
const entries = iterSourceFiles(root, excludes);
|
|
141
|
+
const total = entries.length;
|
|
142
|
+
onProgress({ stage: "scan", detail: `found ${total} files`, current: total, total });
|
|
108
143
|
await new Promise((resolvePromise, reject) => {
|
|
109
144
|
const out = createWriteStream(output);
|
|
110
145
|
const archive = fmt === "zip"
|
|
@@ -114,11 +149,21 @@ export async function buildArchive(root, version, fmt, extraExcludes = []) {
|
|
|
114
149
|
out.on("error", (err) => reject(err));
|
|
115
150
|
archive.on("error", (err) => reject(err));
|
|
116
151
|
archive.pipe(out);
|
|
152
|
+
onProgress({ stage: "pack", detail: `format=${fmt}`, current: 0, total });
|
|
153
|
+
// Progress ticks every ~5% (or every 250 files) so long packs do not look hung.
|
|
154
|
+
const tickEvery = Math.max(1, Math.min(250, Math.ceil(total / 20)));
|
|
155
|
+
let i = 0;
|
|
117
156
|
for (const { absPath, archiveRel } of entries) {
|
|
118
157
|
archive.file(absPath, { name: `${ARCHIVE_ROOT}/${archiveRel}` });
|
|
158
|
+
i += 1;
|
|
159
|
+
if (i === total || i % tickEvery === 0) {
|
|
160
|
+
onProgress({ stage: "pack", current: i, total });
|
|
161
|
+
}
|
|
119
162
|
}
|
|
163
|
+
onProgress({ stage: "finalize", detail: output });
|
|
120
164
|
void archive.finalize();
|
|
121
165
|
});
|
|
166
|
+
onProgress({ stage: "done", detail: output, current: total, total });
|
|
122
167
|
return output;
|
|
123
168
|
}
|
|
124
169
|
export function parseExtraExcludes(raw) {
|
|
@@ -159,7 +204,10 @@ export async function main(argv) {
|
|
|
159
204
|
}
|
|
160
205
|
const fmt = selectFormat(fmtArg);
|
|
161
206
|
try {
|
|
162
|
-
const out = await buildArchive(projectRoot, version, fmt,
|
|
207
|
+
const out = await buildArchive(projectRoot, version, fmt, {
|
|
208
|
+
extraExcludes: parseExtraExcludes(extra),
|
|
209
|
+
onProgress: emitBuildProgress,
|
|
210
|
+
});
|
|
163
211
|
const printable = relative(projectRoot, out) || out;
|
|
164
212
|
process.stdout.write(`Created ${printable}\n`);
|
|
165
213
|
return 0;
|
|
@@ -79,16 +79,21 @@ export function runBuildNative(projectRoot, version, _seams = {}) {
|
|
|
79
79
|
return [false, "build requires a release version"];
|
|
80
80
|
}
|
|
81
81
|
const env = { ...process.env, DEFT_RELEASE_VERSION: version };
|
|
82
|
+
// Inherit stderr so build-dist-runner progress ticks stream live during
|
|
83
|
+
// release Step 8; capture stdout only for the archive path (#2953).
|
|
82
84
|
const result = spawnSync(process.execPath, [BUILD_DIST_RUNNER, version, projectRoot], {
|
|
83
85
|
cwd: projectRoot,
|
|
84
86
|
encoding: "utf8",
|
|
85
87
|
env,
|
|
88
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
86
89
|
});
|
|
87
90
|
if (result.status !== 0) {
|
|
88
|
-
const
|
|
91
|
+
const captured = (typeof result.stderr === "string" ? result.stderr : "") ||
|
|
92
|
+
(typeof result.stdout === "string" ? result.stdout : "");
|
|
93
|
+
const msg = captured.trim() || `exit ${result.status ?? 1}`;
|
|
89
94
|
return [false, `build failed: ${msg}`];
|
|
90
95
|
}
|
|
91
|
-
const out = (result.stdout
|
|
96
|
+
const out = (typeof result.stdout === "string" ? result.stdout : "").trim();
|
|
92
97
|
return [true, `build ran clean (DEFT_RELEASE_VERSION=${version}) -> ${out}`];
|
|
93
98
|
}
|
|
94
99
|
//# sourceMappingURL=native-steps.js.map
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
+
import { type EnvMap } from "../authz/closed-verb.js";
|
|
2
|
+
import type { ClosedVerbDecision, HumanOriginGrant } from "../authz/types.js";
|
|
1
3
|
import type { PublishConfig, ReleasePublishSeams } from "./types.js";
|
|
2
4
|
export declare function emit(label: string, status: string): void;
|
|
5
|
+
/**
|
|
6
|
+
* Fail-closed gate before draft→public (#1095 Wave 4).
|
|
7
|
+
* Allow only DEFT_ALLOW_RELEASE_PUBLISH=1 or a matching human-origin grant.
|
|
8
|
+
* Loads conventions/verb-classification.json from projectRoot when present.
|
|
9
|
+
* Test seam: pass grants / env via optional overrides on seams when present.
|
|
10
|
+
*/
|
|
11
|
+
export declare function evaluateReleasePublishGate(version: string, projectRoot: string, options?: {
|
|
12
|
+
readonly grants?: readonly HumanOriginGrant[];
|
|
13
|
+
readonly env?: EnvMap;
|
|
14
|
+
readonly repo?: string | null;
|
|
15
|
+
}): ClosedVerbDecision;
|
|
3
16
|
export declare function runPublish(config: PublishConfig, seams?: ReleasePublishSeams): number;
|
|
4
17
|
//# sourceMappingURL=pipeline.d.ts.map
|
|
@@ -1,10 +1,34 @@
|
|
|
1
|
+
import { closedVerbEnvBypassKey, evaluateClosedVerb } from "../authz/closed-verb.js";
|
|
2
|
+
import { listActiveHumanGrants, loadAuthzState, markGrantUsed } from "../authz/store.js";
|
|
3
|
+
import { loadVerbClassification } from "../authz/verb-classification.js";
|
|
1
4
|
import { EXIT_OK, EXIT_VIOLATION } from "../release/constants.js";
|
|
2
5
|
import { editReleasePublish, viewRelease } from "./gh-api.js";
|
|
3
6
|
export function emit(label, status) {
|
|
4
7
|
process.stderr.write(`[publish] ${label}... ${status}\n`);
|
|
5
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Fail-closed gate before draft→public (#1095 Wave 4).
|
|
11
|
+
* Allow only DEFT_ALLOW_RELEASE_PUBLISH=1 or a matching human-origin grant.
|
|
12
|
+
* Loads conventions/verb-classification.json from projectRoot when present.
|
|
13
|
+
* Test seam: pass grants / env via optional overrides on seams when present.
|
|
14
|
+
*/
|
|
15
|
+
export function evaluateReleasePublishGate(version, projectRoot, options = {}) {
|
|
16
|
+
// Production load of classification SoT (conventions/verb-classification.json).
|
|
17
|
+
const classification = loadVerbClassification(projectRoot);
|
|
18
|
+
const state = loadAuthzState(projectRoot);
|
|
19
|
+
const grants = options.grants ?? listActiveHumanGrants(projectRoot, state);
|
|
20
|
+
return evaluateClosedVerb({
|
|
21
|
+
verb: "release-publish",
|
|
22
|
+
target: version,
|
|
23
|
+
grants,
|
|
24
|
+
env: options.env ?? process.env,
|
|
25
|
+
projectRoot,
|
|
26
|
+
repo: options.repo ?? null,
|
|
27
|
+
classification,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
6
30
|
export function runPublish(config, seams = {}) {
|
|
7
|
-
const { version, repo, dryRun } = config;
|
|
31
|
+
const { version, repo, dryRun, projectRoot } = config;
|
|
8
32
|
const tag = `v${version}`;
|
|
9
33
|
const viewLabel = `View ${tag} on ${repo}`;
|
|
10
34
|
if (dryRun) {
|
|
@@ -26,6 +50,23 @@ export function runPublish(config, seams = {}) {
|
|
|
26
50
|
return EXIT_OK;
|
|
27
51
|
}
|
|
28
52
|
emit(viewLabel, `OK (draft found at ${payload?.url ?? "<no url>"})`);
|
|
53
|
+
// Closed-verb gate: refuse draft→public without grant or env bypass (#1095).
|
|
54
|
+
const gateSeams = seams;
|
|
55
|
+
const gate = evaluateReleasePublishGate(version, projectRoot, {
|
|
56
|
+
grants: gateSeams.closedVerbGrants,
|
|
57
|
+
env: gateSeams.closedVerbEnv,
|
|
58
|
+
repo,
|
|
59
|
+
});
|
|
60
|
+
if (!gate.allowed) {
|
|
61
|
+
const bypassKey = gate.envBypassKey ??
|
|
62
|
+
closedVerbEnvBypassKey("release-publish", projectRoot) ??
|
|
63
|
+
"DEFT_ALLOW_RELEASE_PUBLISH";
|
|
64
|
+
emit(`Closed-verb gate release-publish ${tag}`, `FAIL (${gate.code}: ${gate.reason})`);
|
|
65
|
+
process.stderr.write(`[publish] denied code=${gate.code} — set ${bypassKey}=1 or ` +
|
|
66
|
+
`mint \`deft authz:grant -- --template release-publish --target ${version}\`\n`);
|
|
67
|
+
return EXIT_VIOLATION;
|
|
68
|
+
}
|
|
69
|
+
emit(`Closed-verb gate release-publish ${tag}`, `OK (${gate.code}${gate.humanApprovalRef !== null ? ` grant=${gate.humanApprovalRef}` : ""})`);
|
|
29
70
|
const editLabel = `Edit ${tag} (--draft=false)`;
|
|
30
71
|
const [ok, editReason] = editReleasePublish(version, repo, payload?.id ?? undefined, seams);
|
|
31
72
|
if (!ok) {
|
|
@@ -40,6 +81,10 @@ export function runPublish(config, seams = {}) {
|
|
|
40
81
|
return EXIT_VIOLATION;
|
|
41
82
|
}
|
|
42
83
|
emit(verifyLabel, `OK (${tag} is now public)`);
|
|
84
|
+
// Consume single-use grant after successful draft→public (#1095).
|
|
85
|
+
if (gate.humanApprovalRef !== null && gate.code === "closed-verb-allow") {
|
|
86
|
+
markGrantUsed(projectRoot, gate.humanApprovalRef);
|
|
87
|
+
}
|
|
43
88
|
process.stderr.write(`Release ${tag} published successfully on ${repo}.\n`);
|
|
44
89
|
return EXIT_OK;
|
|
45
90
|
}
|
package/dist/scope/audit-log.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { dirname,
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
4
5
|
import { resolveTriageCachePath } from "../triage/cache-path.js";
|
|
5
6
|
import { utcNowIso } from "./vbrief-json.js";
|
|
6
7
|
export class ScopeAuditLogError extends Error {
|
|
@@ -28,6 +29,12 @@ const DEMOTE_META_REQUIRED = [
|
|
|
28
29
|
"demoted_from",
|
|
29
30
|
];
|
|
30
31
|
let threadLock = false;
|
|
32
|
+
/** Ensure parent exists and return it as the containment root for log writes. */
|
|
33
|
+
function containmentRootForLog(logPath) {
|
|
34
|
+
const parent = dirname(logPath);
|
|
35
|
+
mkdirSync(parent, { recursive: true });
|
|
36
|
+
return resolve(parent);
|
|
37
|
+
}
|
|
31
38
|
function replacerSortKeys(_key, value) {
|
|
32
39
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
33
40
|
const sorted = {};
|
|
@@ -97,34 +104,16 @@ export function canonicalLogPath(projectRoot) {
|
|
|
97
104
|
// Layout-aware (#1703): triage working-set under `.triage-cache/`.
|
|
98
105
|
return resolveTriageCachePath(resolve(projectRoot), "scope-lifecycle.jsonl");
|
|
99
106
|
}
|
|
100
|
-
function withAppendLock(
|
|
101
|
-
const lockPath = `${logPath}.lock`;
|
|
102
|
-
mkdirSync(dirname(logPath), { recursive: true });
|
|
107
|
+
function withAppendLock(fn) {
|
|
103
108
|
while (threadLock) {
|
|
104
109
|
/* spin-wait for in-process serialization */
|
|
105
110
|
}
|
|
106
111
|
threadLock = true;
|
|
107
|
-
let fd = null;
|
|
108
112
|
try {
|
|
109
|
-
fd = openSync(lockPath, "a+");
|
|
110
|
-
if (readFileSync(lockPath).length === 0) {
|
|
111
|
-
writeSync(fd, "\0");
|
|
112
|
-
}
|
|
113
113
|
fn();
|
|
114
114
|
}
|
|
115
115
|
finally {
|
|
116
116
|
threadLock = false;
|
|
117
|
-
if (fd !== null) {
|
|
118
|
-
closeSync(fd);
|
|
119
|
-
}
|
|
120
|
-
try {
|
|
121
|
-
if (existsSync(lockPath)) {
|
|
122
|
-
unlinkSync(lockPath);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
/* best-effort lock cleanup */
|
|
127
|
-
}
|
|
128
117
|
}
|
|
129
118
|
}
|
|
130
119
|
/** Validate and append an audit entry; returns decision_id. */
|
|
@@ -134,10 +123,16 @@ export function append(entry, logPath) {
|
|
|
134
123
|
}
|
|
135
124
|
validateEntry(entry);
|
|
136
125
|
const logFile = resolve(logPath);
|
|
137
|
-
|
|
126
|
+
const root = containmentRootForLog(logFile);
|
|
138
127
|
const line = JSON.stringify(entry, replacerSortKeys);
|
|
139
|
-
|
|
140
|
-
|
|
128
|
+
// #2980 wave B: product write sink routes through containedWrite.
|
|
129
|
+
withAppendLock(() => {
|
|
130
|
+
containedWrite({
|
|
131
|
+
root,
|
|
132
|
+
target: logFile,
|
|
133
|
+
data: `${line}\n`,
|
|
134
|
+
mode: "append",
|
|
135
|
+
});
|
|
141
136
|
});
|
|
142
137
|
return String(entry.decision_id);
|
|
143
138
|
}
|
package/dist/scope/decompose.js
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* A caller supplies a draft with child story definitions; this module validates
|
|
7
7
|
* that draft, writes the child story vBRIEFs, and updates the parent scope references.
|
|
8
8
|
*/
|
|
9
|
-
import { accessSync, constants, existsSync, mkdirSync, readFileSync
|
|
9
|
+
import { accessSync, constants, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
10
10
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
11
11
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
12
|
-
import {
|
|
12
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
13
|
+
import { ProjectionContainmentError } from "../fs/projection-containment.js";
|
|
13
14
|
import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, resolveLifecycleRoot, } from "../layout/resolve.js";
|
|
14
15
|
import { referenceWithDefaultTrust, slugify } from "../vbrief-build/build.js";
|
|
15
16
|
import { EMITTED_VBRIEF_VERSION } from "../vbrief-build/constants.js";
|
|
@@ -54,9 +55,13 @@ function loadJson(path) {
|
|
|
54
55
|
return data;
|
|
55
56
|
}
|
|
56
57
|
function writeJson(projectRoot, path, data) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// #2980 wave D: product write sink routes through containedWrite.
|
|
59
|
+
containedWrite({
|
|
60
|
+
root: resolve(projectRoot),
|
|
61
|
+
target: path,
|
|
62
|
+
data: formatBriefJson(data),
|
|
63
|
+
mode: "replace",
|
|
64
|
+
});
|
|
60
65
|
}
|
|
61
66
|
// ---------------------------------------------------------------------------
|
|
62
67
|
// Story quality helpers (ported from _vbrief_story_quality.py)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync, readFileSync
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, relative, resolve } from "node:path";
|
|
3
3
|
import { referenceTypeMatches } from "@deftai/directive-types";
|
|
4
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
4
5
|
import { formatBriefJson } from "./vbrief-json.js";
|
|
5
6
|
import { collectChildUris, collectPlanRefs, resolveVbriefRef } from "./vbrief-ref.js";
|
|
6
7
|
function rewriteOnePlanRef(value, oldParentResolved, newParentRel, vbriefDir) {
|
|
@@ -53,7 +54,14 @@ function rewriteParentChildReference(parentPath, oldChildResolved, newChildRel,
|
|
|
53
54
|
}
|
|
54
55
|
if (changed) {
|
|
55
56
|
try {
|
|
56
|
-
|
|
57
|
+
// #2980 wave D: product write sink routes through containedWrite.
|
|
58
|
+
const dir = dirname(parentPath);
|
|
59
|
+
containedWrite({
|
|
60
|
+
root: resolve(dir),
|
|
61
|
+
target: parentPath,
|
|
62
|
+
data: formatBriefJson(parentData),
|
|
63
|
+
mode: "replace",
|
|
64
|
+
});
|
|
57
65
|
}
|
|
58
66
|
catch {
|
|
59
67
|
return false;
|
|
@@ -127,7 +135,14 @@ function rewriteChildParentReference(childPath, oldParentResolved, newParentRel,
|
|
|
127
135
|
}
|
|
128
136
|
if (changed) {
|
|
129
137
|
try {
|
|
130
|
-
|
|
138
|
+
// #2980 wave D: product write sink routes through containedWrite.
|
|
139
|
+
const dir = dirname(childPath);
|
|
140
|
+
containedWrite({
|
|
141
|
+
root: resolve(dir),
|
|
142
|
+
target: childPath,
|
|
143
|
+
data: formatBriefJson(childData),
|
|
144
|
+
mode: "replace",
|
|
145
|
+
});
|
|
131
146
|
}
|
|
132
147
|
catch {
|
|
133
148
|
return false;
|
package/dist/scope/demote.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
3
4
|
import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
|
|
4
5
|
import { hasArtifactSuffix, resolveLifecycleFolder } from "../layout/resolve.js";
|
|
5
6
|
import { stripTrailingPathSeparators } from "../text/redos-safe.js";
|
|
@@ -85,7 +86,13 @@ export function demoteOne(filePath, projectRoot, reason, options = {}) {
|
|
|
85
86
|
const timestamp = utcNowIso(now);
|
|
86
87
|
planObj.status = TARGET_STATUS;
|
|
87
88
|
planObj.updated = timestamp;
|
|
88
|
-
|
|
89
|
+
// #2980 wave D: product write sink routes through containedWrite.
|
|
90
|
+
containedWrite({
|
|
91
|
+
root: resolve(projectRoot),
|
|
92
|
+
target: resolved,
|
|
93
|
+
data: formatBriefJson(data),
|
|
94
|
+
mode: "replace",
|
|
95
|
+
});
|
|
89
96
|
const vbriefRoot = dirname(dirname(resolved));
|
|
90
97
|
const targetDir = join(vbriefRoot, TARGET_FOLDER);
|
|
91
98
|
mkdirSync(targetDir, { recursive: true });
|
package/dist/scope/undo.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync } from "node:fs";
|
|
2
2
|
import { basename, join, relative, resolve } from "node:path";
|
|
3
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
3
4
|
import { assertWriteTargetSafe, ProjectionContainmentError } from "../fs/projection-containment.js";
|
|
4
5
|
import { resolveLifecycleRoot } from "../layout/resolve.js";
|
|
5
6
|
import { append, canonicalLogPath, newDecisionId, readAll } from "./audit-log.js";
|
|
@@ -185,7 +186,13 @@ function moveAndFlip(srcFile, destFolder, newStatus, timestamp, projectRoot) {
|
|
|
185
186
|
const planObj = plan;
|
|
186
187
|
planObj.status = newStatus;
|
|
187
188
|
planObj.updated = timestamp;
|
|
188
|
-
|
|
189
|
+
// #2980 wave D: product write sink routes through containedWrite.
|
|
190
|
+
containedWrite({
|
|
191
|
+
root: resolve(projectRoot),
|
|
192
|
+
target: srcFile,
|
|
193
|
+
data: formatBriefJson(data),
|
|
194
|
+
mode: "replace",
|
|
195
|
+
});
|
|
189
196
|
mkdirSync(destFolder, { recursive: true });
|
|
190
197
|
const destPath = join(destFolder, basename(srcFile));
|
|
191
198
|
renameSync(srcFile, destPath);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, renameSync,
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { locateManifest, parseInstallManifest } from "../doctor/manifest.js";
|
|
5
5
|
import { runningInsideDeftRepo } from "../doctor/paths.js";
|
|
6
6
|
import { evaluateReleaseAvailability } from "../doctor/release-availability.js";
|
|
7
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
7
8
|
import { resolveTriageCachePath } from "../triage/cache-path.js";
|
|
8
9
|
const THROTTLE_MS = 24 * 60 * 60 * 1000;
|
|
9
10
|
const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
@@ -56,10 +57,29 @@ function isThrottled(state, latestVersion, now) {
|
|
|
56
57
|
return Number.isFinite(notifiedAt) && now.getTime() - notifiedAt < THROTTLE_MS;
|
|
57
58
|
}
|
|
58
59
|
function defaultWriteState(path, content) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
const dir = dirname(path);
|
|
61
|
+
mkdirSync(dir, { recursive: true });
|
|
62
|
+
const tmpBase = `${basename(path)}.${process.pid}.tmp`;
|
|
63
|
+
const temporary = join(dir, tmpBase);
|
|
64
|
+
try {
|
|
65
|
+
// #2980 wave D: product write sink routes through containedWrite.
|
|
66
|
+
containedWrite({
|
|
67
|
+
root: resolve(dir),
|
|
68
|
+
target: tmpBase,
|
|
69
|
+
data: content,
|
|
70
|
+
mode: "create",
|
|
71
|
+
});
|
|
72
|
+
renameSync(temporary, path);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
try {
|
|
76
|
+
rmSync(temporary, { force: true });
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
/* best-effort cleanup */
|
|
80
|
+
}
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
63
83
|
}
|
|
64
84
|
/**
|
|
65
85
|
* Probe the public npm registry separately from doctor. Doctor stays offline
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, } from "node:fs";
|
|
2
2
|
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { containedWrite } from "../fs/contained-write.js";
|
|
3
4
|
import { hasArtifactSuffix, LEGACY_ARTIFACT_DIR, MIGRATED_ARTIFACT_DIR, } from "../layout/resolve.js";
|
|
4
5
|
import { stableJson } from "./json.js";
|
|
5
6
|
import { RITUAL_STATE_CONTRACT } from "./posture.js";
|
|
@@ -92,24 +93,30 @@ function validateSteps(raw, key) {
|
|
|
92
93
|
return [steps, null];
|
|
93
94
|
}
|
|
94
95
|
function atomicWriteJson(targetPath, payload, prefix) {
|
|
95
|
-
mkdirSync(join(targetPath, ".."), { recursive: true });
|
|
96
96
|
const dir = join(targetPath, "..");
|
|
97
|
-
|
|
98
|
-
const
|
|
97
|
+
mkdirSync(dir, { recursive: true });
|
|
98
|
+
const tmpBase = `${prefix}${process.pid}.json.tmp`;
|
|
99
|
+
const tmpName = join(dir, tmpBase);
|
|
100
|
+
const text = `${stableJson(payload, 2)}\n`;
|
|
99
101
|
try {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
// #2980 wave D: ritual-state product write routes through containedWrite.
|
|
103
|
+
containedWrite({
|
|
104
|
+
root: resolve(dir),
|
|
105
|
+
target: tmpBase,
|
|
106
|
+
data: text,
|
|
107
|
+
mode: "create",
|
|
108
|
+
});
|
|
109
|
+
renameSync(tmpName, targetPath);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
102
112
|
try {
|
|
103
|
-
|
|
113
|
+
rmSync(tmpName, { force: true });
|
|
104
114
|
}
|
|
105
115
|
catch {
|
|
106
|
-
|
|
116
|
+
/* best-effort cleanup */
|
|
107
117
|
}
|
|
118
|
+
throw err;
|
|
108
119
|
}
|
|
109
|
-
finally {
|
|
110
|
-
closeSync(fd);
|
|
111
|
-
}
|
|
112
|
-
renameSync(tmpName, targetPath);
|
|
113
120
|
}
|
|
114
121
|
export function readRitualState(projectRoot) {
|
|
115
122
|
const stateFile = ritualStatePath(projectRoot);
|