@deftai/directive-core 0.99.0 → 0.101.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 +6 -0
- package/dist/authz/actions.js +3 -0
- package/dist/authz/classify.js +241 -30
- package/dist/authz/decompose-apply.d.ts +81 -0
- package/dist/authz/decompose-apply.js +377 -0
- package/dist/authz/index.d.ts +3 -1
- package/dist/authz/index.js +3 -1
- package/dist/authz/store.d.ts +53 -0
- package/dist/authz/store.js +219 -1
- package/dist/authz/types.d.ts +19 -2
- package/dist/authz/types.js +5 -1
- package/dist/check/cached-orchestrator.d.ts +26 -5
- package/dist/check/cached-orchestrator.js +228 -6
- package/dist/check/gate-lists.d.ts +10 -4
- package/dist/check/gate-lists.js +19 -5
- package/dist/check/index.d.ts +3 -2
- package/dist/check/index.js +2 -1
- package/dist/check/named-cause.d.ts +45 -0
- package/dist/check/named-cause.js +121 -0
- package/dist/check/orchestrator.js +6 -1
- package/dist/doctor/checks.d.ts +44 -0
- package/dist/doctor/checks.js +428 -2
- package/dist/doctor/help.d.ts +5 -0
- package/dist/doctor/help.js +31 -0
- package/dist/doctor/index.d.ts +2 -0
- package/dist/doctor/index.js +2 -0
- package/dist/doctor/main.d.ts +6 -7
- package/dist/doctor/main.js +130 -79
- package/dist/doctor/session-coda.d.ts +82 -0
- package/dist/doctor/session-coda.js +151 -0
- package/dist/doctor/types.d.ts +16 -0
- package/dist/doctor/which.d.ts +20 -1
- package/dist/doctor/which.js +67 -1
- package/dist/finish-loop/pr-finish-loop.d.ts +13 -2
- package/dist/finish-loop/pr-finish-loop.js +116 -5
- package/dist/finish-loop/types.d.ts +1 -1
- package/dist/hooks/dispatcher.d.ts +9 -5
- package/dist/hooks/dispatcher.js +23 -15
- package/dist/hooks/readonly.d.ts +6 -3
- package/dist/hooks/readonly.js +44 -25
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/init-deposit/agent-hooks.d.ts +20 -0
- package/dist/init-deposit/agent-hooks.js +51 -31
- package/dist/init-deposit/gitignore.js +2 -0
- package/dist/intake/issue-ingest.js +36 -0
- package/dist/lifecycle/completed-consistency.d.ts +60 -0
- package/dist/lifecycle/completed-consistency.js +443 -0
- package/dist/lifecycle/completed-tracked-on-delivery.d.ts +47 -0
- package/dist/lifecycle/completed-tracked-on-delivery.js +415 -0
- package/dist/lifecycle/index.d.ts +2 -0
- package/dist/lifecycle/index.js +2 -0
- package/dist/literal-acceptance/capture.d.ts +52 -0
- package/dist/literal-acceptance/capture.js +658 -0
- package/dist/literal-acceptance/evaluate.d.ts +47 -0
- package/dist/literal-acceptance/evaluate.js +193 -0
- package/dist/literal-acceptance/index.d.ts +12 -0
- package/dist/literal-acceptance/index.js +12 -0
- package/dist/literal-acceptance/run.d.ts +38 -0
- package/dist/literal-acceptance/run.js +216 -0
- package/dist/literal-acceptance/safety.d.ts +24 -0
- package/dist/literal-acceptance/safety.js +251 -0
- package/dist/literal-acceptance/types.d.ts +82 -0
- package/dist/literal-acceptance/types.js +26 -0
- package/dist/policy/ac-pass-banking.d.ts +58 -0
- package/dist/policy/ac-pass-banking.js +150 -0
- package/dist/policy/ceremony-dial.d.ts +96 -11
- package/dist/policy/ceremony-dial.js +250 -23
- package/dist/policy/index.d.ts +2 -0
- package/dist/policy/index.js +16 -1
- package/dist/policy/merge-approval-head.d.ts +129 -0
- package/dist/policy/merge-approval-head.js +461 -0
- package/dist/policy/require-human-merge.d.ts +5 -0
- package/dist/policy/require-human-merge.js +5 -0
- package/dist/pr-merge-readiness/ci-gate.d.ts +20 -3
- package/dist/pr-merge-readiness/ci-gate.js +65 -5
- package/dist/pr-merge-readiness/compute.d.ts +13 -0
- package/dist/pr-merge-readiness/compute.js +99 -14
- package/dist/pr-merge-readiness/gh.d.ts +49 -0
- package/dist/pr-merge-readiness/gh.js +268 -0
- package/dist/pr-merge-readiness/index.d.ts +2 -2
- package/dist/pr-merge-readiness/index.js +2 -2
- package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +4 -0
- package/dist/pr-wait-mergeable/cascade.d.ts +11 -0
- package/dist/pr-wait-mergeable/cascade.js +115 -1
- package/dist/pr-wait-mergeable/main.d.ts +4 -0
- package/dist/pr-wait-mergeable/main.js +4 -0
- package/dist/pr-wait-mergeable/types.d.ts +3 -1
- package/dist/pr-wait-mergeable/wrappers.d.ts +6 -1
- package/dist/pr-wait-mergeable/wrappers.js +7 -1
- package/dist/preflight/evaluate.d.ts +15 -1
- package/dist/preflight/evaluate.js +33 -2
- package/dist/preflight-cache/evaluate.d.ts +3 -0
- package/dist/preflight-cache/evaluate.js +3 -0
- package/dist/product-first-done-gate/acceptance.d.ts +25 -0
- package/dist/product-first-done-gate/acceptance.js +247 -0
- package/dist/product-first-done-gate/check-mode.d.ts +45 -0
- package/dist/product-first-done-gate/check-mode.js +148 -0
- package/dist/product-first-done-gate/evaluate.d.ts +50 -0
- package/dist/product-first-done-gate/evaluate.js +255 -0
- package/dist/product-first-done-gate/index.d.ts +11 -0
- package/dist/product-first-done-gate/index.js +11 -0
- package/dist/product-first-done-gate/types.d.ts +64 -0
- package/dist/product-first-done-gate/types.js +61 -0
- package/dist/render/framework-commands.js +4 -0
- package/dist/run-summary/emit.d.ts +56 -0
- package/dist/run-summary/emit.js +171 -0
- package/dist/run-summary/index.d.ts +4 -0
- package/dist/run-summary/index.js +4 -0
- package/dist/run-summary/path.d.ts +25 -0
- package/dist/run-summary/path.js +78 -0
- package/dist/run-summary/types.d.ts +76 -0
- package/dist/run-summary/types.js +21 -0
- package/dist/scope/acceptance-evidence.d.ts +110 -0
- package/dist/scope/acceptance-evidence.js +422 -0
- package/dist/scope/coverage-map.d.ts +98 -0
- package/dist/scope/coverage-map.js +558 -0
- package/dist/scope/decompose.d.ts +5 -0
- package/dist/scope/decompose.js +134 -11
- package/dist/scope/index.d.ts +3 -0
- package/dist/scope/index.js +3 -0
- package/dist/scope/parent-lineage.d.ts +125 -0
- package/dist/scope/parent-lineage.js +626 -0
- package/dist/scope/transition.d.ts +8 -0
- package/dist/scope/transition.js +61 -2
- package/dist/session/ac-pass-banking.d.ts +221 -0
- package/dist/session/ac-pass-banking.js +761 -0
- package/dist/session/active-cli.d.ts +79 -0
- package/dist/session/active-cli.js +382 -0
- package/dist/session/deposit-sha.d.ts +49 -0
- package/dist/session/deposit-sha.js +121 -0
- package/dist/session/effort-budget.d.ts +136 -0
- package/dist/session/effort-budget.js +352 -0
- package/dist/session/index.d.ts +7 -0
- package/dist/session/index.js +7 -0
- package/dist/session/orientation-compression.d.ts +127 -0
- package/dist/session/orientation-compression.js +425 -0
- package/dist/session/orientation-state.d.ts +29 -0
- package/dist/session/orientation-state.js +85 -0
- package/dist/session/session-start.d.ts +31 -0
- package/dist/session/session-start.js +228 -2
- package/dist/session/toolchain-preflight.d.ts +64 -0
- package/dist/session/toolchain-preflight.js +151 -0
- package/dist/session/verify-session-ritual.d.ts +14 -0
- package/dist/session/verify-session-ritual.js +33 -0
- package/dist/story-ready/evaluate.d.ts +11 -0
- package/dist/story-ready/evaluate.js +41 -3
- package/dist/triage/actions/candidates-log.d.ts +1 -2
- package/dist/triage/actions/candidates-log.js +37 -6
- package/dist/triage/bootstrap/gitignore.d.ts +1 -1
- package/dist/triage/bootstrap/gitignore.js +8 -2
- package/dist/triage/bootstrap/index.js +15 -3
- package/dist/triage/classify/index.d.ts +1 -0
- package/dist/triage/classify/index.js +2 -0
- package/dist/triage/classify/label-mirror.js +16 -0
- package/dist/triage/classify/mirror-discovery-tip.d.ts +95 -0
- package/dist/triage/classify/mirror-discovery-tip.js +234 -0
- package/dist/triage/reconcile/reconcile.js +28 -6
- package/dist/triage/summary/index.js +12 -4
- package/dist/triage/welcome/default-mode.js +16 -0
- package/dist/ts-check-lane/run-lane.js +21 -4
- package/dist/vbrief-validate/conformance.js +7 -0
- package/dist/vitest-runner/coverage-debt-teardown.js +16 -4
- package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
- package/dist/xbrief-migrate/migrate-project.js +92 -3
- package/package.json +15 -3
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parent-lineage gates for story-ready and pre-PR/preflight (#3241 / epic #3237).
|
|
3
|
+
*
|
|
4
|
+
* After #3238 coverage artifacts exist, a green child can still contradict parent
|
|
5
|
+
* invariants unless build/review gates re-check structure. This module:
|
|
6
|
+
* - resolves parent via plan.planRef (or plan references)
|
|
7
|
+
* - when parent authors requirement IDs, requires child coverage artifacts
|
|
8
|
+
* - reuses validateCoverageMap (structure-only; no LLM judgment)
|
|
9
|
+
* - classifies defects: child_spec | parent_child_drift
|
|
10
|
+
*
|
|
11
|
+
* Backward compatible: no parent link, or parent with zero requirement IDs → N/A pass.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
14
|
+
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
15
|
+
import { extractParentRequirements, formatCoverageReportLine, validateCoverageMap, } from "./coverage-map.js";
|
|
16
|
+
export const PARENT_LINEAGE_SCHEMA = "deft.scope.parent_lineage.v1";
|
|
17
|
+
/** Lifecycle folders scanned for unique plan.id recovery of a moved parent. */
|
|
18
|
+
const PARENT_LINEAGE_LIFECYCLE_FOLDERS = [
|
|
19
|
+
"proposed",
|
|
20
|
+
"pending",
|
|
21
|
+
"active",
|
|
22
|
+
"completed",
|
|
23
|
+
"cancelled",
|
|
24
|
+
];
|
|
25
|
+
/** Defect classes for gate output (#3241 AC: distinguish child-spec vs parent/child drift). */
|
|
26
|
+
export const LINEAGE_DEFECT_CLASSES = ["child_spec", "parent_child_drift"];
|
|
27
|
+
function asRecord(value) {
|
|
28
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function isNonEmptyString(value) {
|
|
34
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Walk up from a child artifact path to find the lifecycle root (xbrief/ or vbrief/).
|
|
38
|
+
*/
|
|
39
|
+
export function findLifecycleRootFromArtifact(artifactPath) {
|
|
40
|
+
let current = resolve(dirname(artifactPath));
|
|
41
|
+
for (let i = 0; i < 8; i += 1) {
|
|
42
|
+
const base = current.split(/[/\\]/).pop()?.toLowerCase() ?? "";
|
|
43
|
+
if (base === "xbrief" || base === "vbrief") {
|
|
44
|
+
return current;
|
|
45
|
+
}
|
|
46
|
+
const parent = dirname(current);
|
|
47
|
+
if (parent === current)
|
|
48
|
+
break;
|
|
49
|
+
current = parent;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolve plan.planRef (or similar relative ref) against a lifecycle root.
|
|
55
|
+
* Rejects absolute paths and `..` traversal.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveParentPathFromRef(planRef, lifecycleRoot) {
|
|
58
|
+
const trimmed = planRef.trim();
|
|
59
|
+
if (!trimmed) {
|
|
60
|
+
return { path: null, error: "empty planRef" };
|
|
61
|
+
}
|
|
62
|
+
if (isAbsolute(trimmed) || /^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
63
|
+
return { path: null, error: `planRef must be relative to lifecycle root (got absolute)` };
|
|
64
|
+
}
|
|
65
|
+
const segments = trimmed.split(/[/\\]+/).filter((s) => s.length > 0);
|
|
66
|
+
if (segments.includes("..")) {
|
|
67
|
+
return { path: null, error: `planRef must not contain parent traversal (..)` };
|
|
68
|
+
}
|
|
69
|
+
// Strip leading xbrief/ or vbrief/ if present (planRef is often relative to lifecycle root).
|
|
70
|
+
const first = segments[0]?.toLowerCase();
|
|
71
|
+
const relSegments = first === "xbrief" || first === "vbrief" ? segments.slice(1) : segments;
|
|
72
|
+
const resolved = resolve(lifecycleRoot, ...relSegments);
|
|
73
|
+
const rootResolved = resolve(lifecycleRoot);
|
|
74
|
+
if (resolved !== rootResolved && !resolved.startsWith(rootResolved + sep)) {
|
|
75
|
+
return { path: null, error: `planRef escapes lifecycle root` };
|
|
76
|
+
}
|
|
77
|
+
return { path: resolved, error: null };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Extract coverage draft surface from a child xBRIEF for validateCoverageMap.
|
|
81
|
+
* Prefer plan.metadata.parent_lineage, then plan.metadata, then plan/root.
|
|
82
|
+
*/
|
|
83
|
+
export function extractChildCoverageDraft(child) {
|
|
84
|
+
const root = asRecord(child);
|
|
85
|
+
if (root === null) {
|
|
86
|
+
return { draft: null, source: null, hasCoverageMapKey: false };
|
|
87
|
+
}
|
|
88
|
+
const plan = asRecord(root.plan) ?? root;
|
|
89
|
+
const metadata = asRecord(plan.metadata);
|
|
90
|
+
const candidates = [];
|
|
91
|
+
if (metadata !== null) {
|
|
92
|
+
const lineage = asRecord(metadata.parent_lineage ?? metadata.parentLineage);
|
|
93
|
+
if (lineage !== null) {
|
|
94
|
+
candidates.push({ source: "plan.metadata.parent_lineage", obj: lineage });
|
|
95
|
+
}
|
|
96
|
+
candidates.push({ source: "plan.metadata", obj: metadata });
|
|
97
|
+
}
|
|
98
|
+
candidates.push({ source: "plan", obj: plan });
|
|
99
|
+
candidates.push({ source: "root", obj: root });
|
|
100
|
+
for (const c of candidates) {
|
|
101
|
+
const hasMap = "coverage_map" in c.obj ||
|
|
102
|
+
"coverageMap" in c.obj ||
|
|
103
|
+
asRecord(c.obj.coverage)?.coverage_map !== undefined;
|
|
104
|
+
if (!hasMap)
|
|
105
|
+
continue;
|
|
106
|
+
const coverageNested = asRecord(c.obj.coverage);
|
|
107
|
+
const coverage_map = c.obj.coverage_map ??
|
|
108
|
+
c.obj.coverageMap ??
|
|
109
|
+
coverageNested?.coverage_map ??
|
|
110
|
+
coverageNested?.coverageMap;
|
|
111
|
+
const behavioral_deltas = c.obj.behavioral_deltas ??
|
|
112
|
+
c.obj.behavioralDeltas ??
|
|
113
|
+
coverageNested?.behavioral_deltas ??
|
|
114
|
+
coverageNested?.behavioralDeltas;
|
|
115
|
+
return {
|
|
116
|
+
draft: {
|
|
117
|
+
coverage_map,
|
|
118
|
+
behavioral_deltas,
|
|
119
|
+
},
|
|
120
|
+
source: c.source,
|
|
121
|
+
hasCoverageMapKey: true,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return { draft: null, source: null, hasCoverageMapKey: false };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Build the parent_lineage metadata block stamped onto decomposed children (#3241).
|
|
128
|
+
*/
|
|
129
|
+
export function buildParentLineageArtifact(opts) {
|
|
130
|
+
const out = {
|
|
131
|
+
schema: PARENT_LINEAGE_SCHEMA,
|
|
132
|
+
};
|
|
133
|
+
if (opts.coverage_map !== undefined) {
|
|
134
|
+
out.coverage_map = opts.coverage_map;
|
|
135
|
+
}
|
|
136
|
+
if (opts.behavioral_deltas !== undefined) {
|
|
137
|
+
out.behavioral_deltas = opts.behavioral_deltas;
|
|
138
|
+
}
|
|
139
|
+
if (opts.parent_requirement_ids !== undefined) {
|
|
140
|
+
out.parent_requirement_ids = [...opts.parent_requirement_ids];
|
|
141
|
+
}
|
|
142
|
+
if (opts.negative_invariant_ids !== undefined) {
|
|
143
|
+
out.negative_invariant_ids = [...opts.negative_invariant_ids];
|
|
144
|
+
}
|
|
145
|
+
if (opts.parent_plan_id !== undefined && opts.parent_plan_id.trim().length > 0) {
|
|
146
|
+
out.parent_plan_id = opts.parent_plan_id.trim();
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
function resolveParentRefFromChild(child) {
|
|
151
|
+
const plan = asRecord(child.plan) ?? child;
|
|
152
|
+
if (isNonEmptyString(plan.planRef))
|
|
153
|
+
return plan.planRef.trim();
|
|
154
|
+
if (isNonEmptyString(plan.plan_ref))
|
|
155
|
+
return plan.plan_ref.trim();
|
|
156
|
+
const refs = plan.references;
|
|
157
|
+
if (Array.isArray(refs)) {
|
|
158
|
+
for (const ref of refs) {
|
|
159
|
+
const r = asRecord(ref);
|
|
160
|
+
if (r === null)
|
|
161
|
+
continue;
|
|
162
|
+
const type = String(r.type ?? "").toLowerCase();
|
|
163
|
+
// Parent plan provenance: x-xbrief/plan or x-vbrief/plan (or plain plan).
|
|
164
|
+
if (type === "plan" ||
|
|
165
|
+
type.endsWith("/plan") ||
|
|
166
|
+
type.includes("xbrief/plan") ||
|
|
167
|
+
type.includes("vbrief/plan")) {
|
|
168
|
+
if (isNonEmptyString(r.uri)) {
|
|
169
|
+
const uri = r.uri.trim();
|
|
170
|
+
// Skip issue/github URIs.
|
|
171
|
+
if (uri.startsWith("http://") || uri.startsWith("https://") || uri.startsWith("#")) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
return uri.startsWith("file://") ? uri.slice("file://".length) : uri;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function loadJsonFile(path) {
|
|
182
|
+
try {
|
|
183
|
+
if (!existsSync(path)) {
|
|
184
|
+
return { ok: false, error: `parent not found at ${path}` };
|
|
185
|
+
}
|
|
186
|
+
const raw = readFileSync(path, "utf8");
|
|
187
|
+
const data = JSON.parse(raw);
|
|
188
|
+
const rec = asRecord(data);
|
|
189
|
+
if (rec === null) {
|
|
190
|
+
return { ok: false, error: `parent at ${path} is not a JSON object` };
|
|
191
|
+
}
|
|
192
|
+
return { ok: true, data: rec };
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
error: `could not load parent at ${path}: ${String(err.message ?? err)}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** Read plan.id from a loaded parent document (plan.id or root id). */
|
|
202
|
+
export function extractPlanId(doc) {
|
|
203
|
+
const root = asRecord(doc);
|
|
204
|
+
if (root === null)
|
|
205
|
+
return null;
|
|
206
|
+
const plan = asRecord(root.plan) ?? root;
|
|
207
|
+
const id = plan.id;
|
|
208
|
+
return typeof id === "string" && id.trim().length > 0 ? id.trim() : null;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Scan lifecycle folders for artifacts whose plan.id exactly equals parentPlanId.
|
|
212
|
+
* Returns every match (caller enforces uniqueness). Does not use basenames.
|
|
213
|
+
*/
|
|
214
|
+
export function findParentsByPlanId(lifecycleRoot, parentPlanId) {
|
|
215
|
+
const want = parentPlanId.trim();
|
|
216
|
+
if (!want)
|
|
217
|
+
return [];
|
|
218
|
+
const matches = [];
|
|
219
|
+
for (const folder of PARENT_LINEAGE_LIFECYCLE_FOLDERS) {
|
|
220
|
+
const dir = join(lifecycleRoot, folder);
|
|
221
|
+
if (!existsSync(dir))
|
|
222
|
+
continue;
|
|
223
|
+
let entries;
|
|
224
|
+
try {
|
|
225
|
+
entries = readdirSync(dir);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
for (const name of entries) {
|
|
231
|
+
if (!name.endsWith(".json"))
|
|
232
|
+
continue;
|
|
233
|
+
const full = join(dir, name);
|
|
234
|
+
const loaded = loadJsonFile(full);
|
|
235
|
+
if (!loaded.ok)
|
|
236
|
+
continue;
|
|
237
|
+
const id = extractPlanId(loaded.data);
|
|
238
|
+
if (id === want) {
|
|
239
|
+
matches.push({ path: full, data: loaded.data });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return matches;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Load parent for lineage checks.
|
|
247
|
+
*
|
|
248
|
+
* 1. Prefer the exact planRef path when it exists; require plan.id match when
|
|
249
|
+
* the child stamps parent_plan_id (path replacement with a different plan is rejected).
|
|
250
|
+
* 2. When the exact path is missing AND the child stamps parent_plan_id: recover by
|
|
251
|
+
* scanning lifecycle folders for plan.id === parent_plan_id and accepting ONLY
|
|
252
|
+
* when exactly one artifact matches. This handles lifecycle moves when child
|
|
253
|
+
* planRef rewrite lagged — not silent basename recovery of unrelated files.
|
|
254
|
+
* 3. Without a durable stamp, stale/missing planRef fails closed (no basename guess).
|
|
255
|
+
*/
|
|
256
|
+
export function loadParentExact(parentPath, opts = {}) {
|
|
257
|
+
const expected = opts.expectedParentPlanId !== undefined &&
|
|
258
|
+
opts.expectedParentPlanId !== null &&
|
|
259
|
+
opts.expectedParentPlanId.trim().length > 0
|
|
260
|
+
? opts.expectedParentPlanId.trim()
|
|
261
|
+
: null;
|
|
262
|
+
const primary = loadJsonFile(parentPath);
|
|
263
|
+
if (primary.ok) {
|
|
264
|
+
if (expected !== null) {
|
|
265
|
+
const actual = extractPlanId(primary.data) ?? "";
|
|
266
|
+
if (actual !== expected) {
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
error: `parent at ${parentPath} has plan.id='${actual || "(missing)"}' but child stamps ` +
|
|
270
|
+
`parent_plan_id='${expected}' — refuse identity mismatch (rewrite planRef or re-stamp)`,
|
|
271
|
+
path: parentPath,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return { ok: true, data: primary.data, path: parentPath };
|
|
276
|
+
}
|
|
277
|
+
// Exact path missing: unique plan.id recovery only (no basename recovery).
|
|
278
|
+
// Documented intentional: when planRef is stale after a lifecycle move, a stamped
|
|
279
|
+
// parent_plan_id that matches exactly one active/completed/… artifact is the
|
|
280
|
+
// durable identity key — never recover by filename alone.
|
|
281
|
+
if (expected !== null && opts.lifecycleRoot !== undefined && opts.lifecycleRoot !== null) {
|
|
282
|
+
const matches = findParentsByPlanId(opts.lifecycleRoot, expected);
|
|
283
|
+
const unique = matches.length === 1 ? matches[0] : undefined;
|
|
284
|
+
if (unique !== undefined) {
|
|
285
|
+
return { ok: true, data: unique.data, path: unique.path };
|
|
286
|
+
}
|
|
287
|
+
if (matches.length > 1) {
|
|
288
|
+
const paths = matches.map((m) => m.path).join(", ");
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
error: `parent planRef path missing at ${parentPath}; parent_plan_id='${expected}' matches ` +
|
|
292
|
+
`${matches.length} lifecycle artifacts (ambiguous — rewrite planRef): ${paths}`,
|
|
293
|
+
path: parentPath,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
ok: false,
|
|
298
|
+
error: `${primary.error}. Parent planRef path missing and no unique lifecycle artifact with ` +
|
|
299
|
+
`plan.id='${expected}' (rewrite the child's planRef; no basename guess).`,
|
|
300
|
+
path: parentPath,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
ok: false,
|
|
305
|
+
error: `${primary.error}. Parent path must match planRef exactly — if the parent ` +
|
|
306
|
+
`moved lifecycle folders, stamp parent_plan_id (decompose) or rewrite planRef ` +
|
|
307
|
+
`(no basename guess).`,
|
|
308
|
+
path: parentPath,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/** Stamped parent_plan_id from child plan.metadata.parent_lineage (if any). */
|
|
312
|
+
export function extractStampedParentPlanId(child) {
|
|
313
|
+
const root = asRecord(child);
|
|
314
|
+
if (root === null)
|
|
315
|
+
return null;
|
|
316
|
+
const plan = asRecord(root.plan) ?? root;
|
|
317
|
+
const metadata = asRecord(plan.metadata);
|
|
318
|
+
if (metadata === null)
|
|
319
|
+
return null;
|
|
320
|
+
const lineage = asRecord(metadata.parent_lineage ?? metadata.parentLineage);
|
|
321
|
+
if (lineage === null)
|
|
322
|
+
return null;
|
|
323
|
+
const raw = lineage.parent_plan_id ?? lineage.parentPlanId;
|
|
324
|
+
return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : null;
|
|
325
|
+
}
|
|
326
|
+
function classifyCoverageFailure(errors) {
|
|
327
|
+
// Missing map / parse shape issues are child-spec; uncovered/conflict vs parent is drift.
|
|
328
|
+
const childSpecHints = [
|
|
329
|
+
"coverage_map is required",
|
|
330
|
+
"coverage_map must be",
|
|
331
|
+
"disposition must be",
|
|
332
|
+
"missing/invalid fields",
|
|
333
|
+
"parent_requirement_id is required",
|
|
334
|
+
"missing delta_id",
|
|
335
|
+
"requires delta_id",
|
|
336
|
+
"requires provenance",
|
|
337
|
+
"requires split_group",
|
|
338
|
+
"requires part",
|
|
339
|
+
"behavioral_deltas must be",
|
|
340
|
+
];
|
|
341
|
+
for (const e of errors) {
|
|
342
|
+
const lower = e.toLowerCase();
|
|
343
|
+
if (childSpecHints.some((h) => lower.includes(h.toLowerCase()))) {
|
|
344
|
+
return "child_spec";
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return "parent_child_drift";
|
|
348
|
+
}
|
|
349
|
+
function okResult(partial) {
|
|
350
|
+
return {
|
|
351
|
+
ok: true,
|
|
352
|
+
defect_class: null,
|
|
353
|
+
errors: [],
|
|
354
|
+
coverage_report: partial.coverage_report,
|
|
355
|
+
applicable: partial.applicable,
|
|
356
|
+
parent_path: partial.parent_path,
|
|
357
|
+
parent_requirement_ids: partial.parent_requirement_ids,
|
|
358
|
+
negative_invariant_ids: partial.negative_invariant_ids,
|
|
359
|
+
message: partial.message,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function failResult(opts) {
|
|
363
|
+
return {
|
|
364
|
+
ok: false,
|
|
365
|
+
applicable: opts.applicable ?? true,
|
|
366
|
+
defect_class: opts.defect_class,
|
|
367
|
+
message: opts.message,
|
|
368
|
+
parent_path: opts.parent_path ?? null,
|
|
369
|
+
parent_requirement_ids: opts.parent_requirement_ids ?? [],
|
|
370
|
+
negative_invariant_ids: opts.negative_invariant_ids ?? [],
|
|
371
|
+
coverage_report: opts.coverage_report ?? null,
|
|
372
|
+
errors: opts.errors,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Pure parent-lineage evaluation for an in-memory child + optional parent.
|
|
377
|
+
* When parent is omitted, uses parentPath / project layout from options.
|
|
378
|
+
*/
|
|
379
|
+
export function evaluateParentLineage(opts) {
|
|
380
|
+
if (opts.skip === true) {
|
|
381
|
+
return okResult({
|
|
382
|
+
applicable: false,
|
|
383
|
+
parent_path: null,
|
|
384
|
+
parent_requirement_ids: [],
|
|
385
|
+
negative_invariant_ids: [],
|
|
386
|
+
coverage_report: null,
|
|
387
|
+
message: "parent lineage: skipped",
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
const child = asRecord(opts.child);
|
|
391
|
+
if (child === null) {
|
|
392
|
+
return failResult({
|
|
393
|
+
defect_class: "child_spec",
|
|
394
|
+
message: "parent lineage: child is not a JSON object",
|
|
395
|
+
errors: ["child is not a JSON object"],
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const planRef = resolveParentRefFromChild(child);
|
|
399
|
+
if (planRef === null && opts.parent === undefined && opts.parentPath === undefined) {
|
|
400
|
+
return okResult({
|
|
401
|
+
applicable: false,
|
|
402
|
+
parent_path: null,
|
|
403
|
+
parent_requirement_ids: [],
|
|
404
|
+
negative_invariant_ids: [],
|
|
405
|
+
coverage_report: null,
|
|
406
|
+
message: "parent lineage: N/A (no planRef / parent link)",
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
let parentDoc = asRecord(opts.parent);
|
|
410
|
+
let parentPath = opts.parentPath ?? null;
|
|
411
|
+
const stampedParentPlanId = extractStampedParentPlanId(child);
|
|
412
|
+
if (parentDoc === null) {
|
|
413
|
+
let lifecycleRoot = null;
|
|
414
|
+
if (opts.childPath !== undefined) {
|
|
415
|
+
lifecycleRoot = findLifecycleRootFromArtifact(opts.childPath);
|
|
416
|
+
}
|
|
417
|
+
if (lifecycleRoot === null && opts.projectRoot !== undefined) {
|
|
418
|
+
const xbrief = join(opts.projectRoot, "xbrief");
|
|
419
|
+
const vbrief = join(opts.projectRoot, "vbrief");
|
|
420
|
+
if (existsSync(xbrief))
|
|
421
|
+
lifecycleRoot = xbrief;
|
|
422
|
+
else if (existsSync(vbrief))
|
|
423
|
+
lifecycleRoot = vbrief;
|
|
424
|
+
}
|
|
425
|
+
if (parentPath === null && planRef !== null) {
|
|
426
|
+
if (lifecycleRoot === null) {
|
|
427
|
+
return failResult({
|
|
428
|
+
defect_class: "child_spec",
|
|
429
|
+
message: "parent lineage: cannot resolve planRef (no lifecycle root from child path or project root)",
|
|
430
|
+
errors: ["cannot resolve planRef: missing lifecycle root"],
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
const resolved = resolveParentPathFromRef(planRef, lifecycleRoot);
|
|
434
|
+
if (resolved.error !== null || resolved.path === null) {
|
|
435
|
+
return failResult({
|
|
436
|
+
defect_class: "child_spec",
|
|
437
|
+
message: `parent lineage: invalid planRef '${planRef}': ${resolved.error}`,
|
|
438
|
+
errors: [resolved.error ?? "invalid planRef"],
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
parentPath = resolved.path;
|
|
442
|
+
}
|
|
443
|
+
if (parentPath === null) {
|
|
444
|
+
return failResult({
|
|
445
|
+
defect_class: "child_spec",
|
|
446
|
+
message: "parent lineage: parent path unresolved",
|
|
447
|
+
errors: ["parent path unresolved"],
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
// Exact planRef, else unique parent_plan_id recovery (no basename guess).
|
|
451
|
+
const loaded = loadParentExact(parentPath, {
|
|
452
|
+
expectedParentPlanId: stampedParentPlanId,
|
|
453
|
+
lifecycleRoot,
|
|
454
|
+
});
|
|
455
|
+
if (!loaded.ok) {
|
|
456
|
+
return failResult({
|
|
457
|
+
defect_class: "child_spec",
|
|
458
|
+
message: `parent lineage: ${loaded.error}`,
|
|
459
|
+
errors: [loaded.error],
|
|
460
|
+
parent_path: loaded.path,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
parentDoc = loaded.data;
|
|
464
|
+
parentPath = loaded.path;
|
|
465
|
+
}
|
|
466
|
+
const requirements = extractParentRequirements(parentDoc);
|
|
467
|
+
const parentIds = requirements.map((r) => r.id);
|
|
468
|
+
const negativeIds = requirements.filter((r) => r.negativeInvariant).map((r) => r.id);
|
|
469
|
+
if (parentIds.length === 0) {
|
|
470
|
+
// Still refuse identity mismatch when child stamps a durable parent id.
|
|
471
|
+
if (stampedParentPlanId !== null) {
|
|
472
|
+
const actualId = extractPlanId(parentDoc) ?? "";
|
|
473
|
+
if (actualId !== stampedParentPlanId) {
|
|
474
|
+
return failResult({
|
|
475
|
+
defect_class: "child_spec",
|
|
476
|
+
message: `parent lineage: parent plan.id='${actualId || "(missing)"}' but child stamps ` +
|
|
477
|
+
`parent_plan_id='${stampedParentPlanId}' — refuse identity mismatch`,
|
|
478
|
+
errors: [
|
|
479
|
+
`parent identity mismatch: plan.id='${actualId || "(missing)"}' vs parent_plan_id='${stampedParentPlanId}'`,
|
|
480
|
+
],
|
|
481
|
+
parent_path: parentPath,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return okResult({
|
|
486
|
+
applicable: false,
|
|
487
|
+
parent_path: parentPath,
|
|
488
|
+
parent_requirement_ids: [],
|
|
489
|
+
negative_invariant_ids: [],
|
|
490
|
+
coverage_report: null,
|
|
491
|
+
message: "parent lineage: N/A (parent authors no requirement IDs)",
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
// When parent authors requirement IDs, durable parent identity is mandatory.
|
|
495
|
+
// Without parent_plan_id, an exact-path replacement can silently validate against
|
|
496
|
+
// the wrong parent (or N/A-pass an empty imposter). Decompose fails closed if the
|
|
497
|
+
// parent has no plan.id; hand-authored children must stamp the same field.
|
|
498
|
+
if (stampedParentPlanId === null) {
|
|
499
|
+
return failResult({
|
|
500
|
+
defect_class: "child_spec",
|
|
501
|
+
message: "parent lineage: child missing durable parent_plan_id stamp while parent authors " +
|
|
502
|
+
`requirement ID(s) (${parentIds.join(", ")}). Re-decompose from a parent with plan.id, ` +
|
|
503
|
+
"or stamp plan.metadata.parent_lineage.parent_plan_id to the parent's plan.id.",
|
|
504
|
+
errors: [
|
|
505
|
+
"missing parent_plan_id stamp on child",
|
|
506
|
+
`parent authors requirement IDs: ${parentIds.join(", ")}`,
|
|
507
|
+
],
|
|
508
|
+
parent_path: parentPath,
|
|
509
|
+
parent_requirement_ids: parentIds,
|
|
510
|
+
negative_invariant_ids: negativeIds,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
const actualParentId = extractPlanId(parentDoc) ?? "";
|
|
514
|
+
if (actualParentId !== stampedParentPlanId) {
|
|
515
|
+
return failResult({
|
|
516
|
+
defect_class: "child_spec",
|
|
517
|
+
message: `parent lineage: parent plan.id='${actualParentId || "(missing)"}' but child stamps ` +
|
|
518
|
+
`parent_plan_id='${stampedParentPlanId}' — refuse identity mismatch (rewrite planRef or re-stamp)`,
|
|
519
|
+
errors: [
|
|
520
|
+
`parent identity mismatch: plan.id='${actualParentId || "(missing)"}' vs parent_plan_id='${stampedParentPlanId}'`,
|
|
521
|
+
],
|
|
522
|
+
parent_path: parentPath,
|
|
523
|
+
parent_requirement_ids: parentIds,
|
|
524
|
+
negative_invariant_ids: negativeIds,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
const { draft, source, hasCoverageMapKey } = extractChildCoverageDraft(child);
|
|
528
|
+
if (!hasCoverageMapKey || draft === null) {
|
|
529
|
+
return failResult({
|
|
530
|
+
defect_class: "child_spec",
|
|
531
|
+
message: "parent lineage: child missing parent coverage artifacts " +
|
|
532
|
+
`(parent authors ${parentIds.length} requirement ID(s): ${parentIds.join(", ")}). ` +
|
|
533
|
+
"Stamp plan.metadata.parent_lineage.coverage_map (from #3238 decompose) or declare coverage_map.",
|
|
534
|
+
errors: [
|
|
535
|
+
"missing parent coverage artifacts on child",
|
|
536
|
+
`uncovered parent requirement IDs: ${parentIds.join(", ")}`,
|
|
537
|
+
],
|
|
538
|
+
parent_path: parentPath,
|
|
539
|
+
parent_requirement_ids: parentIds,
|
|
540
|
+
negative_invariant_ids: negativeIds,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
const validation = validateCoverageMap({
|
|
544
|
+
parent: parentDoc,
|
|
545
|
+
draft,
|
|
546
|
+
});
|
|
547
|
+
if (!validation.ok) {
|
|
548
|
+
const defect = classifyCoverageFailure(validation.errors);
|
|
549
|
+
const negHits = validation.errors.filter((e) => e.includes("negative invariant") || e.includes("silent removal"));
|
|
550
|
+
const summary = validation.errors.slice(0, 4).join("; ");
|
|
551
|
+
const kindLabel = defect === "child_spec" ? "child-spec defect" : "parent/child drift";
|
|
552
|
+
let message = `parent lineage: ${kindLabel} — ${summary}` +
|
|
553
|
+
(validation.errors.length > 4 ? ` (+${validation.errors.length - 4} more)` : "");
|
|
554
|
+
if (negHits.length > 0) {
|
|
555
|
+
message +=
|
|
556
|
+
" (negative invariant requires coverage or approved behavioral_delta; silent drop forbidden)";
|
|
557
|
+
}
|
|
558
|
+
message += ` [${source ?? "coverage"}] ${formatCoverageReportLine(validation.report)}`;
|
|
559
|
+
return failResult({
|
|
560
|
+
defect_class: defect,
|
|
561
|
+
message,
|
|
562
|
+
errors: validation.errors,
|
|
563
|
+
parent_path: parentPath,
|
|
564
|
+
parent_requirement_ids: parentIds,
|
|
565
|
+
negative_invariant_ids: negativeIds,
|
|
566
|
+
coverage_report: validation.report,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
return okResult({
|
|
570
|
+
applicable: true,
|
|
571
|
+
parent_path: parentPath,
|
|
572
|
+
parent_requirement_ids: parentIds,
|
|
573
|
+
negative_invariant_ids: negativeIds,
|
|
574
|
+
coverage_report: validation.report,
|
|
575
|
+
message: `parent lineage: OK (${parentIds.length} parent requirement ID(s); ` +
|
|
576
|
+
`${negativeIds.length} negative invariant(s); source=${source ?? "coverage"})`,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Convenience: load child from path and evaluate lineage.
|
|
581
|
+
*/
|
|
582
|
+
export function evaluateParentLineageAtPath(childPath, options = {}) {
|
|
583
|
+
let raw;
|
|
584
|
+
try {
|
|
585
|
+
raw = readFileSync(childPath, "utf8");
|
|
586
|
+
}
|
|
587
|
+
catch (err) {
|
|
588
|
+
return failResult({
|
|
589
|
+
defect_class: "child_spec",
|
|
590
|
+
message: `parent lineage: could not read child at ${childPath}: ${String(err.message ?? err)}`,
|
|
591
|
+
errors: [`could not read child: ${String(err.message ?? err)}`],
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
let child;
|
|
595
|
+
try {
|
|
596
|
+
child = JSON.parse(raw);
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
return failResult({
|
|
600
|
+
defect_class: "child_spec",
|
|
601
|
+
message: `parent lineage: child at ${childPath} is not valid JSON: ${String(err.message ?? err)}`,
|
|
602
|
+
errors: [`invalid child JSON: ${String(err.message ?? err)}`],
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
return evaluateParentLineage({
|
|
606
|
+
child,
|
|
607
|
+
childPath,
|
|
608
|
+
projectRoot: options.projectRoot,
|
|
609
|
+
skip: options.skip,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
/** One-line machine-readable lineage summary for gate stdout. */
|
|
613
|
+
export function formatParentLineageLine(result) {
|
|
614
|
+
const payload = {
|
|
615
|
+
schema: "deft.scope.parent_lineage_report.v1",
|
|
616
|
+
ok: result.ok,
|
|
617
|
+
applicable: result.applicable,
|
|
618
|
+
defect_class: result.defect_class,
|
|
619
|
+
parent_path: result.parent_path,
|
|
620
|
+
parent_requirement_ids: result.parent_requirement_ids,
|
|
621
|
+
negative_invariant_ids: result.negative_invariant_ids,
|
|
622
|
+
errors: result.errors,
|
|
623
|
+
};
|
|
624
|
+
return `PARENT_LINEAGE ${JSON.stringify(payload)}`;
|
|
625
|
+
}
|
|
626
|
+
//# sourceMappingURL=parent-lineage.js.map
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { GitRunner } from "../session/git.js";
|
|
2
|
+
import { type CriterionAcceptanceReport } from "./acceptance-evidence.js";
|
|
2
3
|
import { detectLifecycleFolder } from "./decomposed-refs.js";
|
|
3
4
|
import { type DeliveryEvidenceInput, type NonDeliveryDisposition } from "./delivery-evidence.js";
|
|
4
5
|
import type { WipCapCheck } from "./wip-cap-check.js";
|
|
5
6
|
export interface TransitionResult {
|
|
6
7
|
readonly ok: boolean;
|
|
7
8
|
readonly message: string;
|
|
9
|
+
/** Per-criterion acceptance reports when complete ran the #3240 gate. */
|
|
10
|
+
readonly acceptanceReports?: readonly CriterionAcceptanceReport[];
|
|
8
11
|
}
|
|
9
12
|
/** Optional completion evidence / disposition for the delivery gate (#3041). */
|
|
10
13
|
export interface TransitionOptions {
|
|
@@ -13,6 +16,11 @@ export interface TransitionOptions {
|
|
|
13
16
|
readonly runGit?: GitRunner;
|
|
14
17
|
readonly verifier?: string;
|
|
15
18
|
readonly assumeEvidenceValidated?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Test-only escape hatch: skip the #3240 per-item acceptance evidence gate.
|
|
21
|
+
* Production callers MUST leave this false/undefined.
|
|
22
|
+
*/
|
|
23
|
+
readonly skipAcceptanceEvidenceGate?: boolean;
|
|
16
24
|
}
|
|
17
25
|
export declare function runTransition(action: string, filePath: string, now?: Date, options?: TransitionOptions): TransitionResult;
|
|
18
26
|
export declare function recordWipCapOverride(filePath: string, projectRoot: string, check: WipCapCheck, now?: Date): void;
|