@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.
Files changed (166) hide show
  1. package/dist/authz/actions.d.ts +6 -0
  2. package/dist/authz/actions.js +3 -0
  3. package/dist/authz/classify.js +241 -30
  4. package/dist/authz/decompose-apply.d.ts +81 -0
  5. package/dist/authz/decompose-apply.js +377 -0
  6. package/dist/authz/index.d.ts +3 -1
  7. package/dist/authz/index.js +3 -1
  8. package/dist/authz/store.d.ts +53 -0
  9. package/dist/authz/store.js +219 -1
  10. package/dist/authz/types.d.ts +19 -2
  11. package/dist/authz/types.js +5 -1
  12. package/dist/check/cached-orchestrator.d.ts +26 -5
  13. package/dist/check/cached-orchestrator.js +228 -6
  14. package/dist/check/gate-lists.d.ts +10 -4
  15. package/dist/check/gate-lists.js +19 -5
  16. package/dist/check/index.d.ts +3 -2
  17. package/dist/check/index.js +2 -1
  18. package/dist/check/named-cause.d.ts +45 -0
  19. package/dist/check/named-cause.js +121 -0
  20. package/dist/check/orchestrator.js +6 -1
  21. package/dist/doctor/checks.d.ts +44 -0
  22. package/dist/doctor/checks.js +428 -2
  23. package/dist/doctor/help.d.ts +5 -0
  24. package/dist/doctor/help.js +31 -0
  25. package/dist/doctor/index.d.ts +2 -0
  26. package/dist/doctor/index.js +2 -0
  27. package/dist/doctor/main.d.ts +6 -7
  28. package/dist/doctor/main.js +130 -79
  29. package/dist/doctor/session-coda.d.ts +82 -0
  30. package/dist/doctor/session-coda.js +151 -0
  31. package/dist/doctor/types.d.ts +16 -0
  32. package/dist/doctor/which.d.ts +20 -1
  33. package/dist/doctor/which.js +67 -1
  34. package/dist/finish-loop/pr-finish-loop.d.ts +13 -2
  35. package/dist/finish-loop/pr-finish-loop.js +116 -5
  36. package/dist/finish-loop/types.d.ts +1 -1
  37. package/dist/hooks/dispatcher.d.ts +9 -5
  38. package/dist/hooks/dispatcher.js +23 -15
  39. package/dist/hooks/readonly.d.ts +6 -3
  40. package/dist/hooks/readonly.js +44 -25
  41. package/dist/index.d.ts +3 -0
  42. package/dist/index.js +3 -0
  43. package/dist/init-deposit/agent-hooks.d.ts +20 -0
  44. package/dist/init-deposit/agent-hooks.js +51 -31
  45. package/dist/init-deposit/gitignore.js +2 -0
  46. package/dist/intake/issue-ingest.js +36 -0
  47. package/dist/lifecycle/completed-consistency.d.ts +60 -0
  48. package/dist/lifecycle/completed-consistency.js +443 -0
  49. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +47 -0
  50. package/dist/lifecycle/completed-tracked-on-delivery.js +415 -0
  51. package/dist/lifecycle/index.d.ts +2 -0
  52. package/dist/lifecycle/index.js +2 -0
  53. package/dist/literal-acceptance/capture.d.ts +52 -0
  54. package/dist/literal-acceptance/capture.js +658 -0
  55. package/dist/literal-acceptance/evaluate.d.ts +47 -0
  56. package/dist/literal-acceptance/evaluate.js +193 -0
  57. package/dist/literal-acceptance/index.d.ts +12 -0
  58. package/dist/literal-acceptance/index.js +12 -0
  59. package/dist/literal-acceptance/run.d.ts +38 -0
  60. package/dist/literal-acceptance/run.js +216 -0
  61. package/dist/literal-acceptance/safety.d.ts +24 -0
  62. package/dist/literal-acceptance/safety.js +251 -0
  63. package/dist/literal-acceptance/types.d.ts +82 -0
  64. package/dist/literal-acceptance/types.js +26 -0
  65. package/dist/policy/ac-pass-banking.d.ts +58 -0
  66. package/dist/policy/ac-pass-banking.js +150 -0
  67. package/dist/policy/ceremony-dial.d.ts +96 -11
  68. package/dist/policy/ceremony-dial.js +250 -23
  69. package/dist/policy/index.d.ts +2 -0
  70. package/dist/policy/index.js +16 -1
  71. package/dist/policy/merge-approval-head.d.ts +129 -0
  72. package/dist/policy/merge-approval-head.js +461 -0
  73. package/dist/policy/require-human-merge.d.ts +5 -0
  74. package/dist/policy/require-human-merge.js +5 -0
  75. package/dist/pr-merge-readiness/ci-gate.d.ts +20 -3
  76. package/dist/pr-merge-readiness/ci-gate.js +65 -5
  77. package/dist/pr-merge-readiness/compute.d.ts +13 -0
  78. package/dist/pr-merge-readiness/compute.js +99 -14
  79. package/dist/pr-merge-readiness/gh.d.ts +49 -0
  80. package/dist/pr-merge-readiness/gh.js +268 -0
  81. package/dist/pr-merge-readiness/index.d.ts +2 -2
  82. package/dist/pr-merge-readiness/index.js +2 -2
  83. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +4 -0
  84. package/dist/pr-wait-mergeable/cascade.d.ts +11 -0
  85. package/dist/pr-wait-mergeable/cascade.js +115 -1
  86. package/dist/pr-wait-mergeable/main.d.ts +4 -0
  87. package/dist/pr-wait-mergeable/main.js +4 -0
  88. package/dist/pr-wait-mergeable/types.d.ts +3 -1
  89. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -1
  90. package/dist/pr-wait-mergeable/wrappers.js +7 -1
  91. package/dist/preflight/evaluate.d.ts +15 -1
  92. package/dist/preflight/evaluate.js +33 -2
  93. package/dist/preflight-cache/evaluate.d.ts +3 -0
  94. package/dist/preflight-cache/evaluate.js +3 -0
  95. package/dist/product-first-done-gate/acceptance.d.ts +25 -0
  96. package/dist/product-first-done-gate/acceptance.js +247 -0
  97. package/dist/product-first-done-gate/check-mode.d.ts +45 -0
  98. package/dist/product-first-done-gate/check-mode.js +148 -0
  99. package/dist/product-first-done-gate/evaluate.d.ts +50 -0
  100. package/dist/product-first-done-gate/evaluate.js +255 -0
  101. package/dist/product-first-done-gate/index.d.ts +11 -0
  102. package/dist/product-first-done-gate/index.js +11 -0
  103. package/dist/product-first-done-gate/types.d.ts +64 -0
  104. package/dist/product-first-done-gate/types.js +61 -0
  105. package/dist/render/framework-commands.js +4 -0
  106. package/dist/run-summary/emit.d.ts +56 -0
  107. package/dist/run-summary/emit.js +171 -0
  108. package/dist/run-summary/index.d.ts +4 -0
  109. package/dist/run-summary/index.js +4 -0
  110. package/dist/run-summary/path.d.ts +25 -0
  111. package/dist/run-summary/path.js +78 -0
  112. package/dist/run-summary/types.d.ts +76 -0
  113. package/dist/run-summary/types.js +21 -0
  114. package/dist/scope/acceptance-evidence.d.ts +110 -0
  115. package/dist/scope/acceptance-evidence.js +422 -0
  116. package/dist/scope/coverage-map.d.ts +98 -0
  117. package/dist/scope/coverage-map.js +558 -0
  118. package/dist/scope/decompose.d.ts +5 -0
  119. package/dist/scope/decompose.js +134 -11
  120. package/dist/scope/index.d.ts +3 -0
  121. package/dist/scope/index.js +3 -0
  122. package/dist/scope/parent-lineage.d.ts +125 -0
  123. package/dist/scope/parent-lineage.js +626 -0
  124. package/dist/scope/transition.d.ts +8 -0
  125. package/dist/scope/transition.js +61 -2
  126. package/dist/session/ac-pass-banking.d.ts +221 -0
  127. package/dist/session/ac-pass-banking.js +761 -0
  128. package/dist/session/active-cli.d.ts +79 -0
  129. package/dist/session/active-cli.js +382 -0
  130. package/dist/session/deposit-sha.d.ts +49 -0
  131. package/dist/session/deposit-sha.js +121 -0
  132. package/dist/session/effort-budget.d.ts +136 -0
  133. package/dist/session/effort-budget.js +352 -0
  134. package/dist/session/index.d.ts +7 -0
  135. package/dist/session/index.js +7 -0
  136. package/dist/session/orientation-compression.d.ts +127 -0
  137. package/dist/session/orientation-compression.js +425 -0
  138. package/dist/session/orientation-state.d.ts +29 -0
  139. package/dist/session/orientation-state.js +85 -0
  140. package/dist/session/session-start.d.ts +31 -0
  141. package/dist/session/session-start.js +228 -2
  142. package/dist/session/toolchain-preflight.d.ts +64 -0
  143. package/dist/session/toolchain-preflight.js +151 -0
  144. package/dist/session/verify-session-ritual.d.ts +14 -0
  145. package/dist/session/verify-session-ritual.js +33 -0
  146. package/dist/story-ready/evaluate.d.ts +11 -0
  147. package/dist/story-ready/evaluate.js +41 -3
  148. package/dist/triage/actions/candidates-log.d.ts +1 -2
  149. package/dist/triage/actions/candidates-log.js +37 -6
  150. package/dist/triage/bootstrap/gitignore.d.ts +1 -1
  151. package/dist/triage/bootstrap/gitignore.js +8 -2
  152. package/dist/triage/bootstrap/index.js +15 -3
  153. package/dist/triage/classify/index.d.ts +1 -0
  154. package/dist/triage/classify/index.js +2 -0
  155. package/dist/triage/classify/label-mirror.js +16 -0
  156. package/dist/triage/classify/mirror-discovery-tip.d.ts +95 -0
  157. package/dist/triage/classify/mirror-discovery-tip.js +234 -0
  158. package/dist/triage/reconcile/reconcile.js +28 -6
  159. package/dist/triage/summary/index.js +12 -4
  160. package/dist/triage/welcome/default-mode.js +16 -0
  161. package/dist/ts-check-lane/run-lane.js +21 -4
  162. package/dist/vbrief-validate/conformance.js +7 -0
  163. package/dist/vitest-runner/coverage-debt-teardown.js +16 -4
  164. package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
  165. package/dist/xbrief-migrate/migrate-project.js +92 -3
  166. package/package.json +15 -3
