@deftai/directive-core 0.107.0 → 0.109.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/architecture/sor-preflight.js +2 -0
- package/dist/cache/operations.js +1 -1
- package/dist/check/cached-orchestrator.js +28 -3
- package/dist/check/gate-lists.d.ts +14 -0
- package/dist/check/gate-lists.js +26 -3
- package/dist/check/named-cause.js +18 -3
- package/dist/check/session-completed-ac.d.ts +1 -1
- package/dist/check/session-completed-ac.js +1 -1
- package/dist/codebase/provider.js +7 -1
- package/dist/consumer-check-contract/evaluate.d.ts +47 -0
- package/dist/consumer-check-contract/evaluate.js +181 -15
- package/dist/delivery-attempt/evaluate.d.ts +9 -1
- package/dist/delivery-attempt/evaluate.js +69 -0
- package/dist/delivery-attempt/handoff.js +1 -1
- package/dist/delivery-attempt/index.d.ts +1 -1
- package/dist/delivery-attempt/index.js +1 -1
- package/dist/deposit/live-procedure-exclusions.d.ts +18 -0
- package/dist/deposit/live-procedure-exclusions.js +110 -0
- package/dist/deposit/live-procedure-targets.d.ts +45 -0
- package/dist/deposit/live-procedure-targets.js +274 -0
- package/dist/deposit/python-free.d.ts +6 -0
- package/dist/deposit/python-free.js +15 -0
- package/dist/deposit/rewrite-deposit-links.d.ts +42 -0
- package/dist/deposit/rewrite-deposit-links.js +148 -0
- package/dist/deposit/run-stage-content-pack.d.ts +2 -0
- package/dist/deposit/run-stage-content-pack.js +3 -0
- package/dist/deposit/stage-content-pack.d.ts +17 -0
- package/dist/deposit/stage-content-pack.js +91 -0
- package/dist/design-critique/citation-grammar.d.ts +60 -0
- package/dist/design-critique/citation-grammar.js +303 -0
- package/dist/design-critique/completed-arc-record.d.ts +62 -0
- package/dist/design-critique/completed-arc-record.js +277 -0
- package/dist/freshness/bind.d.ts +32 -3
- package/dist/freshness/bind.js +58 -12
- package/dist/hooks/classify/host-session-identity.d.ts +89 -0
- package/dist/hooks/classify/host-session-identity.js +467 -0
- package/dist/hooks/classify/index.d.ts +3 -2
- package/dist/hooks/classify/index.js +3 -2
- package/dist/hooks/classify/paths.d.ts +9 -0
- package/dist/hooks/classify/paths.js +39 -0
- package/dist/hooks/classify/stdin.d.ts +5 -0
- package/dist/hooks/classify/stdin.js +94 -1
- package/dist/hooks/dest-form.d.ts +20 -1
- package/dist/hooks/dest-form.js +158 -21
- package/dist/hooks/dispatcher.d.ts +58 -3
- package/dist/hooks/dispatcher.js +762 -73
- package/dist/hooks/fixtures/cases.d.ts +22 -2
- package/dist/hooks/fixtures/cases.js +228 -0
- package/dist/hooks/git-destructive-log.d.ts +32 -0
- package/dist/hooks/git-destructive-log.js +46 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/scope.js +3 -1
- package/dist/hooks/shell-write-targets.d.ts +10 -0
- package/dist/hooks/shell-write-targets.js +274 -0
- package/dist/hooks/tools.d.ts +20 -2
- package/dist/hooks/tools.js +30 -1
- package/dist/init-deposit/agent-hooks.d.ts +2 -1
- package/dist/init-deposit/agent-hooks.js +8 -1
- package/dist/init-deposit/gitignore.d.ts +7 -0
- package/dist/init-deposit/gitignore.js +24 -0
- package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
- package/dist/init-deposit/hook-runtime-travel.js +269 -0
- package/dist/init-deposit/init-deposit.js +3 -0
- package/dist/init-deposit/refresh.js +7 -0
- package/dist/init-deposit/runtime-writers.d.ts +14 -0
- package/dist/init-deposit/runtime-writers.js +33 -0
- package/dist/intake/clause-derivation.js +9 -3
- package/dist/intake/issue-ingest.d.ts +6 -1
- package/dist/intake/issue-ingest.js +83 -4
- package/dist/intake/platform-capabilities.d.ts +9 -2
- package/dist/intake/platform-capabilities.js +38 -9
- package/dist/intake/reconcile-issues.js +13 -13
- package/dist/lifecycle/brief-envelope.d.ts +25 -0
- package/dist/lifecycle/brief-envelope.js +42 -0
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.js +1 -0
- package/dist/literal-acceptance/evaluate.js +14 -5
- package/dist/literal-acceptance/index.d.ts +1 -1
- package/dist/literal-acceptance/index.js +1 -1
- package/dist/literal-acceptance/run.d.ts +2 -0
- package/dist/literal-acceptance/run.js +19 -1
- package/dist/orphan-active/candidate-scope.d.ts +53 -0
- package/dist/orphan-active/candidate-scope.js +157 -0
- package/dist/orphan-active/evaluate.d.ts +52 -0
- package/dist/orphan-active/evaluate.js +250 -128
- package/dist/orphan-active/index.d.ts +2 -0
- package/dist/orphan-active/index.js +2 -0
- package/dist/orphan-active/issue-state.d.ts +121 -0
- package/dist/orphan-active/issue-state.js +278 -0
- package/dist/platform/cursor-managed-runtime.d.ts +95 -0
- package/dist/platform/cursor-managed-runtime.js +241 -0
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +1 -0
- package/dist/platform/platform-capabilities.d.ts +23 -1
- package/dist/platform/platform-capabilities.js +55 -15
- package/dist/policy/ceremony-dial.js +5 -9
- package/dist/policy/host-hooks.js +4 -9
- package/dist/policy/merge-approval-head.js +7 -6
- package/dist/policy/org-force-on-migration.js +5 -10
- package/dist/policy/plan-extensions.d.ts +14 -2
- package/dist/policy/plan-extensions.js +24 -3
- package/dist/policy/product-signal.js +5 -10
- package/dist/policy/require-human-merge.js +5 -9
- package/dist/policy/resolve.js +77 -16
- package/dist/policy/value-feedback.js +9 -18
- package/dist/pr-closeout-attestable/evaluate.d.ts +80 -0
- package/dist/pr-closeout-attestable/evaluate.js +306 -0
- package/dist/pr-closeout-attestable/index.d.ts +2 -0
- package/dist/pr-closeout-attestable/index.js +2 -0
- package/dist/pr-closing-keywords/gh.js +32 -9
- package/dist/pr-closing-keywords/main.d.ts +1 -0
- package/dist/pr-closing-keywords/main.js +68 -3
- package/dist/pr-closing-keywords/types.d.ts +2 -0
- package/dist/pr-merge-readiness/gh.js +32 -9
- package/dist/pr-protected-issues/gh.d.ts +6 -2
- package/dist/pr-protected-issues/gh.js +42 -11
- package/dist/pr-wait-mergeable/cascade.d.ts +9 -1
- package/dist/pr-wait-mergeable/cascade.js +21 -1
- package/dist/pr-wait-mergeable/types.d.ts +2 -0
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -0
- package/dist/pr-wait-mergeable/wrappers.js +14 -3
- package/dist/preflight/evaluate.d.ts +15 -0
- package/dist/preflight/evaluate.js +29 -3
- package/dist/product-first-done-gate/acceptance-resolver.d.ts +14 -2
- package/dist/product-first-done-gate/acceptance-resolver.js +13 -2
- package/dist/product-first-done-gate/evaluate.js +23 -9
- package/dist/product-first-done-gate/types.js +2 -0
- package/dist/release/cli-drift-report.d.ts +68 -0
- package/dist/release/cli-drift-report.js +189 -0
- package/dist/release/consumer-hard-stops.d.ts +48 -0
- package/dist/release/consumer-hard-stops.js +140 -0
- package/dist/release/consumer-readiness-disclosure.d.ts +18 -0
- package/dist/release/consumer-readiness-disclosure.js +51 -0
- package/dist/release/index.d.ts +3 -0
- package/dist/release/index.js +3 -0
- package/dist/release/issue-state-fetch.d.ts +5 -3
- package/dist/release/issue-state-fetch.js +86 -21
- package/dist/release/native-steps.js +1 -0
- package/dist/release/pipeline.js +74 -0
- package/dist/release/run-consumer-readiness.d.ts +15 -0
- package/dist/release/run-consumer-readiness.js +24 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/render/constants.d.ts +4 -0
- package/dist/render/constants.js +11 -8
- package/dist/render/export-spec.js +31 -5
- package/dist/render/index.d.ts +1 -1
- package/dist/render/index.js +1 -1
- package/dist/render/prd-render.d.ts +4 -1
- package/dist/render/prd-render.js +82 -23
- package/dist/render/project-render.js +14 -7
- package/dist/render/scope-outlook.d.ts +2 -0
- package/dist/render/scope-outlook.js +3 -0
- package/dist/render/spec-render.js +2 -2
- package/dist/resolution/package-manager.d.ts +33 -4
- package/dist/resolution/package-manager.js +210 -9
- package/dist/review-monitor/constants.d.ts +7 -0
- package/dist/review-monitor/constants.js +7 -0
- package/dist/review-monitor/github-lease.d.ts +98 -1
- package/dist/review-monitor/github-lease.js +186 -4
- package/dist/review-monitor/lease-comment.d.ts +36 -0
- package/dist/review-monitor/lease-comment.js +103 -2
- package/dist/run-summary/types.d.ts +2 -2
- package/dist/scm/build-command.d.ts +2 -2
- package/dist/scm/build-command.js +2 -2
- package/dist/scm/call-shape.d.ts +25 -0
- package/dist/scm/call-shape.js +59 -0
- package/dist/scm/call.d.ts +7 -4
- package/dist/scm/call.js +48 -9
- package/dist/scm/design-critique-chip.d.ts +1 -0
- package/dist/scm/design-critique-chip.js +22 -6
- package/dist/scm/gh-rest.d.ts +34 -13
- package/dist/scm/gh-rest.js +168 -15
- package/dist/scm/index.d.ts +2 -0
- package/dist/scm/index.js +2 -0
- package/dist/scm/readiness.d.ts +2 -0
- package/dist/scm/readiness.js +28 -4
- package/dist/scm/spawn-status.d.ts +33 -0
- package/dist/scm/spawn-status.js +53 -0
- package/dist/scope/acceptance-evidence.d.ts +1 -1
- package/dist/scope/project-definition-sync.js +14 -7
- package/dist/scope/transition.js +4 -14
- package/dist/session/ac-pass-banking.d.ts +2 -2
- package/dist/session/ac-pass-banking.js +2 -2
- package/dist/session/git.d.ts +24 -0
- package/dist/session/git.js +138 -2
- package/dist/session/host-session-owner.d.ts +53 -0
- package/dist/session/host-session-owner.js +89 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/session/occupancy.d.ts +259 -13
- package/dist/session/occupancy.js +877 -33
- package/dist/session/ritual-sentinel.d.ts +5 -0
- package/dist/session/ritual-sentinel.js +5 -0
- package/dist/session/session-ready.d.ts +5 -1
- package/dist/session/session-ready.js +98 -8
- package/dist/session/session-start.d.ts +5 -1
- package/dist/session/session-start.js +32 -17
- package/dist/session/toolchain-preflight.d.ts +13 -3
- package/dist/session/toolchain-preflight.js +93 -18
- package/dist/session/verify-ac-session-cache.d.ts +2 -2
- package/dist/session/verify-ac-session-cache.js +2 -2
- package/dist/session/verify-session-ritual.d.ts +11 -0
- package/dist/session/verify-session-ritual.js +60 -15
- package/dist/slice/constants.d.ts +1 -1
- package/dist/spec-authority/constants.d.ts +10 -2
- package/dist/spec-authority/constants.js +53 -8
- package/dist/spec-authority/resolver.d.ts +3 -0
- package/dist/spec-authority/resolver.js +55 -7
- package/dist/subprocess/max-buffer.d.ts +14 -0
- package/dist/subprocess/max-buffer.js +15 -0
- package/dist/swarm/complete-cohort.d.ts +2 -0
- package/dist/swarm/complete-cohort.js +21 -13
- package/dist/swarm/index.d.ts +1 -0
- package/dist/swarm/index.js +1 -0
- package/dist/swarm/launch-cli.js +53 -33
- package/dist/swarm/launch.d.ts +8 -0
- package/dist/swarm/launch.js +63 -43
- package/dist/swarm/pre-dispatch-cli.js +2 -1
- package/dist/swarm/pre-dispatch.js +10 -1
- package/dist/swarm/subagent-status-dir.d.ts +27 -0
- package/dist/swarm/subagent-status-dir.js +42 -0
- package/dist/swarm/worktrees.d.ts +17 -3
- package/dist/swarm/worktrees.js +48 -4
- package/dist/triage/evaluate/worktrees.js +153 -6
- package/dist/triage/scope/mutations-core.d.ts +2 -6
- package/dist/triage/scope/mutations-core.js +23 -55
- package/dist/triage/scope-drift/add-ignore.js +5 -39
- package/dist/triage/subscribe/index.d.ts +2 -6
- package/dist/triage/subscribe/index.js +15 -65
- package/dist/triage/welcome/writers.js +15 -74
- package/dist/umbrella-current-shape/index.d.ts +51 -3
- package/dist/umbrella-current-shape/index.js +106 -18
- package/dist/validate-content/deposit-required.d.ts +39 -0
- package/dist/validate-content/deposit-required.js +147 -0
- package/dist/validate-content/index.d.ts +1 -0
- package/dist/validate-content/index.js +1 -0
- package/dist/validate-content/validate-links.d.ts +2 -3
- package/dist/validate-content/validate-links.js +29 -2
- package/dist/vbrief-activate/activate.d.ts +7 -2
- package/dist/vbrief-activate/activate.js +26 -13
- package/dist/vbrief-build/index.d.ts +2 -1
- package/dist/vbrief-build/index.js +2 -1
- package/dist/vbrief-build/parity-scenarios.js +5 -5
- package/dist/vbrief-build/project-definition-io.d.ts +63 -3
- package/dist/vbrief-build/project-definition-io.js +357 -31
- package/dist/vbrief-build/project-definition-mutation.d.ts +32 -0
- package/dist/vbrief-build/project-definition-mutation.js +43 -0
- package/dist/vbrief-validate/conformance.d.ts +1 -0
- package/dist/vbrief-validate/conformance.js +85 -8
- package/dist/vbrief-validate/main.js +2 -0
- package/dist/vbrief-validate/precutover.js +5 -10
- package/dist/verify-ac/clauses.d.ts +62 -2
- package/dist/verify-ac/clauses.js +183 -111
- package/dist/verify-ac/evaluate.d.ts +9 -0
- package/dist/verify-ac/evaluate.js +32 -9
- package/dist/verify-ac/index.d.ts +1 -1
- package/dist/verify-ac/index.js +1 -1
- package/dist/verify-env/node-runtime.d.ts +8 -4
- package/dist/verify-env/node-runtime.js +9 -6
- package/dist/verify-env/toolchain-check.d.ts +35 -3
- package/dist/verify-env/toolchain-check.js +155 -36
- package/dist/verify-source/deposit-closure.d.ts +23 -0
- package/dist/verify-source/deposit-closure.js +162 -0
- package/dist/verify-source/index.d.ts +2 -0
- package/dist/verify-source/index.js +2 -0
- package/dist/verify-source/project-definition-mutation-boundary.d.ts +104 -0
- package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
- package/dist/verify-source/semantic-single-source.d.ts +36 -0
- package/dist/verify-source/semantic-single-source.js +349 -0
- package/package.json +7 -3
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync, } from "node:fs";
|
|
2
|
+
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, realpathSync, renameSync, rmdirSync, unlinkSync, writeSync, } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { performance } from "node:perf_hooks";
|
|
4
5
|
import { containedWrite } from "../fs/contained-write.js";
|
|
5
6
|
import { resolveProjectDefinitionPath } from "../layout/resolve.js";
|
|
6
7
|
import { pythonJsonPretty } from "./json.js";
|
|
7
8
|
import { ProjectDefinitionIOError } from "./types.js";
|
|
8
9
|
const mutationThreadLock = { held: false };
|
|
10
|
+
/** Setup override for a noncanonical PROJECT-DEFINITION path. */
|
|
11
|
+
export const ENV_PROJECT_PATH = "DEFT_PROJECT_PATH";
|
|
12
|
+
/**
|
|
13
|
+
* Constant display label for a PROJECT-DEFINITION reached through the
|
|
14
|
+
* `DEFT_PROJECT_PATH` override (#3796). A configured path is operator- or
|
|
15
|
+
* environment-supplied data, so lock and loader diagnostics name this label
|
|
16
|
+
* instead of interpolating the raw path. Matches the label already used by
|
|
17
|
+
* `verify:vbrief-conformance` so one artifact reads the same way on every
|
|
18
|
+
* diagnostic surface.
|
|
19
|
+
*/
|
|
20
|
+
export const CONFIGURED_PROJECT_DEFINITION_LABEL = "<configured PROJECT-DEFINITION>";
|
|
9
21
|
/**
|
|
10
22
|
* Absolute path to the PROJECT-DEFINITION artifact. Layout-aware (#2302):
|
|
11
23
|
* resolves `xbrief/PROJECT-DEFINITION.xbrief.json` on a migrated tree, else the
|
|
@@ -13,23 +25,209 @@ const mutationThreadLock = { held: false };
|
|
|
13
25
|
* name the path that actually applies to the project's layout.
|
|
14
26
|
*/
|
|
15
27
|
export function projectDefinitionPath(projectRoot) {
|
|
28
|
+
const override = process.env[ENV_PROJECT_PATH]?.trim();
|
|
29
|
+
if (override) {
|
|
30
|
+
const configuredPath = resolve(projectRoot, override);
|
|
31
|
+
return existsSync(configuredPath) ? realpathSync(configuredPath) : configuredPath;
|
|
32
|
+
}
|
|
16
33
|
return resolveProjectDefinitionPath(resolve(projectRoot));
|
|
17
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Control-safe label for an artifact path in a human diagnostic (#3796).
|
|
37
|
+
* A configured artifact collapses to {@link CONFIGURED_PROJECT_DEFINITION_LABEL};
|
|
38
|
+
* a layout-resolved canonical artifact keeps its path because the layout
|
|
39
|
+
* resolver derived it rather than reading it from configuration.
|
|
40
|
+
*/
|
|
41
|
+
export function projectDefinitionArtifactLabel(artifactPath) {
|
|
42
|
+
return process.env[ENV_PROJECT_PATH]?.trim() ? CONFIGURED_PROJECT_DEFINITION_LABEL : artifactPath;
|
|
43
|
+
}
|
|
44
|
+
const sleepCell = new Int32Array(new SharedArrayBuffer(4));
|
|
18
45
|
function defaultSleep(ms) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
46
|
+
Atomics.wait(sleepCell, 0, 0, ms);
|
|
47
|
+
}
|
|
48
|
+
const DEFAULT_ACQUISITION_BUDGET_MS = 30_000;
|
|
49
|
+
const LOCK_OWNER_ENTRY_RE = /^([1-9]\d*)-([a-f0-9]{32})$/;
|
|
50
|
+
const RENAME_CONTENTION_CODES = new Set([
|
|
51
|
+
"EACCES",
|
|
52
|
+
"EEXIST",
|
|
53
|
+
"EISDIR",
|
|
54
|
+
"ENOTDIR",
|
|
55
|
+
"ENOTEMPTY",
|
|
56
|
+
"EPERM",
|
|
57
|
+
]);
|
|
58
|
+
/** Acquisition failed within the shared budget. `reason` names the blocker. */
|
|
59
|
+
export class ProjectDefinitionLockError extends Error {
|
|
60
|
+
reason;
|
|
61
|
+
constructor(message, reason) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = "ProjectDefinitionLockError";
|
|
64
|
+
this.reason = reason;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function defaultProbeProcess(pid) {
|
|
68
|
+
try {
|
|
69
|
+
process.kill(pid, 0);
|
|
70
|
+
return "alive";
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
const code = err.code;
|
|
74
|
+
if (code === "ESRCH")
|
|
75
|
+
return "dead";
|
|
76
|
+
// EPERM proves a process holds the PID but not that we may signal it. That
|
|
77
|
+
// is indistinguishable from a live owner, so it is never a reap licence.
|
|
78
|
+
if (code === "EPERM")
|
|
79
|
+
return "alive";
|
|
80
|
+
return "unknown";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function createAcquisitionBudget(monotonicNowMs, totalMs) {
|
|
84
|
+
const startedAt = monotonicNowMs();
|
|
85
|
+
return {
|
|
86
|
+
expired: () => monotonicNowMs() - startedAt >= totalMs,
|
|
87
|
+
totalMs,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function lstatIfExists(path) {
|
|
91
|
+
return lstatSync(path, { throwIfNoEntry: false }) ?? null;
|
|
92
|
+
}
|
|
93
|
+
function parseDirectoryLockOwnerEntry(entryName) {
|
|
94
|
+
const match = LOCK_OWNER_ENTRY_RE.exec(entryName);
|
|
95
|
+
if (match === null)
|
|
96
|
+
return null;
|
|
97
|
+
const pid = Number(match[1]);
|
|
98
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || match[2] === undefined)
|
|
99
|
+
return null;
|
|
100
|
+
return { pid, token: match[2], entryName };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Read the single owner entry of a well-formed lock directory. Any other shape
|
|
104
|
+
* (empty, multiple entries, unparseable name) is deliberately `null` -- the
|
|
105
|
+
* caller must then fail closed rather than guess which entry owns the lock.
|
|
106
|
+
*/
|
|
107
|
+
function readDirectoryLockOwner(lockPath) {
|
|
108
|
+
let entries;
|
|
109
|
+
try {
|
|
110
|
+
entries = readdirSync(lockPath);
|
|
22
111
|
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
if (err.code === "ENOENT")
|
|
114
|
+
return null;
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
if (entries.length !== 1)
|
|
118
|
+
return null;
|
|
119
|
+
return parseDirectoryLockOwnerEntry(entries[0] ?? "");
|
|
23
120
|
}
|
|
24
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Reap a current-version directory lock whose owner is unambiguously dead.
|
|
123
|
+
*
|
|
124
|
+
* The unique owner entry is removed first: exactly one contender can win that
|
|
125
|
+
* unlink, and only the winner may `rmdir`. Because every published lock
|
|
126
|
+
* directory is non-empty at the instant it becomes visible, a delayed `rmdir`
|
|
127
|
+
* from an older generation cannot remove a replacement -- it fails `ENOTEMPTY`.
|
|
128
|
+
*
|
|
129
|
+
* Reaping is automatic only for a `dead` liveness result. `alive` (which cannot
|
|
130
|
+
* be distinguished from PID reuse) and `unknown` fail closed, as does any
|
|
131
|
+
* malformed directory: age is not evidence of safety (#3796).
|
|
132
|
+
*/
|
|
133
|
+
function reapDirectoryLock(lockPath, probeProcess, beforeLockDirRemove) {
|
|
134
|
+
const owner = readDirectoryLockOwner(lockPath);
|
|
135
|
+
if (owner === null) {
|
|
136
|
+
return { reaped: false, reason: "malformed-lock-directory" };
|
|
137
|
+
}
|
|
138
|
+
const liveness = probeProcess(owner.pid);
|
|
139
|
+
if (liveness === "alive")
|
|
140
|
+
return { reaped: false, reason: "owner-alive" };
|
|
141
|
+
if (liveness === "unknown")
|
|
142
|
+
return { reaped: false, reason: "owner-liveness-unknown" };
|
|
143
|
+
try {
|
|
144
|
+
unlinkSync(join(lockPath, owner.entryName));
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
// Another contender won the entry unlink; only that winner may rmdir.
|
|
148
|
+
if (err.code === "ENOENT") {
|
|
149
|
+
return { reaped: false, reason: "contended" };
|
|
150
|
+
}
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
beforeLockDirRemove(lockPath);
|
|
154
|
+
try {
|
|
155
|
+
rmdirSync(lockPath);
|
|
156
|
+
return { reaped: true };
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
const code = err.code;
|
|
160
|
+
if (code === "ENOENT")
|
|
161
|
+
return { reaped: true };
|
|
162
|
+
// A replacement generation was published into this pathname while the
|
|
163
|
+
// rmdir was delayed. It is non-empty, so the rmdir cannot destroy it.
|
|
164
|
+
if (code === "ENOTEMPTY" || code === "EEXIST") {
|
|
165
|
+
return { reaped: false, reason: "contended" };
|
|
166
|
+
}
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function describeLockFailure(artifactLabel, lockPath, reason, budgetMs) {
|
|
171
|
+
// The sidecar name is derived from the artifact name, so naming it would leak
|
|
172
|
+
// a configured filename back into a message that is supposed to be labelled.
|
|
173
|
+
const sidecarName = artifactLabel === CONFIGURED_PROJECT_DEFINITION_LABEL ? ".lock" : basename(lockPath);
|
|
174
|
+
const header = `timed out acquiring the PROJECT-DEFINITION mutation lock for ${artifactLabel} ` +
|
|
175
|
+
`after ${budgetMs}ms (blocked by: ${reason}).`;
|
|
176
|
+
if (reason === "legacy-file-sidecar") {
|
|
177
|
+
return [
|
|
178
|
+
header,
|
|
179
|
+
"A legacy single-file lock sidecar holds the public lock pathname. It is",
|
|
180
|
+
"never removed or moved automatically: releasing that pathname would let a",
|
|
181
|
+
"new waiter enter while the legacy holder's descriptor and critical section",
|
|
182
|
+
"are still live.",
|
|
183
|
+
"Manual recovery:",
|
|
184
|
+
" 1. Stop every legacy deft client that mutates this project.",
|
|
185
|
+
" 2. Confirm no PROJECT-DEFINITION mutation is running.",
|
|
186
|
+
` 3. Remove the '${sidecarName}' sidecar beside ${artifactLabel}.`,
|
|
187
|
+
].join("\n");
|
|
188
|
+
}
|
|
189
|
+
if (reason === "malformed-lock-directory" || reason === "owner-liveness-unknown") {
|
|
190
|
+
return [
|
|
191
|
+
header,
|
|
192
|
+
"The lock state cannot be attributed to a known owner, so it is never",
|
|
193
|
+
"recovered automatically -- elapsed time is not evidence that the owner is",
|
|
194
|
+
"gone.",
|
|
195
|
+
"Manual recovery:",
|
|
196
|
+
" 1. Stop every deft client that mutates this project.",
|
|
197
|
+
" 2. Confirm no PROJECT-DEFINITION mutation is running.",
|
|
198
|
+
` 3. Remove the '${sidecarName}' lock directory beside ${artifactLabel}.`,
|
|
199
|
+
].join("\n");
|
|
200
|
+
}
|
|
201
|
+
return [
|
|
202
|
+
header,
|
|
203
|
+
"Another PROJECT-DEFINITION mutation still holds the lock. Retry once it",
|
|
204
|
+
"completes; if the owner has gone without releasing, stop all deft clients",
|
|
205
|
+
`and remove the '${sidecarName}' lock directory beside ${artifactLabel}.`,
|
|
206
|
+
].join("\n");
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Serialise PROJECT-DEFINITION read-modify-write critical sections.
|
|
210
|
+
*
|
|
211
|
+
* Acquisition publishes a fully-materialised, non-empty owner directory by
|
|
212
|
+
* renaming it onto the public lock pathname, so no contender can observe a
|
|
213
|
+
* partial lock. Prefer {@link withProjectDefinitionMutation} in
|
|
214
|
+
* `project-definition-mutation.ts`: it binds load/parse/persist to the captured
|
|
215
|
+
* artifact path so a caller cannot lock one identity and write another (#3796).
|
|
216
|
+
*/
|
|
25
217
|
export function projectDefinitionMutationLock(projectRoot, fn, deps = {}) {
|
|
26
218
|
const sleepMs = deps.sleepMs ?? defaultSleep;
|
|
27
|
-
const
|
|
219
|
+
const monotonicNowMs = deps.monotonicNowMs ?? (() => performance.now());
|
|
220
|
+
const budgetMs = deps.acquisitionBudgetMs ?? DEFAULT_ACQUISITION_BUDGET_MS;
|
|
221
|
+
const probeProcess = deps.probeProcess ?? defaultProbeProcess;
|
|
222
|
+
const writeOwner = deps.writeOwner ?? writeSync;
|
|
223
|
+
const renameLock = deps.renameLock ?? renameSync;
|
|
224
|
+
const beforeLockDirRemove = deps.beforeLockDirRemove ?? (() => undefined);
|
|
28
225
|
// Derive the sidecar lock path from the layout-aware resolved PROJECT-DEFINITION
|
|
29
226
|
// path (xbrief/ when migrated, else vbrief/) so the lock lives next to the real
|
|
30
227
|
// artifact and every mutator sharing a project root contends on the same lock,
|
|
31
228
|
// instead of the constant vbrief/ path which would strand a stray lock (#1260).
|
|
32
|
-
const path =
|
|
229
|
+
const path = projectDefinitionPath(projectRoot);
|
|
230
|
+
const artifactLabel = projectDefinitionArtifactLabel(path);
|
|
33
231
|
const lockPath = `${path}.lock`;
|
|
34
232
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
35
233
|
if (mutationThreadLock.held) {
|
|
@@ -37,50 +235,178 @@ export function projectDefinitionMutationLock(projectRoot, fn, deps = {}) {
|
|
|
37
235
|
}
|
|
38
236
|
mutationThreadLock.held = true;
|
|
39
237
|
let fd;
|
|
238
|
+
let ownerEntryPath;
|
|
239
|
+
let ownerToken;
|
|
240
|
+
let preparedEntryPath;
|
|
241
|
+
let preparedLockPath;
|
|
242
|
+
let acquired = false;
|
|
40
243
|
try {
|
|
41
|
-
|
|
244
|
+
// Fully materialize owner metadata in a unique sibling directory before
|
|
245
|
+
// publishing it. Renaming the non-empty directory is the exclusive claim: no
|
|
246
|
+
// contender can observe an empty or partial lock, and a stale reaper cannot
|
|
247
|
+
// remove a replacement because replacements are non-empty when they become
|
|
248
|
+
// visible.
|
|
249
|
+
ownerToken = randomBytes(16).toString("hex");
|
|
250
|
+
const ownerEntryName = `${process.pid}-${ownerToken}`;
|
|
251
|
+
const preparedPath = `${lockPath}.claim-${ownerEntryName}`;
|
|
252
|
+
preparedLockPath = preparedPath;
|
|
253
|
+
preparedEntryPath = join(preparedPath, ownerEntryName);
|
|
254
|
+
mkdirSync(preparedPath);
|
|
255
|
+
const payload = `${JSON.stringify({ pid: process.pid, token: ownerToken })}\n`;
|
|
256
|
+
try {
|
|
257
|
+
fd = openSync(preparedEntryPath, "wx");
|
|
258
|
+
const written = writeOwner(fd, payload);
|
|
259
|
+
if (written !== Buffer.byteLength(payload)) {
|
|
260
|
+
throw new Error("short write while recording project definition lock owner");
|
|
261
|
+
}
|
|
262
|
+
closeSync(fd);
|
|
263
|
+
fd = undefined;
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
if (fd !== undefined) {
|
|
267
|
+
try {
|
|
268
|
+
closeSync(fd);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
/* best-effort */
|
|
272
|
+
}
|
|
273
|
+
fd = undefined;
|
|
274
|
+
}
|
|
275
|
+
throw err;
|
|
276
|
+
}
|
|
277
|
+
// One budget, read once from the monotonic clock, shared by every retry and
|
|
278
|
+
// recovery branch below. Callback execution happens after acquisition and is
|
|
279
|
+
// deliberately outside this budget.
|
|
280
|
+
const budget = createAcquisitionBudget(monotonicNowMs, budgetMs);
|
|
281
|
+
// Every blocked branch either yields for a retry or spends the shared budget
|
|
282
|
+
// and reports the blocker it just observed. Keeping that in one place means
|
|
283
|
+
// no branch carries a reason it never reports.
|
|
284
|
+
const yieldOrFail = (reason) => {
|
|
285
|
+
if (budget.expired()) {
|
|
286
|
+
throw new ProjectDefinitionLockError(describeLockFailure(artifactLabel, lockPath, reason, budget.totalMs), reason);
|
|
287
|
+
}
|
|
288
|
+
sleepMs(20);
|
|
289
|
+
};
|
|
42
290
|
while (true) {
|
|
291
|
+
// Check for a legacy file BEFORE attempting to publish. POSIX `rename`
|
|
292
|
+
// refuses to move a directory onto a file (ENOTDIR), but Windows
|
|
293
|
+
// `MoveFileEx` is called with MOVEFILE_REPLACE_EXISTING and will happily
|
|
294
|
+
// replace the file -- which would destroy the legacy sidecar this
|
|
295
|
+
// protocol exists to preserve. So absence of a non-directory at the
|
|
296
|
+
// public pathname is a precondition of publication, not a side effect of
|
|
297
|
+
// it. (A legacy client that creates the sidecar inside the window between
|
|
298
|
+
// this check and the rename is still a Windows-only residual; nothing in
|
|
299
|
+
// portable Node makes the publish itself conditional on the destination.)
|
|
300
|
+
const existing = lstatIfExists(lockPath);
|
|
301
|
+
if (existing !== null && !existing.isDirectory()) {
|
|
302
|
+
yieldOrFail("legacy-file-sidecar");
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
43
305
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
306
|
+
renameLock(preparedPath, lockPath);
|
|
307
|
+
acquired = true;
|
|
308
|
+
preparedLockPath = undefined;
|
|
309
|
+
preparedEntryPath = undefined;
|
|
310
|
+
ownerEntryPath = join(lockPath, ownerEntryName);
|
|
49
311
|
break;
|
|
50
312
|
}
|
|
51
313
|
catch (err) {
|
|
52
314
|
const code = err.code;
|
|
53
|
-
if (code
|
|
315
|
+
if (code === undefined || !RENAME_CONTENTION_CODES.has(code)) {
|
|
54
316
|
throw err;
|
|
55
317
|
}
|
|
56
|
-
|
|
57
|
-
|
|
318
|
+
const lockStat = lstatIfExists(lockPath);
|
|
319
|
+
if (lockStat === null) {
|
|
320
|
+
// Windows may surface destination collisions as EACCES/EPERM, but
|
|
321
|
+
// without a destination these are permission failures rather than
|
|
322
|
+
// contention. Propagate them instead of spinning until timeout.
|
|
323
|
+
if (code === "EACCES" || code === "EPERM")
|
|
324
|
+
throw err;
|
|
325
|
+
// The observed owner released between rename and inspection.
|
|
326
|
+
continue;
|
|
58
327
|
}
|
|
59
|
-
|
|
328
|
+
if (lockStat.isDirectory()) {
|
|
329
|
+
const outcome = reapDirectoryLock(lockPath, probeProcess, beforeLockDirRemove);
|
|
330
|
+
if (outcome.reaped)
|
|
331
|
+
continue;
|
|
332
|
+
yieldOrFail(outcome.reason);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
// A legacy client took the public lock pathname with a plain file
|
|
336
|
+
// between the pre-check above and this rename. `rename` is atomic name
|
|
337
|
+
// movement, not compare-and-remove: vacating this name -- by unlink or
|
|
338
|
+
// by quarantine -- hands it to a non-cooperating `open(..., "wx")`
|
|
339
|
+
// waiter while the displaced holder's descriptor and critical section
|
|
340
|
+
// stay live. So the file is preserved and recovery is manual (#3796).
|
|
341
|
+
yieldOrFail("legacy-file-sidecar");
|
|
60
342
|
}
|
|
61
343
|
}
|
|
62
|
-
return fn();
|
|
344
|
+
return fn(path);
|
|
63
345
|
}
|
|
64
346
|
finally {
|
|
65
|
-
if (fd !== undefined) {
|
|
66
|
-
closeSync(fd);
|
|
67
|
-
}
|
|
68
|
-
mutationThreadLock.held = false;
|
|
69
347
|
try {
|
|
70
|
-
if (
|
|
71
|
-
|
|
348
|
+
if (fd !== undefined) {
|
|
349
|
+
try {
|
|
350
|
+
closeSync(fd);
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
/* best-effort */
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (acquired) {
|
|
357
|
+
try {
|
|
358
|
+
if (ownerEntryPath !== undefined && ownerToken !== undefined) {
|
|
359
|
+
const current = readDirectoryLockOwner(lockPath);
|
|
360
|
+
if (current?.token === ownerToken &&
|
|
361
|
+
join(lockPath, current.entryName) === ownerEntryPath) {
|
|
362
|
+
unlinkSync(ownerEntryPath);
|
|
363
|
+
rmdirSync(lockPath);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
/* best-effort */
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
// Publication never happened: drop the prepared owner so a crash-free
|
|
373
|
+
// failure path leaves no litter. Surviving litter from a crash stays
|
|
374
|
+
// fail-closed rather than being age-guessed into safety.
|
|
375
|
+
try {
|
|
376
|
+
if (preparedEntryPath !== undefined)
|
|
377
|
+
unlinkSync(preparedEntryPath);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
/* best-effort */
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
if (preparedLockPath !== undefined)
|
|
384
|
+
rmdirSync(preparedLockPath);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
/* best-effort */
|
|
388
|
+
}
|
|
72
389
|
}
|
|
73
390
|
}
|
|
74
|
-
|
|
75
|
-
|
|
391
|
+
finally {
|
|
392
|
+
mutationThreadLock.held = false;
|
|
76
393
|
}
|
|
77
394
|
}
|
|
78
395
|
}
|
|
79
396
|
/** Read PROJECT-DEFINITION.vbrief.json and return ``(data, path)``. */
|
|
80
397
|
export function loadProjectDefinitionForMutation(projectRoot) {
|
|
81
398
|
const path = projectDefinitionPath(projectRoot);
|
|
399
|
+
return [parseProjectDefinitionAt(path), path];
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Load and parse the PROJECT-DEFINITION at an already-captured path (#3796).
|
|
403
|
+
* Diagnostics name the artifact through {@link projectDefinitionArtifactLabel}
|
|
404
|
+
* so a configured path is never interpolated raw.
|
|
405
|
+
*/
|
|
406
|
+
export function parseProjectDefinitionAt(path) {
|
|
407
|
+
const label = projectDefinitionArtifactLabel(path);
|
|
82
408
|
if (!existsSync(path)) {
|
|
83
|
-
throw new ProjectDefinitionIOError(`PROJECT-DEFINITION not found at ${
|
|
409
|
+
throw new ProjectDefinitionIOError(`PROJECT-DEFINITION not found at ${label}; run task triage:welcome / ` +
|
|
84
410
|
"task triage:bootstrap to scaffold one first.");
|
|
85
411
|
}
|
|
86
412
|
let raw;
|
|
@@ -89,7 +415,7 @@ export function loadProjectDefinitionForMutation(projectRoot) {
|
|
|
89
415
|
}
|
|
90
416
|
catch (err) {
|
|
91
417
|
const msg = err instanceof Error ? err.message : String(err);
|
|
92
|
-
throw new ProjectDefinitionIOError(`Could not read PROJECT-DEFINITION at ${
|
|
418
|
+
throw new ProjectDefinitionIOError(`Could not read PROJECT-DEFINITION at ${label}: ${msg}`);
|
|
93
419
|
}
|
|
94
420
|
let data;
|
|
95
421
|
try {
|
|
@@ -97,12 +423,12 @@ export function loadProjectDefinitionForMutation(projectRoot) {
|
|
|
97
423
|
}
|
|
98
424
|
catch (err) {
|
|
99
425
|
const msg = err instanceof Error ? err.message : String(err);
|
|
100
|
-
throw new ProjectDefinitionIOError(`PROJECT-DEFINITION at ${
|
|
426
|
+
throw new ProjectDefinitionIOError(`PROJECT-DEFINITION at ${label} is not valid JSON: ${msg}`);
|
|
101
427
|
}
|
|
102
428
|
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
103
|
-
throw new ProjectDefinitionIOError(`PROJECT-DEFINITION at ${
|
|
429
|
+
throw new ProjectDefinitionIOError(`PROJECT-DEFINITION at ${label} top-level value is not a JSON object`);
|
|
104
430
|
}
|
|
105
|
-
return
|
|
431
|
+
return structuredClone(data);
|
|
106
432
|
}
|
|
107
433
|
/** Atomically write ``data`` to ``path`` as pretty-printed JSON. */
|
|
108
434
|
export function atomicWriteProjectDefinition(path, data) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type MutationLockDeps } from "./project-definition-io.js";
|
|
2
|
+
import type { JsonObject } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Capability handed to a PROJECT-DEFINITION mutation critical section (#3796).
|
|
5
|
+
*
|
|
6
|
+
* The artifact identity is captured once, when the lock is acquired, and every
|
|
7
|
+
* read and write goes through this object. That is what makes "every mutator
|
|
8
|
+
* uses the shared lock" checkable: a caller cannot resolve the path again, so it
|
|
9
|
+
* cannot lock one artifact and then load or persist a different one after a
|
|
10
|
+
* configured path or symlink is retargeted mid-section.
|
|
11
|
+
*/
|
|
12
|
+
export interface ProjectDefinitionMutation {
|
|
13
|
+
/** Artifact path captured at lock acquisition. Stable for the section. */
|
|
14
|
+
readonly artifactPath: string;
|
|
15
|
+
/** Control-safe label for this artifact in diagnostics. */
|
|
16
|
+
readonly artifactLabel: string;
|
|
17
|
+
/** Load and parse the captured artifact. Throws `ProjectDefinitionIOError`. */
|
|
18
|
+
load(): JsonObject;
|
|
19
|
+
/** Atomically persist `data` to the captured artifact under containment. */
|
|
20
|
+
persist(data: JsonObject): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Run `fn` inside the shared PROJECT-DEFINITION mutation lock with load, parse
|
|
24
|
+
* and persist bound to the artifact identity captured at acquisition.
|
|
25
|
+
*
|
|
26
|
+
* This is the only sanctioned mutation entry point. `verify:*`-adjacent
|
|
27
|
+
* inventory coverage in `project-definition-mutation-inventory.test.ts` fails
|
|
28
|
+
* closed when production code reaches for the raw resolver, the raw lock, or the
|
|
29
|
+
* raw write sink instead (#3796).
|
|
30
|
+
*/
|
|
31
|
+
export declare function withProjectDefinitionMutation<T>(projectRoot: string, fn: (mutation: ProjectDefinitionMutation) => T, deps?: MutationLockDeps): T;
|
|
32
|
+
//# sourceMappingURL=project-definition-mutation.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
import { assertWriteTargetSafe } from "../fs/projection-containment.js";
|
|
3
|
+
import { atomicWriteProjectDefinition, parseProjectDefinitionAt, projectDefinitionArtifactLabel, projectDefinitionMutationLock, } from "./project-definition-io.js";
|
|
4
|
+
/**
|
|
5
|
+
* Containment root for a persist.
|
|
6
|
+
*
|
|
7
|
+
* For an in-tree artifact this is the project root, so a force-added directory
|
|
8
|
+
* symlink anywhere on the way down (`xbrief/` and friends) fails closed before
|
|
9
|
+
* temp+rename -- the #3042 / #3077 guarantee. A `DEFT_PROJECT_PATH` override may
|
|
10
|
+
* legitimately resolve outside the project root, and refusing those writes would
|
|
11
|
+
* be a new restriction rather than a mutation-identity fix, so those fall back to
|
|
12
|
+
* the artifact's own directory and keep the leaf-symlink refusal.
|
|
13
|
+
*/
|
|
14
|
+
function containmentRootFor(projectRoot, artifactPath) {
|
|
15
|
+
const rel = relative(projectRoot, resolve(artifactPath));
|
|
16
|
+
const inTree = rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel);
|
|
17
|
+
return inTree ? projectRoot : dirname(resolve(artifactPath));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Run `fn` inside the shared PROJECT-DEFINITION mutation lock with load, parse
|
|
21
|
+
* and persist bound to the artifact identity captured at acquisition.
|
|
22
|
+
*
|
|
23
|
+
* This is the only sanctioned mutation entry point. `verify:*`-adjacent
|
|
24
|
+
* inventory coverage in `project-definition-mutation-inventory.test.ts` fails
|
|
25
|
+
* closed when production code reaches for the raw resolver, the raw lock, or the
|
|
26
|
+
* raw write sink instead (#3796).
|
|
27
|
+
*/
|
|
28
|
+
export function withProjectDefinitionMutation(projectRoot, fn, deps = {}) {
|
|
29
|
+
const root = resolve(projectRoot);
|
|
30
|
+
return projectDefinitionMutationLock(projectRoot, (artifactPath) => {
|
|
31
|
+
const mutation = {
|
|
32
|
+
artifactPath,
|
|
33
|
+
artifactLabel: projectDefinitionArtifactLabel(artifactPath),
|
|
34
|
+
load: () => parseProjectDefinitionAt(artifactPath),
|
|
35
|
+
persist: (data) => {
|
|
36
|
+
assertWriteTargetSafe(containmentRootFor(root, artifactPath), resolve(artifactPath));
|
|
37
|
+
atomicWriteProjectDefinition(artifactPath, data);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
return fn(mutation);
|
|
41
|
+
}, deps);
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=project-definition-mutation.js.map
|
|
@@ -21,5 +21,6 @@ export interface ConformanceEvaluateResult {
|
|
|
21
21
|
export declare function evaluateConformance(projectRoot: string, options?: {
|
|
22
22
|
mode?: ConformanceMode;
|
|
23
23
|
allowListPath?: string | null;
|
|
24
|
+
projectDefinitionPath?: string | null;
|
|
24
25
|
}): ConformanceEvaluateResult;
|
|
25
26
|
//# sourceMappingURL=conformance.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { GitCommandError, GitNotFoundError, gitStagedFiles, gitTrackedFiles, } from "../encoding/git.js";
|
|
4
4
|
import { fnmatchCase } from "../encoding/text.js";
|
|
@@ -71,8 +71,21 @@ export const ITEM_CORE = new Set([
|
|
|
71
71
|
// (accepted via EXTENSION_PREFIXES). Bare evidence/disposition fail #1620.
|
|
72
72
|
]);
|
|
73
73
|
export const EXTENSION_PREFIXES = ["x-directive/", "x-vbrief/", "x-xbrief/"];
|
|
74
|
+
const SAFE_FINDING_KEY = /^[A-Za-z][A-Za-z0-9._/-]{0,63}$/;
|
|
75
|
+
const SENSITIVE_FINDING_KEY = /(?:auth|credential|password|private|secret|token|api.?key)/i;
|
|
76
|
+
/**
|
|
77
|
+
* Bounded presentation of a raw finding key (#3796). `finding.key` stays exact
|
|
78
|
+
* for machine consumers; only this rendered form is sanitised, so a hostile or
|
|
79
|
+
* sensitive key cannot ride a stderr diagnostic out of the process.
|
|
80
|
+
*/
|
|
81
|
+
function renderFindingKey(key) {
|
|
82
|
+
return SAFE_FINDING_KEY.test(key) && !SENSITIVE_FINDING_KEY.test(key)
|
|
83
|
+
? key
|
|
84
|
+
: `<redacted-key length=${[...key].length}>`;
|
|
85
|
+
}
|
|
74
86
|
export function renderFinding(finding) {
|
|
75
|
-
return ` ${finding.path} [${finding.level}] bare key
|
|
87
|
+
return (` ${finding.path} [${finding.level}] bare key ` +
|
|
88
|
+
`'${renderFindingKey(finding.key)}' at ${finding.location}`);
|
|
76
89
|
}
|
|
77
90
|
function isConformant(key, core) {
|
|
78
91
|
if (core.has(key)) {
|
|
@@ -244,21 +257,85 @@ export function evaluateConformance(projectRoot, options = {}) {
|
|
|
244
257
|
}
|
|
245
258
|
const candidates = relPaths
|
|
246
259
|
.map((p) => p.replace(/\\/g, "/"))
|
|
247
|
-
.filter((posix) => isVbriefPath(posix) && !isAllowListed(posix, customGlobs))
|
|
260
|
+
.filter((posix) => isVbriefPath(posix) && !isAllowListed(posix, customGlobs))
|
|
261
|
+
.map((posix) => ({ displayPath: posix, fullPath: join(root, posix), configured: false }));
|
|
262
|
+
const configuredPath = options.projectDefinitionPath?.trim();
|
|
263
|
+
if (configuredPath) {
|
|
264
|
+
const lexicalPath = resolve(root, configuredPath);
|
|
265
|
+
if (!existsSync(lexicalPath)) {
|
|
266
|
+
return {
|
|
267
|
+
exitCode: 2,
|
|
268
|
+
findings: [],
|
|
269
|
+
message: "❌ verify_vbrief_conformance: configured PROJECT-DEFINITION does not exist.\n" +
|
|
270
|
+
" Recovery: fix or unset DEFT_PROJECT_PATH, then rerun conformance.",
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
let fullPath;
|
|
274
|
+
try {
|
|
275
|
+
fullPath = realpathSync(lexicalPath);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return {
|
|
279
|
+
exitCode: 2,
|
|
280
|
+
findings: [],
|
|
281
|
+
message: "❌ verify_vbrief_conformance: configured PROJECT-DEFINITION is unreadable.\n" +
|
|
282
|
+
" Recovery: fix its permissions or DEFT_PROJECT_PATH, then rerun conformance.",
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const existing = candidates.find((candidate) => {
|
|
286
|
+
try {
|
|
287
|
+
return realpathSync(candidate.fullPath) === fullPath;
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return resolve(candidate.fullPath) === fullPath;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
if (existing === undefined) {
|
|
294
|
+
candidates.push({
|
|
295
|
+
displayPath: "<configured PROJECT-DEFINITION>",
|
|
296
|
+
fullPath,
|
|
297
|
+
configured: true,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
existing.displayPath = "<configured PROJECT-DEFINITION>";
|
|
302
|
+
existing.fullPath = fullPath;
|
|
303
|
+
existing.configured = true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
248
306
|
const findings = [];
|
|
249
|
-
for (const
|
|
250
|
-
const full = join(root, posix);
|
|
307
|
+
for (const candidate of candidates) {
|
|
251
308
|
let text;
|
|
252
309
|
try {
|
|
253
|
-
text = readFileSync(
|
|
310
|
+
text = readFileSync(candidate.fullPath, "utf8");
|
|
254
311
|
}
|
|
255
312
|
catch {
|
|
313
|
+
if (candidate.configured) {
|
|
314
|
+
return {
|
|
315
|
+
exitCode: 2,
|
|
316
|
+
findings,
|
|
317
|
+
message: "❌ verify_vbrief_conformance: configured PROJECT-DEFINITION is unreadable.\n" +
|
|
318
|
+
" Recovery: fix its permissions or DEFT_PROJECT_PATH, then rerun conformance.",
|
|
319
|
+
};
|
|
320
|
+
}
|
|
256
321
|
continue;
|
|
257
322
|
}
|
|
323
|
+
let data;
|
|
258
324
|
try {
|
|
259
|
-
|
|
325
|
+
data = JSON.parse(text);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
if (candidate.configured) {
|
|
329
|
+
return {
|
|
330
|
+
exitCode: 2,
|
|
331
|
+
findings,
|
|
332
|
+
message: "❌ verify_vbrief_conformance: configured PROJECT-DEFINITION is not valid JSON.\n" +
|
|
333
|
+
" Recovery: repair the JSON, then rerun conformance.",
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
continue;
|
|
260
337
|
}
|
|
261
|
-
|
|
338
|
+
findings.push(...scanVbrief(candidate.displayPath, data));
|
|
262
339
|
}
|
|
263
340
|
if (findings.length > 0) {
|
|
264
341
|
const uniquePaths = new Set(findings.map((f) => f.path));
|