@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,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-acceptance-criterion evidence / disposition for scope:complete (#3240 / epic #3237 Q3).
|
|
3
|
+
*
|
|
4
|
+
* Merge ancestry alone must not auto-complete acceptance items. Each non-terminal
|
|
5
|
+
* plan item needs either typed evidence or a human-origin disposition before complete
|
|
6
|
+
* may advance it. Kind suitability: merge/review alone cannot satisfy smoke, UAT,
|
|
7
|
+
* deploy, or observed_behavior requirements.
|
|
8
|
+
*
|
|
9
|
+
* Canonical item keys are namespaced under #1620 / #3305 (Option B):
|
|
10
|
+
* - plan.items[].x-directive/evidence
|
|
11
|
+
* - plan.items[].x-directive/disposition
|
|
12
|
+
* Bare `evidence` / `disposition` are not valid typed evidence (no dual-read).
|
|
13
|
+
* ITEM_CORE is not expanded with bare keys; verify:vbrief-conformance rejects them.
|
|
14
|
+
*/
|
|
15
|
+
import { isHumanOrigin } from "../authz/origin.js";
|
|
16
|
+
/** Canonical namespaced key for typed acceptance evidence (#3305 / #1620). */
|
|
17
|
+
export const ACCEPTANCE_EVIDENCE_KEY = "x-directive/evidence";
|
|
18
|
+
/** Canonical namespaced key for human-origin disposition (#3305 / #1620). */
|
|
19
|
+
export const ACCEPTANCE_DISPOSITION_KEY = "x-directive/disposition";
|
|
20
|
+
/** Closed evidence kinds (locked Q3). */
|
|
21
|
+
export const ACCEPTANCE_EVIDENCE_KINDS = [
|
|
22
|
+
"test",
|
|
23
|
+
"review",
|
|
24
|
+
"merge",
|
|
25
|
+
"deploy",
|
|
26
|
+
"smoke",
|
|
27
|
+
"uat",
|
|
28
|
+
"observed_behavior",
|
|
29
|
+
];
|
|
30
|
+
/** Closed dispositions (locked Q3). */
|
|
31
|
+
export const ACCEPTANCE_DISPOSITIONS = ["waived", "deferred", "not_applicable"];
|
|
32
|
+
/**
|
|
33
|
+
* Axes that merge/review evidence alone cannot satisfy.
|
|
34
|
+
* Matches epic #3237 Q3 suitability rule.
|
|
35
|
+
*/
|
|
36
|
+
export const STRICT_ACCEPTANCE_AXES = ["deploy", "smoke", "uat", "observed_behavior"];
|
|
37
|
+
const EVIDENCE_KIND_SET = new Set(ACCEPTANCE_EVIDENCE_KINDS);
|
|
38
|
+
const DISPOSITION_SET = new Set(ACCEPTANCE_DISPOSITIONS);
|
|
39
|
+
const STRICT_AXIS_SET = new Set(STRICT_ACCEPTANCE_AXES);
|
|
40
|
+
/** Item statuses that still represent unfinished acceptance work (#2862 / #3240). */
|
|
41
|
+
const NON_TERMINAL_ITEM_STATUSES = new Set(["pending", "proposed", "running"]);
|
|
42
|
+
function asRecord(value) {
|
|
43
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function isNonEmptyString(value) {
|
|
49
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
50
|
+
}
|
|
51
|
+
export function isAcceptanceEvidenceKind(value) {
|
|
52
|
+
return typeof value === "string" && EVIDENCE_KIND_SET.has(value.trim().toLowerCase());
|
|
53
|
+
}
|
|
54
|
+
export function isAcceptanceDispositionKind(value) {
|
|
55
|
+
return typeof value === "string" && DISPOSITION_SET.has(value.trim().toLowerCase());
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Infer strict axes a criterion requires from explicit fields and title/Acceptance text.
|
|
59
|
+
* Explicit `requires` / `requiredEvidenceKind` / `acceptanceAxis` wins when valid.
|
|
60
|
+
*/
|
|
61
|
+
export function inferRequiredStrictAxes(item) {
|
|
62
|
+
const explicit = [
|
|
63
|
+
item.requires,
|
|
64
|
+
item.requiredEvidenceKind,
|
|
65
|
+
item.required_evidence_kind,
|
|
66
|
+
item.acceptanceAxis,
|
|
67
|
+
item.acceptance_axis,
|
|
68
|
+
];
|
|
69
|
+
for (const raw of explicit) {
|
|
70
|
+
if (typeof raw !== "string")
|
|
71
|
+
continue;
|
|
72
|
+
const norm = raw.trim().toLowerCase().replace(/-/g, "_");
|
|
73
|
+
if (STRICT_AXIS_SET.has(norm)) {
|
|
74
|
+
return [norm];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const narrative = asRecord(item.narrative);
|
|
78
|
+
const parts = [];
|
|
79
|
+
if (typeof item.title === "string")
|
|
80
|
+
parts.push(item.title);
|
|
81
|
+
if (typeof item.id === "string")
|
|
82
|
+
parts.push(item.id);
|
|
83
|
+
if (narrative !== null) {
|
|
84
|
+
for (const key of ["Acceptance", "acceptance", "Description", "description", "Test", "test"]) {
|
|
85
|
+
if (typeof narrative[key] === "string")
|
|
86
|
+
parts.push(narrative[key]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const text = parts.join("\n").toLowerCase();
|
|
90
|
+
const found = [];
|
|
91
|
+
// Order: longer / more specific phrases first so "observed behavior" wins over generic.
|
|
92
|
+
if (/\bobserved[_\s-]?behavior\b/.test(text) ||
|
|
93
|
+
/\bobserved behaviour\b/.test(text) ||
|
|
94
|
+
/\brunning behaviour\b/.test(text) ||
|
|
95
|
+
/\brunning behavior\b/.test(text)) {
|
|
96
|
+
found.push("observed_behavior");
|
|
97
|
+
}
|
|
98
|
+
if (/\buat\b/.test(text) || /\buser acceptance\b/.test(text)) {
|
|
99
|
+
found.push("uat");
|
|
100
|
+
}
|
|
101
|
+
if (/\bsmoke\b/.test(text)) {
|
|
102
|
+
found.push("smoke");
|
|
103
|
+
}
|
|
104
|
+
if (/\bdeploy(?:ment|ed|s)?\b/.test(text)) {
|
|
105
|
+
found.push("deploy");
|
|
106
|
+
}
|
|
107
|
+
return found;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Whether evidence.kind is suitable for the criterion's required strict axes.
|
|
111
|
+
* merge and review never satisfy smoke/UAT/deploy/observed_behavior.
|
|
112
|
+
*
|
|
113
|
+
* A single evidence.kind covers only that axis. When free-text inference yields
|
|
114
|
+
* multiple axes (e.g. "smoke after deploy"), every required axis must match the
|
|
115
|
+
* same kind — which is only possible when the axes are identical. Otherwise use
|
|
116
|
+
* explicit `requires` / `acceptanceAxis` for a single axis, split the criterion,
|
|
117
|
+
* or record a human-origin disposition (#3240 Greptile P1).
|
|
118
|
+
*/
|
|
119
|
+
export function isEvidenceKindSuitable(kind, requiredAxes) {
|
|
120
|
+
if (requiredAxes.length === 0) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (kind === "merge" || kind === "review") {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
// All required strict axes must be covered by this single evidence kind.
|
|
127
|
+
return requiredAxes.every((axis) => kind === axis);
|
|
128
|
+
}
|
|
129
|
+
function parseEvidence(raw) {
|
|
130
|
+
const obj = asRecord(raw);
|
|
131
|
+
if (obj === null) {
|
|
132
|
+
return { ok: false, message: "evidence must be an object" };
|
|
133
|
+
}
|
|
134
|
+
const kindRaw = typeof obj.kind === "string" ? obj.kind.trim().toLowerCase() : "";
|
|
135
|
+
if (!isAcceptanceEvidenceKind(kindRaw)) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
message: `unknown or missing evidence.kind ${JSON.stringify(obj.kind)}; ` +
|
|
139
|
+
`allowed: ${ACCEPTANCE_EVIDENCE_KINDS.join("|")}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (!isNonEmptyString(obj.pointer)) {
|
|
143
|
+
return { ok: false, message: "evidence.pointer is required (non-empty string)" };
|
|
144
|
+
}
|
|
145
|
+
if (!isNonEmptyString(obj.recorded_at)) {
|
|
146
|
+
return { ok: false, message: "evidence.recorded_at is required" };
|
|
147
|
+
}
|
|
148
|
+
if (!isNonEmptyString(obj.recorded_by)) {
|
|
149
|
+
return { ok: false, message: "evidence.recorded_by is required" };
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
record: {
|
|
154
|
+
kind: kindRaw,
|
|
155
|
+
pointer: obj.pointer.trim(),
|
|
156
|
+
recorded_at: obj.recorded_at.trim(),
|
|
157
|
+
recorded_by: obj.recorded_by.trim(),
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function parseDisposition(raw) {
|
|
162
|
+
const obj = asRecord(raw);
|
|
163
|
+
if (obj === null) {
|
|
164
|
+
return { ok: false, message: "disposition must be an object" };
|
|
165
|
+
}
|
|
166
|
+
const dispRaw = typeof obj.disposition === "string" ? obj.disposition.trim().toLowerCase() : "";
|
|
167
|
+
if (!isAcceptanceDispositionKind(dispRaw)) {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
message: `unknown or missing disposition.disposition ${JSON.stringify(obj.disposition)}; ` +
|
|
171
|
+
`allowed: ${ACCEPTANCE_DISPOSITIONS.join("|")}`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (!isNonEmptyString(obj.reason)) {
|
|
175
|
+
return { ok: false, message: "disposition.reason is required" };
|
|
176
|
+
}
|
|
177
|
+
if (!isNonEmptyString(obj.recorded_at)) {
|
|
178
|
+
return { ok: false, message: "disposition.recorded_at is required" };
|
|
179
|
+
}
|
|
180
|
+
const provenance = asRecord(obj.provenance);
|
|
181
|
+
if (provenance === null) {
|
|
182
|
+
return { ok: false, message: "disposition.provenance is required (human-origin)" };
|
|
183
|
+
}
|
|
184
|
+
// Reuse authz human-origin gate (#2944) so agent-self stamps cannot waive criteria.
|
|
185
|
+
const originForCheck = {
|
|
186
|
+
kind: String(provenance.kind ?? ""),
|
|
187
|
+
actor: String(provenance.actor ?? ""),
|
|
188
|
+
mintedAt: typeof provenance.mintedAt === "string" ? provenance.mintedAt : "",
|
|
189
|
+
mintedVia: typeof provenance.mintedVia === "string" ? provenance.mintedVia : "",
|
|
190
|
+
eventRef: typeof provenance.eventRef === "string" ? provenance.eventRef : null,
|
|
191
|
+
};
|
|
192
|
+
if (!isHumanOrigin(originForCheck)) {
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
message: "disposition.provenance must be human-origin " +
|
|
196
|
+
"(kind operator-cli|operator-session|human-event + non-agent actor)",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const record = {
|
|
200
|
+
disposition: dispRaw,
|
|
201
|
+
reason: obj.reason.trim(),
|
|
202
|
+
provenance: {
|
|
203
|
+
kind: originForCheck.kind,
|
|
204
|
+
actor: originForCheck.actor,
|
|
205
|
+
...(originForCheck.mintedAt.length > 0 ? { mintedAt: originForCheck.mintedAt } : {}),
|
|
206
|
+
...(originForCheck.mintedVia.length > 0 ? { mintedVia: originForCheck.mintedVia } : {}),
|
|
207
|
+
...(originForCheck.eventRef !== null ? { eventRef: originForCheck.eventRef } : {}),
|
|
208
|
+
},
|
|
209
|
+
recorded_at: obj.recorded_at.trim(),
|
|
210
|
+
};
|
|
211
|
+
if (isNonEmptyString(obj.resume_when)) {
|
|
212
|
+
return {
|
|
213
|
+
ok: true,
|
|
214
|
+
record: { ...record, resume_when: obj.resume_when.trim() },
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return { ok: true, record };
|
|
218
|
+
}
|
|
219
|
+
function itemLabel(item, path) {
|
|
220
|
+
if (typeof item.title === "string" && item.title.trim().length > 0) {
|
|
221
|
+
return item.title.trim();
|
|
222
|
+
}
|
|
223
|
+
if (typeof item.id === "string" && item.id.trim().length > 0) {
|
|
224
|
+
return item.id.trim();
|
|
225
|
+
}
|
|
226
|
+
return path;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Read only namespaced acceptance fields (#3305). Bare `evidence` / `disposition`
|
|
230
|
+
* are never treated as success (no permanent dual-read).
|
|
231
|
+
*/
|
|
232
|
+
export function readNamespacedAcceptanceFields(item) {
|
|
233
|
+
const evidence = item[ACCEPTANCE_EVIDENCE_KEY];
|
|
234
|
+
const disposition = item[ACCEPTANCE_DISPOSITION_KEY];
|
|
235
|
+
return {
|
|
236
|
+
evidence,
|
|
237
|
+
disposition,
|
|
238
|
+
hasEvidence: evidence !== undefined && evidence !== null,
|
|
239
|
+
hasDisposition: disposition !== undefined && disposition !== null,
|
|
240
|
+
hasBareEvidence: item.evidence !== undefined && item.evidence !== null,
|
|
241
|
+
hasBareDisposition: item.disposition !== undefined && item.disposition !== null,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/** Clear bare and namespaced acceptance fields so stamps never leave dual/conflicting keys. */
|
|
245
|
+
function clearAcceptanceStampFields(item) {
|
|
246
|
+
delete item.evidence;
|
|
247
|
+
delete item.disposition;
|
|
248
|
+
delete item[ACCEPTANCE_EVIDENCE_KEY];
|
|
249
|
+
delete item[ACCEPTANCE_DISPOSITION_KEY];
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Stamp typed evidence under the canonical namespaced key only.
|
|
253
|
+
* Clears bare evidence/disposition and any prior namespaced disposition so the item
|
|
254
|
+
* cannot carry both acceptance fields after a replace (#3305 Greptile P2).
|
|
255
|
+
*/
|
|
256
|
+
export function stampNamespacedEvidence(item, record) {
|
|
257
|
+
clearAcceptanceStampFields(item);
|
|
258
|
+
item[ACCEPTANCE_EVIDENCE_KEY] = {
|
|
259
|
+
kind: record.kind,
|
|
260
|
+
pointer: record.pointer,
|
|
261
|
+
recorded_at: record.recorded_at,
|
|
262
|
+
recorded_by: record.recorded_by,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Stamp human-origin disposition under the canonical namespaced key only.
|
|
267
|
+
* Clears bare evidence/disposition and any prior namespaced evidence so the item
|
|
268
|
+
* cannot carry both acceptance fields after a replace (#3305 Greptile P2).
|
|
269
|
+
*/
|
|
270
|
+
export function stampNamespacedDisposition(item, record) {
|
|
271
|
+
clearAcceptanceStampFields(item);
|
|
272
|
+
const body = {
|
|
273
|
+
disposition: record.disposition,
|
|
274
|
+
reason: record.reason,
|
|
275
|
+
provenance: { ...record.provenance },
|
|
276
|
+
recorded_at: record.recorded_at,
|
|
277
|
+
};
|
|
278
|
+
if (record.resume_when !== undefined) {
|
|
279
|
+
body.resume_when = record.resume_when;
|
|
280
|
+
}
|
|
281
|
+
item[ACCEPTANCE_DISPOSITION_KEY] = body;
|
|
282
|
+
}
|
|
283
|
+
function evaluateOneItem(item, path) {
|
|
284
|
+
const title = itemLabel(item, path);
|
|
285
|
+
const status = String(item.status ?? "");
|
|
286
|
+
if (!NON_TERMINAL_ITEM_STATUSES.has(status)) {
|
|
287
|
+
// Already-terminal: complete does not re-validate typed evidence (#3240 / #3305).
|
|
288
|
+
// Suitability/provenance apply only when advancing non-terminal items. Pre-marking
|
|
289
|
+
// items completed with narrative-only fields still skips the typed gate — that is
|
|
290
|
+
// intentional for fail/cancel and historical terminals, not a silent dual success path.
|
|
291
|
+
return {
|
|
292
|
+
path,
|
|
293
|
+
title,
|
|
294
|
+
outcome: "already_terminal",
|
|
295
|
+
detail: `status=${status || "(empty)"} (not advanced; typed evidence not re-checked)`,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const fields = readNamespacedAcceptanceFields(item);
|
|
299
|
+
const { hasEvidence, hasDisposition, hasBareEvidence, hasBareDisposition } = fields;
|
|
300
|
+
if (hasEvidence && hasDisposition) {
|
|
301
|
+
return {
|
|
302
|
+
path,
|
|
303
|
+
title,
|
|
304
|
+
outcome: "invalid",
|
|
305
|
+
detail: `exactly one of ${ACCEPTANCE_EVIDENCE_KEY} or ${ACCEPTANCE_DISPOSITION_KEY} required; both present`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (!hasEvidence && !hasDisposition) {
|
|
309
|
+
const bareHint = hasBareEvidence || hasBareDisposition
|
|
310
|
+
? ` bare evidence/disposition ignored — use ${ACCEPTANCE_EVIDENCE_KEY} or ${ACCEPTANCE_DISPOSITION_KEY} (#3305);`
|
|
311
|
+
: "";
|
|
312
|
+
return {
|
|
313
|
+
path,
|
|
314
|
+
title,
|
|
315
|
+
outcome: "missing",
|
|
316
|
+
detail: `no ${ACCEPTANCE_EVIDENCE_KEY} or ${ACCEPTANCE_DISPOSITION_KEY};` +
|
|
317
|
+
`${bareHint} scope:complete refuses to auto-complete (#3240)`,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
if (hasDisposition) {
|
|
321
|
+
const parsed = parseDisposition(fields.disposition);
|
|
322
|
+
if (!parsed.ok) {
|
|
323
|
+
return { path, title, outcome: "invalid", detail: parsed.message };
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
path,
|
|
327
|
+
title,
|
|
328
|
+
outcome: "disposition",
|
|
329
|
+
detail: `${parsed.record.disposition}: ${parsed.record.reason}`,
|
|
330
|
+
disposition: parsed.record,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const parsed = parseEvidence(fields.evidence);
|
|
334
|
+
if (!parsed.ok) {
|
|
335
|
+
return { path, title, outcome: "invalid", detail: parsed.message };
|
|
336
|
+
}
|
|
337
|
+
const requiredAxes = inferRequiredStrictAxes(item);
|
|
338
|
+
if (!isEvidenceKindSuitable(parsed.record.kind, requiredAxes)) {
|
|
339
|
+
const axes = requiredAxes.length > 0 ? requiredAxes.join("|") : "(none)";
|
|
340
|
+
return {
|
|
341
|
+
path,
|
|
342
|
+
title,
|
|
343
|
+
outcome: "invalid",
|
|
344
|
+
detail: `evidence.kind=${parsed.record.kind} is not suitable for required axis/axes [${axes}]; ` +
|
|
345
|
+
`merge/review alone cannot satisfy smoke|uat|deploy|observed_behavior (#3240)`,
|
|
346
|
+
evidence: parsed.record,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
path,
|
|
351
|
+
title,
|
|
352
|
+
outcome: "evidence",
|
|
353
|
+
detail: `${parsed.record.kind} @ ${parsed.record.pointer}`,
|
|
354
|
+
evidence: parsed.record,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function walkItems(items, pathPrefix, reports) {
|
|
358
|
+
if (!Array.isArray(items)) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
items.forEach((item, index) => {
|
|
362
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const obj = item;
|
|
366
|
+
const path = `${pathPrefix}[${index}]`;
|
|
367
|
+
reports.push(evaluateOneItem(obj, path));
|
|
368
|
+
walkItems(obj.subItems, `${path}.subItems`, reports);
|
|
369
|
+
walkItems(obj.items, `${path}.items`, reports);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Fail closed when any non-terminal plan item lacks suitable evidence or a human-origin disposition.
|
|
374
|
+
*/
|
|
375
|
+
export function evaluateAcceptanceEvidenceGate(plan) {
|
|
376
|
+
const reports = [];
|
|
377
|
+
walkItems(plan.items, "items", reports);
|
|
378
|
+
const blockers = reports.filter((r) => r.outcome === "missing" || r.outcome === "invalid");
|
|
379
|
+
if (blockers.length === 0) {
|
|
380
|
+
const lines = reports
|
|
381
|
+
.filter((r) => r.outcome === "evidence" || r.outcome === "disposition")
|
|
382
|
+
.map((r) => ` - ${r.path} "${r.title}": ${r.outcome} (${r.detail})`);
|
|
383
|
+
const summary = lines.length > 0
|
|
384
|
+
? `Acceptance evidence gate passed (#3240):\n${lines.join("\n")}`
|
|
385
|
+
: "Acceptance evidence gate passed (#3240): no non-terminal criteria";
|
|
386
|
+
return { ok: true, message: summary, reports };
|
|
387
|
+
}
|
|
388
|
+
const lines = blockers.map((r) => ` - ${r.path} "${r.title}": ${r.detail}`);
|
|
389
|
+
return {
|
|
390
|
+
ok: false,
|
|
391
|
+
message: `Acceptance evidence required for scope:complete (#3240 / #3305). ` +
|
|
392
|
+
`${blockers.length} criterion/criteria missing suitable evidence or disposition:\n` +
|
|
393
|
+
`${lines.join("\n")}\n` +
|
|
394
|
+
`Each non-terminal plan item needs ${ACCEPTANCE_EVIDENCE_KEY} ` +
|
|
395
|
+
`{kind: test|review|merge|deploy|smoke|uat|observed_behavior, pointer, recorded_at, recorded_by} ` +
|
|
396
|
+
`or ${ACCEPTANCE_DISPOSITION_KEY} {disposition: waived|deferred|not_applicable, reason, provenance (human-origin), recorded_at}. ` +
|
|
397
|
+
`Bare evidence/disposition keys are not valid (#1620 / #3305). ` +
|
|
398
|
+
`merge/review alone cannot satisfy smoke|uat|deploy|observed_behavior criteria.`,
|
|
399
|
+
reports,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Format a short multi-line listing of evidence/disposition for every criterion
|
|
404
|
+
* (including already-terminal) for complete success output.
|
|
405
|
+
*/
|
|
406
|
+
export function formatAcceptanceCompletionListing(reports) {
|
|
407
|
+
if (reports.length === 0) {
|
|
408
|
+
return "Acceptance criteria: (none)";
|
|
409
|
+
}
|
|
410
|
+
const lines = reports.map((r) => {
|
|
411
|
+
if (r.outcome === "evidence" && r.evidence) {
|
|
412
|
+
return ` - ${r.path} "${r.title}": evidence kind=${r.evidence.kind} pointer=${r.evidence.pointer}`;
|
|
413
|
+
}
|
|
414
|
+
if (r.outcome === "disposition" && r.disposition) {
|
|
415
|
+
return (` - ${r.path} "${r.title}": disposition=${r.disposition.disposition} ` +
|
|
416
|
+
`reason=${r.disposition.reason}`);
|
|
417
|
+
}
|
|
418
|
+
return ` - ${r.path} "${r.title}": ${r.outcome} (${r.detail})`;
|
|
419
|
+
});
|
|
420
|
+
return `Acceptance criteria:\n${lines.join("\n")}`;
|
|
421
|
+
}
|
|
422
|
+
//# sourceMappingURL=acceptance-evidence.js.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parent-requirement coverage map for scope:decompose semantic fidelity (#3238 / epic #3237).
|
|
3
|
+
*
|
|
4
|
+
* When a parent scope authors stable requirement IDs, a decomposition draft must
|
|
5
|
+
* declare a machine-readable coverage map for every ID. Dispositions are a closed
|
|
6
|
+
* enum (covered | deferred | split | behavioral_delta) with required side fields.
|
|
7
|
+
* Negative invariants cannot be silently dropped: omit → fail closed; intentional
|
|
8
|
+
* change requires disposition behavioral_delta with a linked delta record.
|
|
9
|
+
*
|
|
10
|
+
* No LLM/prose semantic-equivalence judgment — structure only (Q1/Q7/Q8 locks).
|
|
11
|
+
*/
|
|
12
|
+
export declare const COVERAGE_DISPOSITIONS: readonly ["covered", "deferred", "split", "behavioral_delta"];
|
|
13
|
+
export type CoverageDisposition = (typeof COVERAGE_DISPOSITIONS)[number];
|
|
14
|
+
export declare const BEHAVIORAL_DELTA_CHANGE_KINDS: readonly ["reorder_stages", "remove_invariant", "weaken_invariant", "add_terminal_outcome", "remove_fallback", "change_fallback_order", "other"];
|
|
15
|
+
export type BehavioralDeltaChangeKind = (typeof BEHAVIORAL_DELTA_CHANGE_KINDS)[number];
|
|
16
|
+
export interface ParentRequirement {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly title: string;
|
|
19
|
+
readonly negativeInvariant: boolean;
|
|
20
|
+
readonly path: string;
|
|
21
|
+
}
|
|
22
|
+
export interface DeferredProvenance {
|
|
23
|
+
readonly reason: string;
|
|
24
|
+
readonly target_story_id?: string;
|
|
25
|
+
readonly target_path?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface CoverageMapEntry {
|
|
28
|
+
readonly parent_requirement_id: string;
|
|
29
|
+
readonly disposition: CoverageDisposition;
|
|
30
|
+
readonly child_story_ids?: readonly string[];
|
|
31
|
+
readonly provenance?: DeferredProvenance;
|
|
32
|
+
readonly split_group?: string;
|
|
33
|
+
readonly part?: string;
|
|
34
|
+
readonly delta_id?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface BehavioralDeltaRecord {
|
|
37
|
+
readonly delta_id: string;
|
|
38
|
+
readonly parent_requirement_ids: readonly string[];
|
|
39
|
+
readonly change_kind: BehavioralDeltaChangeKind;
|
|
40
|
+
readonly summary: string;
|
|
41
|
+
readonly before: string;
|
|
42
|
+
readonly after: string;
|
|
43
|
+
readonly rationale: string;
|
|
44
|
+
}
|
|
45
|
+
export interface CoverageEntryReport {
|
|
46
|
+
readonly parent_requirement_id: string;
|
|
47
|
+
readonly disposition: string | null;
|
|
48
|
+
readonly ok: boolean;
|
|
49
|
+
readonly detail: string;
|
|
50
|
+
readonly negative_invariant: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface CoverageReport {
|
|
53
|
+
readonly schema: "deft.decompose.coverage_report.v1";
|
|
54
|
+
readonly ok: boolean;
|
|
55
|
+
readonly parent_requirement_ids: readonly string[];
|
|
56
|
+
readonly negative_invariant_ids: readonly string[];
|
|
57
|
+
readonly entries: readonly CoverageEntryReport[];
|
|
58
|
+
readonly uncovered: readonly string[];
|
|
59
|
+
readonly errors: readonly string[];
|
|
60
|
+
}
|
|
61
|
+
export interface CoverageValidationResult {
|
|
62
|
+
readonly ok: boolean;
|
|
63
|
+
readonly report: CoverageReport;
|
|
64
|
+
readonly errors: readonly string[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Collect authored parent requirement IDs (Q1).
|
|
68
|
+
* Sources: plan.items (nested) with id; plan.requirements; plan.metadata.requirement_ids.
|
|
69
|
+
*/
|
|
70
|
+
export declare function extractParentRequirements(parent: unknown): ParentRequirement[];
|
|
71
|
+
/**
|
|
72
|
+
* Normalize draft.coverage_map from object-map or array form into entry list.
|
|
73
|
+
*/
|
|
74
|
+
export declare function parseCoverageMap(raw: unknown): {
|
|
75
|
+
entries: CoverageMapEntry[];
|
|
76
|
+
errors: string[];
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Parse draft.behavioral_deltas (array). Records missing required fields are rejected.
|
|
80
|
+
*/
|
|
81
|
+
export declare function parseBehavioralDeltas(raw: unknown): {
|
|
82
|
+
deltas: BehavioralDeltaRecord[];
|
|
83
|
+
errors: string[];
|
|
84
|
+
byId: Map<string, BehavioralDeltaRecord>;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Validate coverage map against parent requirements and optional story ids.
|
|
88
|
+
* When the parent authors zero requirement IDs, validation is a no-op success
|
|
89
|
+
* (backward compatible with pre-#3238 drafts).
|
|
90
|
+
*/
|
|
91
|
+
export declare function validateCoverageMap(opts: {
|
|
92
|
+
parent: unknown;
|
|
93
|
+
draft: unknown;
|
|
94
|
+
storyIds?: readonly string[];
|
|
95
|
+
}): CoverageValidationResult;
|
|
96
|
+
/** Format a single-line machine-readable coverage report for CLI stdout. */
|
|
97
|
+
export declare function formatCoverageReportLine(report: CoverageReport): string;
|
|
98
|
+
//# sourceMappingURL=coverage-map.d.ts.map
|