@@ -0,0 +1,558 @@
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 const COVERAGE_DISPOSITIONS = ["covered", "deferred", "split", "behavioral_delta"];
13
+ export const BEHAVIORAL_DELTA_CHANGE_KINDS = [
14
+ "reorder_stages",
15
+ "remove_invariant",
16
+ "weaken_invariant",
17
+ "add_terminal_outcome",
18
+ "remove_fallback",
19
+ "change_fallback_order",
20
+ "other",
21
+ ];
22
+ const DISPOSITION_SET = new Set(COVERAGE_DISPOSITIONS);
23
+ const CHANGE_KIND_SET = new Set(BEHAVIORAL_DELTA_CHANGE_KINDS);
24
+ function asRecord(value) {
25
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
26
+ return value;
27
+ }
28
+ return null;
29
+ }
30
+ function isNonEmptyString(value) {
31
+ return typeof value === "string" && value.trim().length > 0;
32
+ }
33
+ function asStrList(value) {
34
+ if (value === null || value === undefined)
35
+ return [];
36
+ if (typeof value === "string")
37
+ return value.trim().length > 0 ? [value.trim()] : [];
38
+ if (Array.isArray(value)) {
39
+ return value.map((item) => String(item).trim()).filter((s) => s.length > 0);
40
+ }
41
+ return [];
42
+ }
43
+ function isNegativeInvariant(item) {
44
+ if (item.negative_invariant === true || item.negativeInvariant === true)
45
+ return true;
46
+ const kind = item.kind ?? item.type;
47
+ if (typeof kind === "string" && kind.trim().toLowerCase() === "negative_invariant") {
48
+ return true;
49
+ }
50
+ const narrative = asRecord(item.narrative);
51
+ if (narrative !== null) {
52
+ if (narrative.NegativeInvariant === true || narrative.negative_invariant === true) {
53
+ return true;
54
+ }
55
+ }
56
+ return false;
57
+ }
58
+ /**
59
+ * Collect authored parent requirement IDs (Q1).
60
+ * Sources: plan.items (nested) with id; plan.requirements; plan.metadata.requirement_ids.
61
+ */
62
+ export function extractParentRequirements(parent) {
63
+ const root = asRecord(parent);
64
+ if (root === null)
65
+ return [];
66
+ const plan = asRecord(root.plan) ?? root;
67
+ const byId = new Map();
68
+ function add(id, title, negativeInvariant, path) {
69
+ const trimmed = id.trim();
70
+ if (!trimmed)
71
+ return;
72
+ const existing = byId.get(trimmed);
73
+ if (existing !== undefined) {
74
+ if (negativeInvariant && !existing.negativeInvariant) {
75
+ byId.set(trimmed, { ...existing, negativeInvariant: true });
76
+ }
77
+ return;
78
+ }
79
+ byId.set(trimmed, {
80
+ id: trimmed,
81
+ title: title.trim() || trimmed,
82
+ negativeInvariant,
83
+ path,
84
+ });
85
+ }
86
+ function visitItems(items, prefix) {
87
+ if (!Array.isArray(items))
88
+ return;
89
+ for (let i = 0; i < items.length; i += 1) {
90
+ const item = items[i];
91
+ if (typeof item !== "object" || item === null || Array.isArray(item))
92
+ continue;
93
+ const obj = item;
94
+ const path = `${prefix}[${i}]`;
95
+ if (isNonEmptyString(obj.id)) {
96
+ add(obj.id, String(obj.title ?? obj.id), isNegativeInvariant(obj), path);
97
+ }
98
+ visitItems(obj.items, `${path}.items`);
99
+ visitItems(obj.subItems, `${path}.subItems`);
100
+ }
101
+ }
102
+ visitItems(plan.items, "plan.items");
103
+ const requirements = plan.requirements;
104
+ if (Array.isArray(requirements)) {
105
+ for (let i = 0; i < requirements.length; i += 1) {
106
+ const req = requirements[i];
107
+ if (typeof req === "string" && req.trim()) {
108
+ add(req, req, false, `plan.requirements[${i}]`);
109
+ continue;
110
+ }
111
+ if (typeof req === "object" && req !== null && !Array.isArray(req)) {
112
+ const obj = req;
113
+ if (isNonEmptyString(obj.id)) {
114
+ add(obj.id, String(obj.title ?? obj.id), isNegativeInvariant(obj), `plan.requirements[${i}]`);
115
+ }
116
+ }
117
+ }
118
+ }
119
+ const metadata = asRecord(plan.metadata);
120
+ if (metadata !== null) {
121
+ for (const id of asStrList(metadata.requirement_ids)) {
122
+ add(id, id, false, "plan.metadata.requirement_ids");
123
+ }
124
+ for (const id of asStrList(metadata.requirementIds)) {
125
+ add(id, id, false, "plan.metadata.requirementIds");
126
+ }
127
+ }
128
+ return [...byId.values()];
129
+ }
130
+ function parseDisposition(raw) {
131
+ if (typeof raw !== "string")
132
+ return null;
133
+ const norm = raw.trim().toLowerCase();
134
+ return DISPOSITION_SET.has(norm) ? norm : null;
135
+ }
136
+ function parseChangeKind(raw) {
137
+ if (typeof raw !== "string")
138
+ return null;
139
+ const norm = raw.trim().toLowerCase().replace(/-/g, "_");
140
+ return CHANGE_KIND_SET.has(norm) ? norm : null;
141
+ }
142
+ /**
143
+ * Normalize draft.coverage_map from object-map or array form into entry list.
144
+ */
145
+ export function parseCoverageMap(raw) {
146
+ const errors = [];
147
+ const entries = [];
148
+ if (raw === null || raw === undefined) {
149
+ return { entries, errors };
150
+ }
151
+ const pushEntry = (parentId, body, loc) => {
152
+ const disposition = parseDisposition(body.disposition);
153
+ if (disposition === null) {
154
+ errors.push(`${loc}: disposition must be one of ${COVERAGE_DISPOSITIONS.join("|")} ` +
155
+ `(got ${JSON.stringify(body.disposition ?? null)})`);
156
+ return;
157
+ }
158
+ const entry = {
159
+ parent_requirement_id: parentId,
160
+ disposition,
161
+ };
162
+ const childIds = asStrList(body.child_story_ids ?? body.childStoryIds ?? body.story_ids);
163
+ if (childIds.length > 0)
164
+ entry.child_story_ids = childIds;
165
+ if (disposition === "deferred") {
166
+ const provRaw = asRecord(body.provenance) ?? body;
167
+ const reason = provRaw.reason ?? body.reason;
168
+ const targetStory = provRaw.target_story_id ??
169
+ provRaw.targetStoryId ??
170
+ body.target_story_id ??
171
+ body.targetStoryId;
172
+ const targetPath = provRaw.target_path ?? provRaw.targetPath ?? body.target_path ?? body.targetPath;
173
+ if (!isNonEmptyString(reason)) {
174
+ errors.push(`${loc}: disposition deferred requires provenance.reason`);
175
+ }
176
+ else if (!isNonEmptyString(targetStory) && !isNonEmptyString(targetPath)) {
177
+ errors.push(`${loc}: disposition deferred requires provenance.target_story_id and/or target_path`);
178
+ }
179
+ else {
180
+ entry.provenance = {
181
+ reason: String(reason).trim(),
182
+ ...(isNonEmptyString(targetStory) ? { target_story_id: targetStory.trim() } : {}),
183
+ ...(isNonEmptyString(targetPath) ? { target_path: targetPath.trim() } : {}),
184
+ };
185
+ }
186
+ }
187
+ if (disposition === "split") {
188
+ const group = body.split_group ?? body.splitGroup;
189
+ const part = body.part;
190
+ if (!isNonEmptyString(group)) {
191
+ errors.push(`${loc}: disposition split requires split_group`);
192
+ }
193
+ else {
194
+ entry.split_group = group.trim();
195
+ }
196
+ if (part === null || part === undefined || String(part).trim() === "") {
197
+ errors.push(`${loc}: disposition split requires part`);
198
+ }
199
+ else {
200
+ entry.part = String(part).trim();
201
+ }
202
+ }
203
+ if (disposition === "behavioral_delta") {
204
+ const deltaId = body.delta_id ?? body.deltaId;
205
+ if (!isNonEmptyString(deltaId)) {
206
+ errors.push(`${loc}: disposition behavioral_delta requires delta_id (separate approval hook)`);
207
+ }
208
+ else {
209
+ entry.delta_id = deltaId.trim();
210
+ }
211
+ }
212
+ entries.push(entry);
213
+ };
214
+ if (Array.isArray(raw)) {
215
+ for (let i = 0; i < raw.length; i += 1) {
216
+ const item = raw[i];
217
+ const loc = `coverage_map[${i}]`;
218
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
219
+ errors.push(`${loc}: entry must be an object`);
220
+ continue;
221
+ }
222
+ const obj = item;
223
+ const parentId = obj.parent_requirement_id ?? obj.parentRequirementId ?? obj.id;
224
+ if (!isNonEmptyString(parentId)) {
225
+ errors.push(`${loc}: parent_requirement_id is required`);
226
+ continue;
227
+ }
228
+ pushEntry(parentId.trim(), obj, loc);
229
+ }
230
+ return { entries, errors };
231
+ }
232
+ const map = asRecord(raw);
233
+ if (map === null) {
234
+ errors.push("coverage_map must be an object map or array of entries");
235
+ return { entries, errors };
236
+ }
237
+ for (const [key, value] of Object.entries(map)) {
238
+ const loc = `coverage_map[${key}]`;
239
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
240
+ errors.push(`${loc}: entry must be an object`);
241
+ continue;
242
+ }
243
+ const obj = value;
244
+ const parentId = obj.parent_requirement_id ?? obj.parentRequirementId ?? key;
245
+ if (!isNonEmptyString(parentId)) {
246
+ errors.push(`${loc}: parent_requirement_id is required`);
247
+ continue;
248
+ }
249
+ pushEntry(String(parentId).trim(), obj, loc);
250
+ }
251
+ return { entries, errors };
252
+ }
253
+ /**
254
+ * Parse draft.behavioral_deltas (array). Records missing required fields are rejected.
255
+ */
256
+ export function parseBehavioralDeltas(raw) {
257
+ const errors = [];
258
+ const deltas = [];
259
+ const byId = new Map();
260
+ if (raw === null || raw === undefined) {
261
+ return { deltas, errors, byId };
262
+ }
263
+ if (!Array.isArray(raw)) {
264
+ errors.push("behavioral_deltas must be an array");
265
+ return { deltas, errors, byId };
266
+ }
267
+ for (let i = 0; i < raw.length; i += 1) {
268
+ const item = raw[i];
269
+ const loc = `behavioral_deltas[${i}]`;
270
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
271
+ errors.push(`${loc}: must be an object`);
272
+ continue;
273
+ }
274
+ const obj = item;
275
+ const deltaId = obj.delta_id ?? obj.deltaId;
276
+ const changeKind = parseChangeKind(obj.change_kind ?? obj.changeKind);
277
+ const parentIds = asStrList(obj.parent_requirement_ids ?? obj.parentRequirementIds);
278
+ const summary = obj.summary;
279
+ const before = obj.before;
280
+ const after = obj.after;
281
+ const rationale = obj.rationale;
282
+ const fieldErrors = [];
283
+ if (!isNonEmptyString(deltaId))
284
+ fieldErrors.push("delta_id");
285
+ if (parentIds.length === 0)
286
+ fieldErrors.push("parent_requirement_ids[]");
287
+ if (changeKind === null) {
288
+ fieldErrors.push(`change_kind (${BEHAVIORAL_DELTA_CHANGE_KINDS.join("|")})`);
289
+ }
290
+ if (!isNonEmptyString(summary))
291
+ fieldErrors.push("summary");
292
+ if (!isNonEmptyString(before))
293
+ fieldErrors.push("before");
294
+ if (!isNonEmptyString(after))
295
+ fieldErrors.push("after");
296
+ if (!isNonEmptyString(rationale))
297
+ fieldErrors.push("rationale");
298
+ if (fieldErrors.length > 0) {
299
+ errors.push(`${loc}: missing/invalid fields: ${fieldErrors.join(", ")}`);
300
+ continue;
301
+ }
302
+ const id = String(deltaId).trim();
303
+ if (byId.has(id)) {
304
+ errors.push(`${loc}: duplicate delta_id '${id}'`);
305
+ continue;
306
+ }
307
+ const record = {
308
+ delta_id: id,
309
+ parent_requirement_ids: parentIds,
310
+ change_kind: changeKind,
311
+ summary: String(summary).trim(),
312
+ before: String(before).trim(),
313
+ after: String(after).trim(),
314
+ rationale: String(rationale).trim(),
315
+ };
316
+ deltas.push(record);
317
+ byId.set(id, record);
318
+ }
319
+ return { deltas, errors, byId };
320
+ }
321
+ /**
322
+ * Validate coverage map against parent requirements and optional story ids.
323
+ * When the parent authors zero requirement IDs, validation is a no-op success
324
+ * (backward compatible with pre-#3238 drafts).
325
+ */
326
+ export function validateCoverageMap(opts) {
327
+ const requirements = extractParentRequirements(opts.parent);
328
+ const parentIds = requirements.map((r) => r.id);
329
+ const negativeIds = requirements.filter((r) => r.negativeInvariant).map((r) => r.id);
330
+ const reqById = new Map(requirements.map((r) => [r.id, r]));
331
+ const draft = asRecord(opts.draft) ?? {};
332
+ const parseErrors = [];
333
+ const { entries, errors: mapErrors } = parseCoverageMap(draft.coverage_map ?? draft.coverageMap);
334
+ parseErrors.push(...mapErrors);
335
+ const { byId: deltasById, errors: deltaErrors } = parseBehavioralDeltas(draft.behavioral_deltas ?? draft.behavioralDeltas);
336
+ parseErrors.push(...deltaErrors);
337
+ const entryReports = [];
338
+ const errors = [...parseErrors];
339
+ const coveredIds = new Set();
340
+ // parent_id → set of dispositions seen (conflicting dispositions fail closed)
341
+ const dispositionsByParent = new Map();
342
+ // parent_id → entry count (non-split may appear at most once; split uses parts)
343
+ const entryCountByParent = new Map();
344
+ // split_group → parent_id → set of parts
345
+ const splitParts = new Map();
346
+ if (parentIds.length === 0) {
347
+ const report = {
348
+ schema: "deft.decompose.coverage_report.v1",
349
+ ok: parseErrors.length === 0,
350
+ parent_requirement_ids: [],
351
+ negative_invariant_ids: [],
352
+ entries: [],
353
+ uncovered: [],
354
+ errors: parseErrors,
355
+ };
356
+ return { ok: report.ok, report, errors: parseErrors };
357
+ }
358
+ if (entries.length === 0 && parseErrors.length === 0) {
359
+ errors.push("coverage_map is required when the parent authors requirement IDs; " +
360
+ `uncovered: ${parentIds.join(", ")}`);
361
+ }
362
+ const knownStories = opts.storyIds !== undefined ? new Set(opts.storyIds) : null;
363
+ for (const entry of entries) {
364
+ const req = reqById.get(entry.parent_requirement_id);
365
+ const isNeg = req?.negativeInvariant === true;
366
+ const loc = `coverage_map[${entry.parent_requirement_id}]`;
367
+ if (req === undefined) {
368
+ const detail = `unknown parent_requirement_id '${entry.parent_requirement_id}' (not authored on parent)`;
369
+ errors.push(`${loc}: ${detail}`);
370
+ entryReports.push({
371
+ parent_requirement_id: entry.parent_requirement_id,
372
+ disposition: entry.disposition,
373
+ ok: false,
374
+ detail,
375
+ negative_invariant: false,
376
+ });
377
+ continue;
378
+ }
379
+ coveredIds.add(entry.parent_requirement_id);
380
+ const priorCount = entryCountByParent.get(entry.parent_requirement_id) ?? 0;
381
+ entryCountByParent.set(entry.parent_requirement_id, priorCount + 1);
382
+ let dispSet = dispositionsByParent.get(entry.parent_requirement_id);
383
+ if (dispSet === undefined) {
384
+ dispSet = new Set();
385
+ dispositionsByParent.set(entry.parent_requirement_id, dispSet);
386
+ }
387
+ dispSet.add(entry.disposition);
388
+ if (dispSet.size > 1) {
389
+ const detail = `conflicting dispositions for '${entry.parent_requirement_id}': ` +
390
+ `${[...dispSet].sort().join(", ")} (exactly one disposition per parent ID)`;
391
+ errors.push(`${loc}: ${detail}`);
392
+ entryReports.push({
393
+ parent_requirement_id: entry.parent_requirement_id,
394
+ disposition: entry.disposition,
395
+ ok: false,
396
+ detail,
397
+ negative_invariant: isNeg,
398
+ });
399
+ continue;
400
+ }
401
+ // Non-split dispositions must appear exactly once per parent ID (split uses ≥2 parts).
402
+ if (entry.disposition !== "split" && priorCount >= 1) {
403
+ const detail = `duplicate coverage entries for '${entry.parent_requirement_id}' ` +
404
+ `with disposition '${entry.disposition}' (one entry per parent ID; use split for multi-part)`;
405
+ errors.push(`${loc}: ${detail}`);
406
+ entryReports.push({
407
+ parent_requirement_id: entry.parent_requirement_id,
408
+ disposition: entry.disposition,
409
+ ok: false,
410
+ detail,
411
+ negative_invariant: isNeg,
412
+ });
413
+ continue;
414
+ }
415
+ if (entry.child_story_ids !== undefined && knownStories !== null) {
416
+ for (const sid of entry.child_story_ids) {
417
+ if (!knownStories.has(sid)) {
418
+ errors.push(`${loc}: child_story_ids references unknown story '${sid}'`);
419
+ }
420
+ }
421
+ }
422
+ // deferred.target_story_id may name a future story outside this draft; do not
423
+ // require membership in storyIds (target_path alone is also valid provenance).
424
+ if (entry.disposition === "split") {
425
+ const group = entry.split_group ?? "";
426
+ const part = entry.part ?? "";
427
+ let byParent = splitParts.get(group);
428
+ if (byParent === undefined) {
429
+ byParent = new Map();
430
+ splitParts.set(group, byParent);
431
+ }
432
+ let parts = byParent.get(entry.parent_requirement_id);
433
+ if (parts === undefined) {
434
+ parts = new Set();
435
+ byParent.set(entry.parent_requirement_id, parts);
436
+ }
437
+ if (parts.has(part)) {
438
+ const detail = `duplicate split claim for '${entry.parent_requirement_id}' ` +
439
+ `split_group='${group}' part='${part}' (each part must be unique)`;
440
+ errors.push(`${loc}: ${detail}`);
441
+ entryReports.push({
442
+ parent_requirement_id: entry.parent_requirement_id,
443
+ disposition: entry.disposition,
444
+ ok: false,
445
+ detail,
446
+ negative_invariant: isNeg,
447
+ });
448
+ continue;
449
+ }
450
+ parts.add(part);
451
+ }
452
+ if (entry.disposition === "behavioral_delta") {
453
+ const deltaId = entry.delta_id;
454
+ if (!isNonEmptyString(deltaId)) {
455
+ // Already reported at parse time; keep entry report.
456
+ entryReports.push({
457
+ parent_requirement_id: entry.parent_requirement_id,
458
+ disposition: entry.disposition,
459
+ ok: false,
460
+ detail: "missing delta_id",
461
+ negative_invariant: isNeg,
462
+ });
463
+ continue;
464
+ }
465
+ const delta = deltasById.get(deltaId);
466
+ if (delta === undefined) {
467
+ const detail = `behavioral_delta delta_id '${deltaId}' has no linked record in behavioral_deltas ` +
468
+ `(separate approval hook)`;
469
+ errors.push(`${loc}: ${detail}`);
470
+ entryReports.push({
471
+ parent_requirement_id: entry.parent_requirement_id,
472
+ disposition: entry.disposition,
473
+ ok: false,
474
+ detail,
475
+ negative_invariant: isNeg,
476
+ });
477
+ continue;
478
+ }
479
+ if (!delta.parent_requirement_ids.includes(entry.parent_requirement_id)) {
480
+ const detail = `behavioral_delta '${deltaId}' does not list parent_requirement_id ` +
481
+ `'${entry.parent_requirement_id}'`;
482
+ errors.push(`${loc}: ${detail}`);
483
+ entryReports.push({
484
+ parent_requirement_id: entry.parent_requirement_id,
485
+ disposition: entry.disposition,
486
+ ok: false,
487
+ detail,
488
+ negative_invariant: isNeg,
489
+ });
490
+ continue;
491
+ }
492
+ }
493
+ entryReports.push({
494
+ parent_requirement_id: entry.parent_requirement_id,
495
+ disposition: entry.disposition,
496
+ ok: true,
497
+ detail: `disposition=${entry.disposition}`,
498
+ negative_invariant: isNeg,
499
+ });
500
+ }
501
+ // Incomplete split groups: each parent_id under a split_group needs ≥2 distinct parts.
502
+ // A parent requirement may belong to at most one split_group (ambiguous multi-group fails closed).
503
+ const splitGroupsByParent = new Map();
504
+ for (const [group, byParent] of splitParts) {
505
+ for (const [parentId, parts] of byParent) {
506
+ let groups = splitGroupsByParent.get(parentId);
507
+ if (groups === undefined) {
508
+ groups = new Set();
509
+ splitGroupsByParent.set(parentId, groups);
510
+ }
511
+ groups.add(group);
512
+ if (parts.size < 2) {
513
+ errors.push(`coverage_map split_group '${group}' for '${parentId}' is incomplete ` +
514
+ `(need ≥2 distinct parts; got ${parts.size}: ${[...parts].join(", ") || "(none)"})`);
515
+ }
516
+ }
517
+ }
518
+ for (const [parentId, groups] of splitGroupsByParent) {
519
+ if (groups.size > 1) {
520
+ errors.push(`coverage_map parent '${parentId}' appears in multiple split_groups ` +
521
+ `(${[...groups].sort().join(", ")}); use exactly one split_group per parent ID`);
522
+ }
523
+ }
524
+ const uncovered = parentIds.filter((id) => !coveredIds.has(id));
525
+ if (uncovered.length > 0) {
526
+ errors.push(`uncovered parent requirement IDs: ${uncovered.join(", ")}`);
527
+ for (const id of uncovered) {
528
+ const req = reqById.get(id);
529
+ const isNeg = req?.negativeInvariant === true;
530
+ if (isNeg) {
531
+ errors.push(`negative invariant '${id}' omitted without coverage or behavioral_delta ` +
532
+ `(silent removal is forbidden)`);
533
+ }
534
+ entryReports.push({
535
+ parent_requirement_id: id,
536
+ disposition: null,
537
+ ok: false,
538
+ detail: isNeg ? "negative invariant uncovered (silent removal)" : "uncovered",
539
+ negative_invariant: isNeg,
540
+ });
541
+ }
542
+ }
543
+ const report = {
544
+ schema: "deft.decompose.coverage_report.v1",
545
+ ok: errors.length === 0,
546
+ parent_requirement_ids: parentIds,
547
+ negative_invariant_ids: negativeIds,
548
+ entries: entryReports,
549
+ uncovered,
550
+ errors,
551
+ };
552
+ return { ok: report.ok, report, errors };
553
+ }
554
+ /** Format a single-line machine-readable coverage report for CLI stdout. */
555
+ export function formatCoverageReportLine(report) {
556
+ return `COVERAGE_REPORT ${JSON.stringify(report)}`;
557
+ }
558
+ //# sourceMappingURL=coverage-map.js.map
@@ -34,6 +34,11 @@ export interface ApplyDecompositionOptions {
34
34
  draftPath: string;
35
35
  checkOnly: boolean;
36
36
  date: string;
37
+ /**
38
+ * Optional GitHub-style repo (owner/name). When omitted, resolved via
39
+ * DEFT_TRIAGE_REPO / git remote origin so repo-bound structural grants match (#3291).
40
+ */
41
+ repo?: string | null;
37
42
  }
38
43
  export declare function applyDecomposition(opts: ApplyDecompositionOptions): string[];
39
44
  /** CLI entry for scope-decompose (mirrors scope_decompose.py main()). */