@deftai/directive-core 0.107.0 → 0.108.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/check/cached-orchestrator.js +28 -3
- package/dist/check/named-cause.js +18 -3
- package/dist/codebase/provider.js +7 -1
- package/dist/delivery-attempt/handoff.js +1 -1
- package/dist/design-critique/citation-grammar.d.ts +53 -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 +232 -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 +76 -0
- package/dist/hooks/classify/host-session-identity.js +470 -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 +7 -0
- package/dist/hooks/classify/paths.js +35 -0
- package/dist/hooks/classify/stdin.d.ts +5 -0
- package/dist/hooks/classify/stdin.js +27 -0
- package/dist/hooks/dispatcher.d.ts +52 -1
- package/dist/hooks/dispatcher.js +538 -45
- package/dist/hooks/fixtures/cases.d.ts +16 -2
- package/dist/hooks/fixtures/cases.js +175 -0
- package/dist/hooks/scope.js +3 -1
- package/dist/init-deposit/agent-hooks.d.ts +2 -1
- package/dist/init-deposit/agent-hooks.js +8 -1
- package/dist/init-deposit/hook-runtime-travel.d.ts +77 -0
- package/dist/init-deposit/hook-runtime-travel.js +269 -0
- package/dist/intake/clause-derivation.js +9 -3
- package/dist/intake/issue-ingest.js +62 -2
- package/dist/intake/platform-capabilities.d.ts +9 -2
- package/dist/intake/platform-capabilities.js +38 -9
- package/dist/orphan-active/evaluate.d.ts +25 -0
- package/dist/orphan-active/evaluate.js +193 -128
- package/dist/orphan-active/index.d.ts +1 -0
- package/dist/orphan-active/index.js +1 -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/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-protected-issues/gh.d.ts +6 -2
- package/dist/pr-protected-issues/gh.js +7 -3
- 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 +11 -0
- 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 +17 -7
- package/dist/release/cli-drift-report.d.ts +68 -0
- package/dist/release/cli-drift-report.js +189 -0
- package/dist/release/index.d.ts +1 -0
- package/dist/release/index.js +1 -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 +19 -0
- package/dist/release/types.d.ts +14 -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 +182 -1
- package/dist/review-monitor/lease-comment.d.ts +36 -0
- package/dist/review-monitor/lease-comment.js +103 -2
- package/dist/scm/call.js +12 -3
- 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 +9 -1
- package/dist/scm/gh-rest.js +103 -0
- package/dist/scm/readiness.d.ts +2 -0
- package/dist/scm/readiness.js +28 -4
- package/dist/scope/project-definition-sync.js +14 -7
- package/dist/session/git.d.ts +24 -0
- package/dist/session/git.js +138 -2
- package/dist/session/occupancy.d.ts +238 -12
- package/dist/session/occupancy.js +815 -29
- 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-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/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/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/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/project-definition-mutation-boundary.d.ts +104 -0
- package/dist/verify-source/project-definition-mutation-boundary.js +326 -0
- package/package.json +7 -3
|
@@ -9,20 +9,24 @@
|
|
|
9
9
|
* remedy without embedding env values.
|
|
10
10
|
*/
|
|
11
11
|
import { existsSync } from "node:fs";
|
|
12
|
-
import { join } from "node:path";
|
|
12
|
+
import { join, resolve } from "node:path";
|
|
13
13
|
import { allGatesCliDispatchable, GLOBAL_CLI_REMEDY, isCliNativeGate, } from "../check/cli-native-gates.js";
|
|
14
14
|
import { isFrameworkRepoRoot, isFrameworkSourceContext } from "../check/context.js";
|
|
15
15
|
import { CONSUMER_CHECK_GATES, checkGateId, FRAMEWORK_CHECK_GATES, } from "../check/gate-lists.js";
|
|
16
16
|
import { defaultWhich } from "../doctor/which.js";
|
|
17
|
+
import { packageManagerSourceLabel, resolveProjectPackageManager, } from "../resolution/package-manager.js";
|
|
18
|
+
/** Maintainer toolchain remains pnpm-based. Consumer tools are selected dynamically. */
|
|
17
19
|
export const TOOLCHAIN_PREFLIGHT_TOOLS = ["task", "pnpm", "node", "git"];
|
|
18
20
|
const REMEDY = {
|
|
19
21
|
task: "Install go-task: https://taskfile.dev/installation/ (e.g. winget install Task.Task / brew install go-task)",
|
|
22
|
+
npm: "Install or repair Node 20+ (npm is bundled), then re-run the consumer check",
|
|
20
23
|
pnpm: "Enable pnpm via corepack: corepack enable && corepack prepare pnpm@latest --activate",
|
|
21
|
-
node: "Install Node 20+ (see .nvmrc)
|
|
24
|
+
node: "Install Node 20+ (see .nvmrc)",
|
|
22
25
|
git: "Install Git: https://git-scm.com/downloads",
|
|
23
26
|
};
|
|
24
27
|
const CAUSE = {
|
|
25
28
|
task: "go-task binary not found on PATH",
|
|
29
|
+
npm: "npm binary not found on PATH",
|
|
26
30
|
pnpm: "pnpm binary not found on PATH",
|
|
27
31
|
node: "node binary not found on PATH",
|
|
28
32
|
git: "git binary not found on PATH",
|
|
@@ -32,20 +36,29 @@ const CAUSE = {
|
|
|
32
36
|
* When task/node is missing, the orchestrator skips *all* scheduled gates
|
|
33
37
|
* (dynamic from gate-lists), not this static list — avoids hardcode drift.
|
|
34
38
|
*/
|
|
35
|
-
export const
|
|
39
|
+
export const PACKAGE_MANAGER_DEPENDENT_GATE_IDS = [
|
|
36
40
|
"toolchain:check",
|
|
37
41
|
"toolchain:check-consumer",
|
|
38
42
|
"ts:check-lane",
|
|
39
43
|
];
|
|
44
|
+
/** @deprecated use PACKAGE_MANAGER_DEPENDENT_GATE_IDS. */
|
|
45
|
+
export const PNPM_DEPENDENT_GATE_IDS = PACKAGE_MANAGER_DEPENDENT_GATE_IDS;
|
|
40
46
|
/** Sentinel skip id meaning "every gate in the active check composition". */
|
|
41
47
|
export const SKIP_ALL_GATES = "*";
|
|
42
|
-
function probeTool(tool, which) {
|
|
48
|
+
function probeTool(tool, which, packageManager) {
|
|
43
49
|
const present = which(tool) !== null;
|
|
50
|
+
let remedy = REMEDY[tool];
|
|
51
|
+
if (tool === "node" && packageManager === "npm") {
|
|
52
|
+
remedy = "Install or repair Node 20+ (npm is bundled), then re-run the consumer check";
|
|
53
|
+
}
|
|
54
|
+
else if (tool === "node" && packageManager === "pnpm") {
|
|
55
|
+
remedy = "Install Node 20+ (see .nvmrc); then enable pnpm with Corepack";
|
|
56
|
+
}
|
|
44
57
|
return {
|
|
45
58
|
tool,
|
|
46
59
|
present,
|
|
47
60
|
cause: present ? null : CAUSE[tool],
|
|
48
|
-
remedy: present ? null :
|
|
61
|
+
remedy: present ? null : remedy,
|
|
49
62
|
};
|
|
50
63
|
}
|
|
51
64
|
function probeCliDist(frameworkRoot, which, exists) {
|
|
@@ -78,7 +91,8 @@ function formatFindingLine(finding) {
|
|
|
78
91
|
}
|
|
79
92
|
if (finding.impact === "none") {
|
|
80
93
|
const cause = finding.cause ?? "absent";
|
|
81
|
-
|
|
94
|
+
const remedy = finding.remedy === null ? "" : `; remedy: ${finding.remedy}`;
|
|
95
|
+
return `[deft preflight] ${finding.tool}: absent (impact: none) — ${cause}${remedy}`;
|
|
82
96
|
}
|
|
83
97
|
const cause = finding.cause ?? "missing";
|
|
84
98
|
const remedy = finding.remedy ?? "install the tool";
|
|
@@ -90,14 +104,17 @@ function inferConsumerDeposit(options) {
|
|
|
90
104
|
const project = options.projectRoot;
|
|
91
105
|
if (project === undefined)
|
|
92
106
|
return false;
|
|
93
|
-
// Same-path roots are framework-source only when the tree is the source repo.
|
|
94
|
-
if (isFrameworkRepoRoot(project))
|
|
95
|
-
return false;
|
|
96
107
|
const framework = options.frameworkRoot ?? project;
|
|
108
|
+
// session:start passes equal roots for both framework source and a consumer
|
|
109
|
+
// deposit. Equal roots identify maintainer mode only when the tree has the
|
|
110
|
+
// framework-source markers; otherwise this is the production consumer shape.
|
|
111
|
+
if (resolve(framework) === resolve(project)) {
|
|
112
|
+
return !isFrameworkRepoRoot(project);
|
|
113
|
+
}
|
|
97
114
|
return !isFrameworkSourceContext(framework, project);
|
|
98
115
|
}
|
|
99
116
|
/**
|
|
100
|
-
* Run done-gate toolchain preflight.
|
|
117
|
+
* Run done-gate toolchain preflight. Read-only probe — never installs or builds.
|
|
101
118
|
*/
|
|
102
119
|
export function runToolchainPreflight(options = {}) {
|
|
103
120
|
const which = options.which ?? defaultWhich;
|
|
@@ -106,16 +123,44 @@ export function runToolchainPreflight(options = {}) {
|
|
|
106
123
|
const composed = options.composedGates ?? (consumerDeposit ? CONSUMER_CHECK_GATES : FRAMEWORK_CHECK_GATES);
|
|
107
124
|
const allCli = allGatesCliDispatchable(composed);
|
|
108
125
|
const findings = [];
|
|
109
|
-
|
|
110
|
-
|
|
126
|
+
let packageManager = "pnpm";
|
|
127
|
+
let packageManagerSource = "maintainer";
|
|
128
|
+
let packageManagerSelectionFailed = false;
|
|
129
|
+
let selectedTools = TOOLCHAIN_PREFLIGHT_TOOLS;
|
|
130
|
+
if (consumerDeposit) {
|
|
131
|
+
const resolution = resolveProjectPackageManager({
|
|
132
|
+
projectRoot: options.projectRoot,
|
|
133
|
+
env: options.env,
|
|
134
|
+
});
|
|
135
|
+
if (resolution.ok) {
|
|
136
|
+
packageManager = resolution.packageManager;
|
|
137
|
+
packageManagerSource = resolution.source;
|
|
138
|
+
selectedTools = ["task", packageManager, "node", "git"];
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
packageManager = null;
|
|
142
|
+
packageManagerSource = null;
|
|
143
|
+
packageManagerSelectionFailed = true;
|
|
144
|
+
selectedTools = ["task", "node", "git"];
|
|
145
|
+
findings.push({
|
|
146
|
+
tool: "package_manager",
|
|
147
|
+
present: false,
|
|
148
|
+
cause: resolution.message,
|
|
149
|
+
remedy: "Set package.json#packageManager (or DEFT_PACKAGE_MANAGER) to a supported npm or pnpm value",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const tool of selectedTools) {
|
|
154
|
+
findings.push(probeTool(tool, which, packageManager));
|
|
111
155
|
}
|
|
112
156
|
if (options.probeCliDist !== false) {
|
|
113
157
|
findings.push(probeCliDist(options.frameworkRoot ?? options.projectRoot, which, exists));
|
|
114
158
|
}
|
|
115
159
|
const cliPresent = findings.some((f) => f.tool === "cli_dist" && f.present);
|
|
116
160
|
const taskMissing = findings.some((f) => f.tool === "task" && !f.present);
|
|
117
|
-
const pnpmMissing = findings.some((f) => f.tool === "pnpm" && !f.present);
|
|
118
161
|
const nodeMissing = findings.some((f) => f.tool === "node" && !f.present);
|
|
162
|
+
const packageManagerMissing = packageManagerSelectionFailed ||
|
|
163
|
+
(packageManager !== null && findings.some((f) => f.tool === packageManager && !f.present));
|
|
119
164
|
// #3335: in a deposit whose composed gates are all CLI-dispatchable, a
|
|
120
165
|
// missing task binary is impact none — no install-go-task remedy.
|
|
121
166
|
if (taskMissing && allCli) {
|
|
@@ -132,10 +177,30 @@ export function runToolchainPreflight(options = {}) {
|
|
|
132
177
|
};
|
|
133
178
|
}
|
|
134
179
|
}
|
|
180
|
+
// Keep finding impact aligned with the skip calculation. Git is advisory in
|
|
181
|
+
// this preflight, and a local CLI dist matters only when task is absent and
|
|
182
|
+
// every composed gate must dispatch through the CLI.
|
|
183
|
+
for (let index = 0; index < findings.length; index += 1) {
|
|
184
|
+
const finding = findings[index];
|
|
185
|
+
if (finding === undefined || finding.present)
|
|
186
|
+
continue;
|
|
187
|
+
if (finding.tool === "git") {
|
|
188
|
+
findings[index] = { ...finding, impact: "none" };
|
|
189
|
+
}
|
|
190
|
+
else if (finding.tool === "cli_dist") {
|
|
191
|
+
findings[index] = {
|
|
192
|
+
...finding,
|
|
193
|
+
impact: taskMissing && allCli && !cliPresent ? "degraded" : "none",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
135
197
|
const missingCritical = findings.filter((f) => {
|
|
136
198
|
if (f.present || f.impact === "none")
|
|
137
199
|
return false;
|
|
138
|
-
return f.tool === "task" ||
|
|
200
|
+
return (f.tool === "task" ||
|
|
201
|
+
f.tool === "node" ||
|
|
202
|
+
f.tool === "package_manager" ||
|
|
203
|
+
f.tool === packageManager);
|
|
139
204
|
});
|
|
140
205
|
const skip = new Set();
|
|
141
206
|
if (nodeMissing) {
|
|
@@ -156,8 +221,8 @@ export function runToolchainPreflight(options = {}) {
|
|
|
156
221
|
else if (taskMissing && allCli && !cliPresent) {
|
|
157
222
|
skip.add(SKIP_ALL_GATES);
|
|
158
223
|
}
|
|
159
|
-
else if (
|
|
160
|
-
for (const id of
|
|
224
|
+
else if (packageManagerMissing) {
|
|
225
|
+
for (const id of PACKAGE_MANAGER_DEPENDENT_GATE_IDS) {
|
|
161
226
|
skip.add(id);
|
|
162
227
|
}
|
|
163
228
|
}
|
|
@@ -166,6 +231,12 @@ export function runToolchainPreflight(options = {}) {
|
|
|
166
231
|
const status = ok ? "ok" : "degraded";
|
|
167
232
|
const lines = [];
|
|
168
233
|
lines.push(`[deft preflight] toolchain status: ${status}`);
|
|
234
|
+
if (consumerDeposit &&
|
|
235
|
+
packageManager !== null &&
|
|
236
|
+
packageManagerSource !== null &&
|
|
237
|
+
packageManagerSource !== "maintainer") {
|
|
238
|
+
lines.push(`[deft preflight] package manager: ${packageManager} (${packageManagerSourceLabel(packageManagerSource)})`);
|
|
239
|
+
}
|
|
169
240
|
for (const finding of findings) {
|
|
170
241
|
if (!finding.present) {
|
|
171
242
|
lines.push(formatFindingLine(finding));
|
|
@@ -173,8 +244,8 @@ export function runToolchainPreflight(options = {}) {
|
|
|
173
244
|
}
|
|
174
245
|
if (ok) {
|
|
175
246
|
lines.push(taskMissing && allCli
|
|
176
|
-
?
|
|
177
|
-
:
|
|
247
|
+
? `[deft preflight] done-gate toolchain ready (CLI dispatch; go-task not required; package manager: ${packageManager}) (#3335)`
|
|
248
|
+
: `[deft preflight] done-gate toolchain ready (task, ${packageManager}, node)`);
|
|
178
249
|
}
|
|
179
250
|
else {
|
|
180
251
|
lines.push("[deft preflight] degraded mode: directive check will skip toolchain-dependent gates " +
|
|
@@ -190,6 +261,8 @@ export function runToolchainPreflight(options = {}) {
|
|
|
190
261
|
findings,
|
|
191
262
|
lines,
|
|
192
263
|
skipGateIds: [...skip].sort(),
|
|
264
|
+
packageManager,
|
|
265
|
+
packageManagerSource,
|
|
193
266
|
};
|
|
194
267
|
}
|
|
195
268
|
/** Serialize preflight for run-summary / ritual-state (no env values). */
|
|
@@ -198,6 +271,8 @@ export function toolchainPreflightToDict(result) {
|
|
|
198
271
|
status: result.status,
|
|
199
272
|
ok: result.ok,
|
|
200
273
|
degraded: result.degraded,
|
|
274
|
+
package_manager: result.packageManager ?? null,
|
|
275
|
+
package_manager_source: result.packageManagerSource ?? null,
|
|
201
276
|
findings: result.findings.map((f) => ({
|
|
202
277
|
tool: f.tool,
|
|
203
278
|
present: f.present,
|
|
@@ -41,6 +41,11 @@ export interface VerifyResult {
|
|
|
41
41
|
* `rearm` when age/compact stale on an otherwise valid bind; `cold` otherwise.
|
|
42
42
|
*/
|
|
43
43
|
readonly recoveryTier?: SessionCeremonyTier | null;
|
|
44
|
+
/**
|
|
45
|
+
* Internal owner from the exact ritual state evaluated for this verdict.
|
|
46
|
+
* Absent until worktree/HEAD binding succeeds; never projected by emitVerifyJson.
|
|
47
|
+
*/
|
|
48
|
+
readonly boundSessionId?: string;
|
|
44
49
|
}
|
|
45
50
|
export type RitualRunner = (command: readonly string[], projectRoot: string) => {
|
|
46
51
|
code: number;
|
|
@@ -107,6 +112,12 @@ export interface InspectSessionRitualOptions {
|
|
|
107
112
|
readonly posture?: DirectivePosture;
|
|
108
113
|
readonly envPosture?: string | undefined;
|
|
109
114
|
readonly handoffText?: string | null;
|
|
115
|
+
/**
|
|
116
|
+
* Required gated steps for this surface. Defaults to all {@link GATED_STEPS}.
|
|
117
|
+
* Write dispatch passes {@link WRITE_GATED_REQUIRED_STEPS} so a no-write
|
|
118
|
+
* inspect reports the same readiness its verifying counterpart would (#3738).
|
|
119
|
+
*/
|
|
120
|
+
readonly requiredGatedSteps?: readonly GatedStepName[];
|
|
110
121
|
}
|
|
111
122
|
/**
|
|
112
123
|
* Read-only ritual-state inspection for host hooks.
|
|
@@ -136,7 +136,37 @@ function shouldRearmDriftProbe(payload, projectRoot, detect) {
|
|
|
136
136
|
return false;
|
|
137
137
|
return detect(projectRoot).inPlay;
|
|
138
138
|
}
|
|
139
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Persist a payload derived from `expected` only while the record on disk is
|
|
141
|
+
* still the one this verification read (#3769).
|
|
142
|
+
*
|
|
143
|
+
* A lease transfer mid-verification can re-arm the tree under a new owner. The
|
|
144
|
+
* losing session is denied by the occupancy recheck, but without this
|
|
145
|
+
* compare-and-swap its in-flight write would already have replaced the new
|
|
146
|
+
* owner record. Serializing ritual writes outright is the separate
|
|
147
|
+
* ritual-state locking item, deliberately out of scope.
|
|
148
|
+
*
|
|
149
|
+
* Returns null on success, or the reason the write was refused.
|
|
150
|
+
*/
|
|
151
|
+
function writeRitualStateIfStillOwned(projectRoot, payload, expected) {
|
|
152
|
+
const [current] = readRitualState(projectRoot);
|
|
153
|
+
if (current === null) {
|
|
154
|
+
return "session ritual state was removed while this verification was running";
|
|
155
|
+
}
|
|
156
|
+
if (current.sessionId !== expected.sessionId ||
|
|
157
|
+
current.startedAt.getTime() !== expected.startedAt.getTime()) {
|
|
158
|
+
return (`session ritual state was re-armed by ${current.sessionId ?? "<unbound>"} while this ` +
|
|
159
|
+
"verification was running; refusing to overwrite the current owner record");
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
writeRitualState(projectRoot, payload);
|
|
163
|
+
}
|
|
164
|
+
catch (exc) {
|
|
165
|
+
return String(exc);
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
function runGatedStep(projectRoot, payload, stepName, runner, now, expected, detect = detectWorkSelection) {
|
|
140
170
|
const command = resolveGatedEntrypointCommand(stepName, projectRoot, detect);
|
|
141
171
|
const { code, stdout, stderr } = runner(command, projectRoot);
|
|
142
172
|
const message = stdout.trim() || stderr.trim() || `${command[0]} exited ${code}`;
|
|
@@ -152,11 +182,9 @@ function runGatedStep(projectRoot, payload, stepName, runner, now, detect = dete
|
|
|
152
182
|
if (stepName === "cache_fresh") {
|
|
153
183
|
applyCacheFreshDriftProbeField(payload, command);
|
|
154
184
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
catch (exc) {
|
|
159
|
-
return `could not write session ritual state after ${stepName}: ${String(exc)}`;
|
|
185
|
+
const refusal = writeRitualStateIfStillOwned(projectRoot, payload, expected);
|
|
186
|
+
if (refusal !== null) {
|
|
187
|
+
return `could not write session ritual state after ${stepName}: ${refusal}`;
|
|
160
188
|
}
|
|
161
189
|
return null;
|
|
162
190
|
}
|
|
@@ -199,14 +227,11 @@ function evaluateLoadedState(projectRoot, state, input) {
|
|
|
199
227
|
return { code: 1, message: headDriftRecoveryMessage(), recoveryTier: "cold" };
|
|
200
228
|
}
|
|
201
229
|
if (input.rebindForwardHead) {
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
writeRitualState(projectRoot, payload);
|
|
205
|
-
}
|
|
206
|
-
catch (exc) {
|
|
230
|
+
const refusal = writeRitualStateIfStillOwned(projectRoot, { ...state.raw, git_head: currentHead }, { sessionId: state.sessionId, startedAt: state.startedAt });
|
|
231
|
+
if (refusal !== null) {
|
|
207
232
|
return {
|
|
208
233
|
code: 2,
|
|
209
|
-
message: `could not rebind session ritual git HEAD: ${
|
|
234
|
+
message: `could not rebind session ritual git HEAD: ${refusal}`,
|
|
210
235
|
recoveryTier: "cold",
|
|
211
236
|
};
|
|
212
237
|
}
|
|
@@ -218,6 +243,7 @@ function evaluateLoadedState(projectRoot, state, input) {
|
|
|
218
243
|
code: 2,
|
|
219
244
|
message: staleness.error ?? "session ritual staleness policy is invalid",
|
|
220
245
|
recoveryTier: "cold",
|
|
246
|
+
boundSessionId: state.sessionId,
|
|
221
247
|
};
|
|
222
248
|
}
|
|
223
249
|
const maxAgeMs = staleness.hours * 60 * 60 * 1000;
|
|
@@ -232,6 +258,7 @@ function evaluateLoadedState(projectRoot, state, input) {
|
|
|
232
258
|
message: `session ritual state is stale (older than ${staleness.hours}h).${compactNote} ` +
|
|
233
259
|
`Run \`${rearmCmd}\` to re-arm (or \`${coldCmd}\` for a full cold ceremony).`,
|
|
234
260
|
recoveryTier: "rearm",
|
|
261
|
+
boundSessionId: state.sessionId,
|
|
235
262
|
};
|
|
236
263
|
}
|
|
237
264
|
for (const stepName of QUICK_STEPS) {
|
|
@@ -241,6 +268,7 @@ function evaluateLoadedState(projectRoot, state, input) {
|
|
|
241
268
|
code: 1,
|
|
242
269
|
message: failedStepMessage("quick", stepName, step),
|
|
243
270
|
recoveryTier: "cold",
|
|
271
|
+
boundSessionId: state.sessionId,
|
|
244
272
|
};
|
|
245
273
|
}
|
|
246
274
|
}
|
|
@@ -249,10 +277,14 @@ function evaluateLoadedState(projectRoot, state, input) {
|
|
|
249
277
|
for (const stepName of requiredGated) {
|
|
250
278
|
const step = state.gatedSteps[stepName];
|
|
251
279
|
if (!stepPasses(step)) {
|
|
280
|
+
const recoveryTier = "cold";
|
|
252
281
|
return {
|
|
253
282
|
code: 1,
|
|
254
|
-
message: failedStepMessage("gated", stepName, step)
|
|
255
|
-
|
|
283
|
+
message: failedStepMessage("gated", stepName, step) +
|
|
284
|
+
" " +
|
|
285
|
+
formatRitualRecoveryInstruction(recoveryTier),
|
|
286
|
+
recoveryTier,
|
|
287
|
+
boundSessionId: state.sessionId,
|
|
256
288
|
};
|
|
257
289
|
}
|
|
258
290
|
}
|
|
@@ -261,6 +293,7 @@ function evaluateLoadedState(projectRoot, state, input) {
|
|
|
261
293
|
code: 0,
|
|
262
294
|
message: `OK session ritual ${input.tier} tier is fresh.`,
|
|
263
295
|
recoveryTier: null,
|
|
296
|
+
boundSessionId: state.sessionId,
|
|
264
297
|
};
|
|
265
298
|
}
|
|
266
299
|
/**
|
|
@@ -335,6 +368,7 @@ export function inspectSessionRitual(projectRoot, options = {}) {
|
|
|
335
368
|
now: options.now ?? new Date(),
|
|
336
369
|
runGit: options.runGit,
|
|
337
370
|
rebindForwardHead: false,
|
|
371
|
+
requiredGatedSteps: options.requiredGatedSteps,
|
|
338
372
|
});
|
|
339
373
|
return {
|
|
340
374
|
code: evaluated.code,
|
|
@@ -346,6 +380,7 @@ export function inspectSessionRitual(projectRoot, options = {}) {
|
|
|
346
380
|
posture,
|
|
347
381
|
ritualStateRequired,
|
|
348
382
|
recoveryTier: evaluated.recoveryTier,
|
|
383
|
+
...(evaluated.boundSessionId === undefined ? {} : { boundSessionId: evaluated.boundSessionId }),
|
|
349
384
|
};
|
|
350
385
|
}
|
|
351
386
|
export function verifySessionRitual(projectRoot, options = {}) {
|
|
@@ -440,6 +475,9 @@ export function verifySessionRitual(projectRoot, options = {}) {
|
|
|
440
475
|
posture,
|
|
441
476
|
ritualStateRequired,
|
|
442
477
|
recoveryTier: precheck.recoveryTier,
|
|
478
|
+
...(precheck.boundSessionId === undefined
|
|
479
|
+
? {}
|
|
480
|
+
: { boundSessionId: precheck.boundSessionId }),
|
|
443
481
|
};
|
|
444
482
|
}
|
|
445
483
|
const reloadedAfterPrecheck = readRitualState(projectRoot);
|
|
@@ -458,6 +496,9 @@ export function verifySessionRitual(projectRoot, options = {}) {
|
|
|
458
496
|
};
|
|
459
497
|
}
|
|
460
498
|
const payload = { ...state.raw };
|
|
499
|
+
// Identity of the record this verification is amending; a mid-flight lease
|
|
500
|
+
// transfer must not let these writes land on a newer owner (#3769).
|
|
501
|
+
const ownerAtVerify = { sessionId: state.sessionId, startedAt: state.startedAt };
|
|
461
502
|
const gated = { ...payload.gated_steps };
|
|
462
503
|
payload.gated_steps = gated;
|
|
463
504
|
const runCmd = options.runner ?? defaultRitualRunner;
|
|
@@ -477,7 +518,7 @@ export function verifySessionRitual(projectRoot, options = {}) {
|
|
|
477
518
|
if (stepName !== "agent_hooks" && stepPasses(step) && !forced.has(stepName) && !rearmDrift) {
|
|
478
519
|
continue;
|
|
479
520
|
}
|
|
480
|
-
const writeError = runGatedStep(projectRoot, payload, stepName, runCmd, instant, detect);
|
|
521
|
+
const writeError = runGatedStep(projectRoot, payload, stepName, runCmd, instant, ownerAtVerify, detect);
|
|
481
522
|
if (writeError !== null) {
|
|
482
523
|
return {
|
|
483
524
|
code: 2,
|
|
@@ -556,6 +597,9 @@ export function verifySessionRitual(projectRoot, options = {}) {
|
|
|
556
597
|
posture,
|
|
557
598
|
ritualStateRequired,
|
|
558
599
|
recoveryTier: evaluated.recoveryTier,
|
|
600
|
+
...(evaluated.boundSessionId === undefined
|
|
601
|
+
? {}
|
|
602
|
+
: { boundSessionId: evaluated.boundSessionId }),
|
|
559
603
|
};
|
|
560
604
|
}
|
|
561
605
|
return {
|
|
@@ -568,6 +612,7 @@ export function verifySessionRitual(projectRoot, options = {}) {
|
|
|
568
612
|
posture,
|
|
569
613
|
ritualStateRequired,
|
|
570
614
|
recoveryTier: evaluated.recoveryTier,
|
|
615
|
+
...(evaluated.boundSessionId === undefined ? {} : { boundSessionId: evaluated.boundSessionId }),
|
|
571
616
|
};
|
|
572
617
|
}
|
|
573
618
|
export function emitVerifyJson(result) {
|
|
@@ -10,7 +10,7 @@ export declare const REQUIRED_FIELDS: readonly ["slice_id", "umbrella", "umbrell
|
|
|
10
10
|
export declare const OPTIONAL_FIELDS: readonly ["notes"];
|
|
11
11
|
export declare const ALLOWED_FIELDS: Set<"actor" | "slice_id" | "umbrella" | "umbrella_url" | "sliced_at" | "children" | "expected_close_signal" | "notes">;
|
|
12
12
|
export declare const CHILD_REQUIRED_FIELDS: readonly ["n", "url", "wave", "role"];
|
|
13
|
-
export declare const CHILD_ALLOWED_FIELDS: Set<"n" | "
|
|
13
|
+
export declare const CHILD_ALLOWED_FIELDS: Set<"n" | "role" | "url" | "wave">;
|
|
14
14
|
export declare const UUID_RE: RegExp;
|
|
15
15
|
export declare const ISO8601_RE: RegExp;
|
|
16
16
|
export declare const WAVE_FLAG_RE: RegExp;
|
|
@@ -4,10 +4,18 @@ export declare const GENERATED_SPEC_SOURCE_SPEC = "<!-- Source of truth: vbrief/
|
|
|
4
4
|
export declare const GENERATED_SPEC_SOURCE_SPEC_XBRIEF = "<!-- Source of truth: xbrief/specification.xbrief.json -->";
|
|
5
5
|
export declare const GENERATED_SPEC_SOURCE_PD = "<!-- Source of truth: vbrief/PROJECT-DEFINITION.vbrief.json -->";
|
|
6
6
|
export declare const GENERATED_SPEC_SOURCE_PD_XBRIEF = "<!-- Source of truth: xbrief/PROJECT-DEFINITION.xbrief.json -->";
|
|
7
|
-
/**
|
|
8
|
-
export declare function
|
|
7
|
+
/** Return decoded non-PROJECT-DEFINITION source markers from generated markdown. */
|
|
8
|
+
export declare function generatedSpecSourcePaths(content: string): string[];
|
|
9
|
+
/** True when markdown names the expected generated-spec source artifact. */
|
|
10
|
+
export declare function contentHasGeneratedSpecSource(content: string, expectedSourcePath: string): boolean;
|
|
9
11
|
/** True when markdown carries a generated greenfield PD source marker for either layout. */
|
|
10
12
|
export declare function contentHasGeneratedPdSource(content: string): boolean;
|
|
13
|
+
/** Format a source path for a generated artifact banner. */
|
|
14
|
+
export declare function generatedSourcePath(sourcePath: string): string;
|
|
15
|
+
/** Build a canonical four-line generated-artifact banner from the actual source path. */
|
|
16
|
+
export declare function buildGeneratedArtifactBanner(generator: string, purpose: string, sourcePath: string): string;
|
|
17
|
+
/** Build the greenfield project export banner for the resolved PROJECT-DEFINITION path. */
|
|
18
|
+
export declare function buildExportSpecPdBanner(projectDefPath: string): string;
|
|
11
19
|
export declare const EXPORT_SPEC_PD_BANNER: string;
|
|
12
20
|
/** Narratives excluded from stakeholder SPEC/PRD exports (#2013 Wave 0 §5). */
|
|
13
21
|
export declare const STAKEHOLDER_EXCLUDED_NARRATIVE_KEYS: Set<string>;
|
|
@@ -1,22 +1,67 @@
|
|
|
1
|
+
import { basename, dirname, normalize, sep } from "node:path";
|
|
1
2
|
/** Generated-spec banner tokens (lockstep with Python ``_precutover.py``). */
|
|
2
3
|
export const GENERATED_SPEC_PURPOSE = "<!-- Purpose: rendered specification -->";
|
|
3
4
|
export const GENERATED_SPEC_SOURCE_SPEC = "<!-- Source of truth: vbrief/specification.vbrief.json -->";
|
|
4
5
|
export const GENERATED_SPEC_SOURCE_SPEC_XBRIEF = "<!-- Source of truth: xbrief/specification.xbrief.json -->";
|
|
5
6
|
export const GENERATED_SPEC_SOURCE_PD = "<!-- Source of truth: vbrief/PROJECT-DEFINITION.vbrief.json -->";
|
|
6
7
|
export const GENERATED_SPEC_SOURCE_PD_XBRIEF = "<!-- Source of truth: xbrief/PROJECT-DEFINITION.xbrief.json -->";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const GENERATED_SOURCE_MARKER = /^<!-- Source of truth: (.+?) -->\r?$/gm;
|
|
9
|
+
const PROJECT_DEFINITION_SOURCE = /(?:^|\/)PROJECT-DEFINITION\.(?:vbrief|xbrief)\.json$/i;
|
|
10
|
+
/** Return decoded non-PROJECT-DEFINITION source markers from generated markdown. */
|
|
11
|
+
export function generatedSpecSourcePaths(content) {
|
|
12
|
+
const sources = [];
|
|
13
|
+
for (const match of content.matchAll(GENERATED_SOURCE_MARKER)) {
|
|
14
|
+
let source;
|
|
15
|
+
try {
|
|
16
|
+
source = match[1] ? decodeURI(match[1]) : undefined;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// A generated source path is URI-encoded by the banner writer. Reject
|
|
20
|
+
// malformed escapes instead of guessing at a filesystem path.
|
|
21
|
+
}
|
|
22
|
+
if (source && !PROJECT_DEFINITION_SOURCE.test(source))
|
|
23
|
+
sources.push(source);
|
|
24
|
+
}
|
|
25
|
+
return sources;
|
|
26
|
+
}
|
|
27
|
+
/** True when markdown names the expected generated-spec source artifact. */
|
|
28
|
+
export function contentHasGeneratedSpecSource(content, expectedSourcePath) {
|
|
29
|
+
const expected = generatedSourcePath(expectedSourcePath);
|
|
30
|
+
return generatedSpecSourcePaths(content).includes(expected);
|
|
11
31
|
}
|
|
12
32
|
/** True when markdown carries a generated greenfield PD source marker for either layout. */
|
|
13
33
|
export function contentHasGeneratedPdSource(content) {
|
|
14
34
|
return (content.includes(GENERATED_SPEC_SOURCE_PD) || content.includes(GENERATED_SPEC_SOURCE_PD_XBRIEF));
|
|
15
35
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
36
|
+
/** Format a source path for a generated artifact banner. */
|
|
37
|
+
export function generatedSourcePath(sourcePath) {
|
|
38
|
+
const parent = basename(dirname(sourcePath));
|
|
39
|
+
if (parent === "xbrief" || parent === "vbrief") {
|
|
40
|
+
return `${parent}/${basename(sourcePath)}`;
|
|
41
|
+
}
|
|
42
|
+
return normalize(sourcePath).split(sep).join("/");
|
|
43
|
+
}
|
|
44
|
+
function generatedBannerValue(value) {
|
|
45
|
+
return value.replace(/\r\n?|\n/g, " ").replaceAll("-->", "-->");
|
|
46
|
+
}
|
|
47
|
+
/** Build a canonical four-line generated-artifact banner from the actual source path. */
|
|
48
|
+
export function buildGeneratedArtifactBanner(generator, purpose, sourcePath) {
|
|
49
|
+
const safeGenerator = generatedBannerValue(generator);
|
|
50
|
+
const safePurpose = generatedBannerValue(purpose);
|
|
51
|
+
// Source paths must be reversible so an explicit --spec artifact can be
|
|
52
|
+
// bound back to the resolved authority. URI encoding keeps CR/LF and an
|
|
53
|
+
// embedded comment closer on one safe line without losing path identity.
|
|
54
|
+
const safeSourcePath = encodeURI(generatedSourcePath(sourcePath));
|
|
55
|
+
return (`<!-- AUTO-GENERATED by ${safeGenerator} -- DO NOT EDIT MANUALLY -->\n` +
|
|
56
|
+
`<!-- Purpose: ${safePurpose} -->\n` +
|
|
57
|
+
`<!-- Source of truth: ${safeSourcePath} -->\n` +
|
|
58
|
+
`<!-- Regenerate with: ${safeGenerator} -->\n`);
|
|
59
|
+
}
|
|
60
|
+
/** Build the greenfield project export banner for the resolved PROJECT-DEFINITION path. */
|
|
61
|
+
export function buildExportSpecPdBanner(projectDefPath) {
|
|
62
|
+
return buildGeneratedArtifactBanner("task project:export-spec", "rendered specification", projectDefPath);
|
|
63
|
+
}
|
|
64
|
+
export const EXPORT_SPEC_PD_BANNER = buildExportSpecPdBanner("xbrief/PROJECT-DEFINITION.xbrief.json");
|
|
20
65
|
/** Narratives excluded from stakeholder SPEC/PRD exports (#2013 Wave 0 §5). */
|
|
21
66
|
export const STAKEHOLDER_EXCLUDED_NARRATIVE_KEYS = new Set([
|
|
22
67
|
"projectconfig",
|
|
@@ -5,10 +5,13 @@ export interface ResolvedSpecAuthority {
|
|
|
5
5
|
readonly vbriefDir: string;
|
|
6
6
|
readonly projectDefPath: string;
|
|
7
7
|
readonly specPath: string | null;
|
|
8
|
+
readonly sourcePath: string;
|
|
8
9
|
readonly banner: string;
|
|
9
10
|
}
|
|
10
11
|
export declare function resolveSpecAuthority(projectRoot: string): ResolvedSpecAuthority | null;
|
|
11
12
|
export declare function readSpecMarkdown(projectRoot: string): string;
|
|
13
|
+
/** True when the markdown source marker resolves to the project's specification authority. */
|
|
14
|
+
export declare function contentHasResolvedSpecSource(projectRoot: string, content: string): boolean;
|
|
12
15
|
export declare function isFullSpecState(projectRoot: string): boolean;
|
|
13
16
|
export declare function isGreenfieldSpecExport(projectRoot: string): boolean;
|
|
14
17
|
export declare function isCurrentGeneratedSpecification(projectRoot: string): boolean;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { contentHasGeneratedPdSource, contentHasGeneratedSpecSource,
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
|
+
import { resolveLifecycleLayout, resolveLifecycleRoot, resolveProjectDefinitionPath, resolveSpecArtifactPath, } from "../layout/resolve.js";
|
|
5
|
+
import { buildExportSpecPdBanner, buildGeneratedArtifactBanner, contentHasGeneratedPdSource, contentHasGeneratedSpecSource, GENERATED_SPEC_PURPOSE, generatedSpecSourcePaths, } from "./constants.js";
|
|
6
6
|
export function resolveSpecAuthority(projectRoot) {
|
|
7
7
|
const root = projectRoot;
|
|
8
8
|
let layout;
|
|
@@ -20,13 +20,17 @@ export function resolveSpecAuthority(projectRoot) {
|
|
|
20
20
|
const specPath = join(vbriefDir, `specification${layout.artifactSuffix}`);
|
|
21
21
|
const hasSpec = existsSync(specPath);
|
|
22
22
|
const kind = hasSpec ? "full-spec" : "greenfield";
|
|
23
|
-
const
|
|
23
|
+
const sourcePath = hasSpec ? specPath : projectDefPath;
|
|
24
|
+
const banner = hasSpec
|
|
25
|
+
? buildGeneratedArtifactBanner("task spec:render", "rendered specification", sourcePath)
|
|
26
|
+
: buildExportSpecPdBanner(sourcePath);
|
|
24
27
|
return {
|
|
25
28
|
kind,
|
|
26
29
|
projectRoot: root,
|
|
27
30
|
vbriefDir,
|
|
28
31
|
projectDefPath,
|
|
29
32
|
specPath: hasSpec ? specPath : null,
|
|
33
|
+
sourcePath,
|
|
30
34
|
banner,
|
|
31
35
|
};
|
|
32
36
|
}
|
|
@@ -39,12 +43,56 @@ export function readSpecMarkdown(projectRoot) {
|
|
|
39
43
|
return "";
|
|
40
44
|
}
|
|
41
45
|
}
|
|
46
|
+
function sourcePathFromMarker(projectRoot, source) {
|
|
47
|
+
return isAbsolute(source) ? source : resolve(projectRoot, source);
|
|
48
|
+
}
|
|
49
|
+
function contentMatchesResolvedSpecSource(projectRoot, content, resolvedSourcePath) {
|
|
50
|
+
if (contentHasGeneratedSpecSource(content, resolvedSourcePath))
|
|
51
|
+
return true;
|
|
52
|
+
let resolvedSource;
|
|
53
|
+
try {
|
|
54
|
+
resolvedSource = JSON.parse(readFileSync(resolvedSourcePath, "utf8"));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
for (const source of generatedSpecSourcePaths(content)) {
|
|
60
|
+
try {
|
|
61
|
+
const candidate = sourcePathFromMarker(projectRoot, source);
|
|
62
|
+
if (!statSync(candidate).isFile())
|
|
63
|
+
continue;
|
|
64
|
+
// Explicit --spec paths remain valid aliases only when they identify the
|
|
65
|
+
// same JSON authority as the layout-resolved specification artifact.
|
|
66
|
+
if (isDeepStrictEqual(JSON.parse(readFileSync(candidate, "utf8")), resolvedSource)) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Missing or unreadable explicit source markers are not resolved authority.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
/** True when the markdown source marker resolves to the project's specification authority. */
|
|
77
|
+
export function contentHasResolvedSpecSource(projectRoot, content) {
|
|
78
|
+
let resolvedSourcePath;
|
|
79
|
+
try {
|
|
80
|
+
resolvedSourcePath = resolveSpecArtifactPath(projectRoot);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (!existsSync(resolvedSourcePath))
|
|
86
|
+
return false;
|
|
87
|
+
return contentMatchesResolvedSpecSource(projectRoot, content, resolvedSourcePath);
|
|
88
|
+
}
|
|
42
89
|
export function isFullSpecState(projectRoot) {
|
|
43
90
|
const authority = resolveSpecAuthority(projectRoot);
|
|
44
91
|
if (authority?.kind !== "full-spec")
|
|
45
92
|
return false;
|
|
46
93
|
const specMd = readSpecMarkdown(projectRoot);
|
|
47
|
-
return specMd.includes(GENERATED_SPEC_PURPOSE) &&
|
|
94
|
+
return (specMd.includes(GENERATED_SPEC_PURPOSE) &&
|
|
95
|
+
contentMatchesResolvedSpecSource(projectRoot, specMd, authority.sourcePath));
|
|
48
96
|
}
|
|
49
97
|
export function isGreenfieldSpecExport(projectRoot) {
|
|
50
98
|
const authority = resolveSpecAuthority(projectRoot);
|
|
@@ -15,4 +15,18 @@
|
|
|
15
15
|
* so an overflow is never silent again.
|
|
16
16
|
*/
|
|
17
17
|
export declare const SUBPROCESS_MAX_BUFFER: number;
|
|
18
|
+
/**
|
|
19
|
+
* Reason text for a failed capture, so an overflow is never blank (#1867 / #3903).
|
|
20
|
+
*
|
|
21
|
+
* A spawn-level failure (ENOBUFS past the ceiling, timeout kill, missing
|
|
22
|
+
* binary) reports no exit status and empty stderr, whether it arrives as a
|
|
23
|
+
* `spawnSync` result or as a thrown `execFileSync` error. Only then does the
|
|
24
|
+
* error message stand in for stderr: a process that exited with a status and
|
|
25
|
+
* chose to say nothing keeps its empty stderr.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveCaptureFailureStderr(capture: {
|
|
28
|
+
readonly captured: string;
|
|
29
|
+
readonly status: number | null | undefined;
|
|
30
|
+
readonly message: string | undefined;
|
|
31
|
+
}): string;
|
|
18
32
|
//# sourceMappingURL=max-buffer.d.ts.map
|