@deftai/directive-core 0.98.0 → 0.99.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 (59) hide show
  1. package/dist/authz/classify.js +400 -56
  2. package/dist/consumer-check-contract/evaluate.d.ts +40 -0
  3. package/dist/consumer-check-contract/evaluate.js +188 -3
  4. package/dist/consumer-check-contract/index.d.ts +1 -1
  5. package/dist/consumer-check-contract/index.js +1 -1
  6. package/dist/content-contracts/skills/greptile-detector.d.ts +42 -0
  7. package/dist/content-contracts/skills/greptile-detector.js +202 -4
  8. package/dist/decision/index.d.ts +17 -0
  9. package/dist/decision/index.js +35 -0
  10. package/dist/decision/list.d.ts +47 -0
  11. package/dist/decision/list.js +250 -0
  12. package/dist/decision/schema.d.ts +88 -0
  13. package/dist/decision/schema.js +293 -0
  14. package/dist/decision/write.d.ts +82 -0
  15. package/dist/decision/write.js +427 -0
  16. package/dist/eval/report.d.ts +29 -0
  17. package/dist/eval/report.js +69 -0
  18. package/dist/eval/run.d.ts +9 -0
  19. package/dist/eval/run.js +40 -4
  20. package/dist/eval/version-pin.d.ts +99 -0
  21. package/dist/eval/version-pin.js +181 -0
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.js +1 -0
  24. package/dist/platform/host-content-surface.d.ts +74 -0
  25. package/dist/platform/host-content-surface.js +214 -0
  26. package/dist/platform/index.d.ts +1 -0
  27. package/dist/platform/index.js +1 -0
  28. package/dist/policy/ceremony-dial.d.ts +233 -0
  29. package/dist/policy/ceremony-dial.js +829 -0
  30. package/dist/policy/deft-directive-disable.js +12 -2
  31. package/dist/policy/index.d.ts +1 -0
  32. package/dist/policy/index.js +15 -1
  33. package/dist/pr-merge-readiness/evaluate.js +10 -0
  34. package/dist/pr-merge-readiness/mergeability.js +5 -0
  35. package/dist/pr-merge-readiness/output.js +2 -0
  36. package/dist/pr-merge-readiness/parse.js +4 -0
  37. package/dist/pr-merge-readiness/types.d.ts +6 -0
  38. package/dist/scope/effort-activate-gate.d.ts +28 -0
  39. package/dist/scope/effort-activate-gate.js +64 -0
  40. package/dist/scope/index.d.ts +1 -0
  41. package/dist/scope/index.js +1 -0
  42. package/dist/scope/transition.js +8 -0
  43. package/dist/scope-provenance/evaluate.d.ts +21 -0
  44. package/dist/scope-provenance/evaluate.js +143 -33
  45. package/dist/scope-provenance/index.d.ts +1 -1
  46. package/dist/scope-provenance/index.js +1 -1
  47. package/dist/session/session-start.d.ts +24 -1
  48. package/dist/session/session-start.js +183 -26
  49. package/dist/swarm/index.d.ts +2 -0
  50. package/dist/swarm/index.js +2 -0
  51. package/dist/swarm/pre-dispatch-cli.d.ts +19 -0
  52. package/dist/swarm/pre-dispatch-cli.js +143 -0
  53. package/dist/swarm/pre-dispatch.d.ts +87 -0
  54. package/dist/swarm/pre-dispatch.js +373 -0
  55. package/dist/vbrief-activate/activate.js +6 -0
  56. package/dist/vbrief-validate/constants.d.ts +2 -0
  57. package/dist/vbrief-validate/constants.js +2 -0
  58. package/dist/vbrief-validate/schema.js +4 -1
  59. package/package.json +15 -3
@@ -0,0 +1,373 @@
1
+ /**
2
+ * Swarm implement-leaf pre-dispatch gate (#3228).
3
+ *
4
+ * Wires #3143 delivery-attempt DENY_DUPLICATE_ACTIVE onto the swarm re-dispatch
5
+ * path: before starting a peer implement leaf on a unit, if a non-terminal
6
+ * attempt already exists → exit non-zero and do not spawn.
7
+ *
8
+ * Takeover is two steps: cancel (complete status=cancelled) the prior attempt,
9
+ * then pre-dispatch begin again — never concurrent dual active.
10
+ */
11
+ import { existsSync, realpathSync } from "node:fs";
12
+ import { isAbsolute, normalize, resolve } from "node:path";
13
+ import { activeAttempts, beginAttempt, completeAttemptOnDisk, evaluatePreDispatch, hasActiveAttempt, listUnitLedgers, loadOrCreateUnitLedger, loadUnitLedger, markBlocked, saveUnitLedger, withUnitLock, } from "../delivery-attempt/index.js";
14
+ import { EXIT_CONFIG_ERROR, EXIT_GATE_FAILED, EXIT_OK } from "./constants.js";
15
+ import { runText } from "./subprocess.js";
16
+ /** Default workflow id for drive-to:merge-ready implement leaves. */
17
+ export const IMPLEMENT_LEAF_WORKFLOW_ID = "drive-to:merge-ready";
18
+ export const PRE_DISPATCH_ACTIONS = ["begin", "complete", "cancel"];
19
+ export const COMPLETE_STATUSES = ["succeeded", "failed", "cancelled", "blocked"];
20
+ export function resolveSourceRevision(projectRoot, explicit) {
21
+ if (explicit !== undefined && explicit.trim().length > 0) {
22
+ return explicit.trim();
23
+ }
24
+ const captured = runText(["git", "rev-parse", "HEAD"], { cwd: projectRoot });
25
+ if (captured.returncode === 0) {
26
+ const sha = captured.stdout.trim();
27
+ if (sha.length > 0)
28
+ return sha;
29
+ }
30
+ return "unknown";
31
+ }
32
+ /**
33
+ * True when targetId should be treated as a filesystem path (worktree), not an
34
+ * opaque branch/ref id. Branch names with `/` (e.g. `feat/foo`) stay opaque.
35
+ */
36
+ export function looksLikeFilesystemTarget(targetId) {
37
+ const t = targetId.trim();
38
+ if (t.length === 0)
39
+ return false;
40
+ if (isAbsolute(t) || t.startsWith(".") || t.includes("\\"))
41
+ return true;
42
+ if (/^[A-Za-z]:[\\/]/.test(t))
43
+ return true;
44
+ const lower = t.toLowerCase().replace(/\\/g, "/");
45
+ return (lower.includes(".deft-scratch/") ||
46
+ lower.includes("/worktrees/") ||
47
+ lower.startsWith("worktrees/"));
48
+ }
49
+ /**
50
+ * Canonical unit target for ledger keys so relative/absolute/separator/case
51
+ * variants of the same worktree do not split gate state (#3228 Greptile P1).
52
+ *
53
+ * Always resolve under projectRoot to a stable absolute lexical key (even
54
+ * before the path exists). Do **not** realpath: following a symlink that is
55
+ * created between dispatches would change the key and split ledgers.
56
+ * Case-fold prevents case-insensitive FS splits. Existence never changes
57
+ * the ledger key.
58
+ */
59
+ export function normalizeTargetId(projectRoot, targetId) {
60
+ const trimmed = targetId.trim();
61
+ if (trimmed.length === 0)
62
+ return trimmed;
63
+ let pathKey = normalize(resolve(projectRoot, trimmed)).replace(/\\/g, "/").toLowerCase();
64
+ if (pathKey.length > 1 && pathKey.endsWith("/")) {
65
+ pathKey = pathKey.slice(0, -1);
66
+ }
67
+ return pathKey;
68
+ }
69
+ /**
70
+ * Best-effort physical identity for alias peer checks. Ledger keys stay lexical
71
+ * (stable); when a path exists, realpath groups symlink aliases that point at
72
+ * the same worktree so sequential begins still DENY.
73
+ */
74
+ export function physicalTargetKey(projectRoot, targetId) {
75
+ const lexical = normalizeTargetId(projectRoot, targetId);
76
+ const candidates = [lexical, resolve(projectRoot, targetId.trim()), targetId.trim()];
77
+ for (const candidate of candidates) {
78
+ if (candidate.length === 0 || !existsSync(candidate))
79
+ continue;
80
+ try {
81
+ return normalize(realpathSync(candidate)).replace(/\\/g, "/").toLowerCase();
82
+ }
83
+ catch {
84
+ /* try next candidate */
85
+ }
86
+ }
87
+ return lexical;
88
+ }
89
+ /** True when another unit (same scope+workflow, different target key) is active on same physical path. */
90
+ export function hasActiveAliasPeer(projectRoot, scopeId, targetId, workflowId) {
91
+ const mine = physicalTargetKey(projectRoot, targetId);
92
+ for (const ledger of listUnitLedgers(projectRoot)) {
93
+ if (ledger.scopeId !== scopeId || ledger.workflowId !== workflowId)
94
+ continue;
95
+ if (ledger.targetId === targetId)
96
+ continue;
97
+ if (!hasActiveAttempt(ledger))
98
+ continue;
99
+ if (physicalTargetKey(projectRoot, ledger.targetId) === mine) {
100
+ return true;
101
+ }
102
+ }
103
+ return false;
104
+ }
105
+ function unitFields(input) {
106
+ return {
107
+ scopeId: input.scopeId.trim(),
108
+ targetId: normalizeTargetId(input.projectRoot, input.targetId),
109
+ workflowId: (input.workflowId ?? IMPLEMENT_LEAF_WORKFLOW_ID).trim(),
110
+ };
111
+ }
112
+ function baseResult(input, action, partial) {
113
+ const { scopeId, targetId, workflowId } = unitFields(input);
114
+ return {
115
+ exitCode: partial.exitCode,
116
+ decision: partial.decision,
117
+ reason: partial.reason,
118
+ action,
119
+ scopeId,
120
+ targetId,
121
+ workflowId,
122
+ attempt: partial.attempt ?? null,
123
+ activeAttemptIds: partial.activeAttemptIds ?? [],
124
+ };
125
+ }
126
+ function listActiveIds(projectRoot, scopeId, targetId, workflowId) {
127
+ const ledger = loadUnitLedger(projectRoot, scopeId, targetId, workflowId);
128
+ if (ledger === null)
129
+ return [];
130
+ return activeAttempts(ledger).map((a) => a.attemptId);
131
+ }
132
+ /**
133
+ * Parse beginAttemptOnDisk-style error messages without a ReDoS-prone regex
134
+ * (CodeQL: polynomial regex on uncontrolled data).
135
+ */
136
+ export function parseDecisionFromError(message) {
137
+ const prefix = "delivery-attempt ";
138
+ if (!message.startsWith(prefix)) {
139
+ return { decision: null, reason: message };
140
+ }
141
+ const rest = message.slice(prefix.length);
142
+ const colon = rest.indexOf(":");
143
+ if (colon <= 0) {
144
+ return { decision: null, reason: message };
145
+ }
146
+ const code = rest.slice(0, colon).trim();
147
+ // Bound check: decision codes are short fixed tokens (ALLOW_*/DENY_*/BLOCK_*).
148
+ if (code.length > 64 || code.length < 6) {
149
+ return { decision: null, reason: message };
150
+ }
151
+ if (!code.startsWith("ALLOW_") && !code.startsWith("DENY_") && !code.startsWith("BLOCK_")) {
152
+ return { decision: null, reason: message };
153
+ }
154
+ for (let i = 0; i < code.length; i += 1) {
155
+ const c = code.charCodeAt(i);
156
+ const ok = (c >= 65 && c <= 90) || // A-Z
157
+ (c >= 48 && c <= 57) || // 0-9
158
+ c === 95; // _
159
+ if (!ok) {
160
+ return { decision: null, reason: message };
161
+ }
162
+ }
163
+ const reason = rest.slice(colon + 1).trim();
164
+ return {
165
+ decision: code,
166
+ reason: reason.length > 0 ? reason : message,
167
+ };
168
+ }
169
+ function runBegin(input) {
170
+ const { scopeId, targetId, workflowId } = unitFields(input);
171
+ const sourceRevision = resolveSourceRevision(input.projectRoot, input.sourceRevision);
172
+ const trigger = input.trigger ?? "automatic";
173
+ // Exclusive lock → reload → evaluate → begin+save (same decision under lock;
174
+ // no stale preview decision on the success path).
175
+ try {
176
+ return withUnitLock(input.projectRoot, scopeId, targetId, workflowId, () => {
177
+ // Symlink alias peer: different lexical keys, same physical worktree.
178
+ if (hasActiveAliasPeer(input.projectRoot, scopeId, targetId, workflowId)) {
179
+ return baseResult(input, "begin", {
180
+ exitCode: EXIT_GATE_FAILED,
181
+ decision: "DENY_DUPLICATE_ACTIVE",
182
+ reason: "active attempt exists on alias/realpath peer of target",
183
+ activeAttemptIds: listActiveIds(input.projectRoot, scopeId, targetId, workflowId),
184
+ });
185
+ }
186
+ const current = loadOrCreateUnitLedger(input.projectRoot, {
187
+ scopeId,
188
+ targetId,
189
+ workflowId,
190
+ now: input.now,
191
+ });
192
+ const decision = evaluatePreDispatch(current, {
193
+ scopeId,
194
+ targetId,
195
+ workflowId,
196
+ sourceRevision,
197
+ trigger,
198
+ now: input.now,
199
+ });
200
+ if (!decision.allowed) {
201
+ if (decision.handoff !== null) {
202
+ const blocked = markBlocked(current, decision.decision, decision.handoff.resumeCondition, input.now);
203
+ saveUnitLedger(input.projectRoot, blocked);
204
+ }
205
+ return baseResult(input, "begin", {
206
+ exitCode: EXIT_GATE_FAILED,
207
+ decision: decision.decision,
208
+ reason: decision.reason,
209
+ activeAttemptIds: activeAttempts(current).map((a) => a.attemptId),
210
+ });
211
+ }
212
+ const { ledger, attempt } = beginAttempt(current, {
213
+ sourceRevision,
214
+ trigger,
215
+ status: "running",
216
+ workerId: input.workerId ?? null,
217
+ externalRunId: input.externalRunId ?? null,
218
+ now: input.now,
219
+ consumeOverride: decision.decision === "ALLOW_OVERRIDE",
220
+ });
221
+ saveUnitLedger(input.projectRoot, ledger);
222
+ return baseResult(input, "begin", {
223
+ exitCode: EXIT_OK,
224
+ decision: decision.decision,
225
+ reason: `allowed; attempt ${attempt.attemptId} begun`,
226
+ attempt,
227
+ activeAttemptIds: [attempt.attemptId],
228
+ });
229
+ });
230
+ }
231
+ catch (err) {
232
+ const message = err instanceof Error ? err.message : String(err);
233
+ const parsed = parseDecisionFromError(message);
234
+ if (parsed.decision !== null) {
235
+ return baseResult(input, "begin", {
236
+ exitCode: EXIT_GATE_FAILED,
237
+ decision: parsed.decision,
238
+ reason: parsed.reason,
239
+ activeAttemptIds: listActiveIds(input.projectRoot, scopeId, targetId, workflowId),
240
+ });
241
+ }
242
+ return baseResult(input, "begin", {
243
+ exitCode: EXIT_CONFIG_ERROR,
244
+ decision: null,
245
+ reason: message,
246
+ activeAttemptIds: listActiveIds(input.projectRoot, scopeId, targetId, workflowId),
247
+ });
248
+ }
249
+ }
250
+ function runComplete(input, action) {
251
+ const { scopeId, targetId, workflowId } = unitFields(input);
252
+ const status = action === "cancel" ? "cancelled" : (input.status ?? "succeeded");
253
+ const ledger = loadUnitLedger(input.projectRoot, scopeId, targetId, workflowId);
254
+ if (ledger === null) {
255
+ return baseResult(input, action, {
256
+ exitCode: EXIT_GATE_FAILED,
257
+ decision: null,
258
+ reason: "no delivery-attempt ledger for unit",
259
+ activeAttemptIds: [],
260
+ });
261
+ }
262
+ const actives = activeAttempts(ledger);
263
+ if (actives.length === 0 && input.attemptId === undefined && input.externalRunId === undefined) {
264
+ return baseResult(input, action, {
265
+ exitCode: EXIT_GATE_FAILED,
266
+ decision: null,
267
+ reason: "no active attempt to complete/cancel",
268
+ activeAttemptIds: [],
269
+ });
270
+ }
271
+ try {
272
+ const next = completeAttemptOnDisk(input.projectRoot, {
273
+ scopeId,
274
+ targetId,
275
+ workflowId,
276
+ attemptId: input.attemptId,
277
+ externalRunId: input.externalRunId ?? null,
278
+ status,
279
+ now: input.now,
280
+ });
281
+ const closed = next.attempts.find((a) => a.attemptId === input.attemptId) ??
282
+ next.attempts.filter((a) => a.endedAt !== null).at(-1) ??
283
+ null;
284
+ return baseResult(input, action, {
285
+ exitCode: EXIT_OK,
286
+ decision: null,
287
+ reason: `attempt ${closed?.attemptId ?? "?"} marked ${status}`,
288
+ attempt: closed,
289
+ activeAttemptIds: activeAttempts(next).map((a) => a.attemptId),
290
+ });
291
+ }
292
+ catch (err) {
293
+ const message = err instanceof Error ? err.message : String(err);
294
+ return baseResult(input, action, {
295
+ exitCode: EXIT_CONFIG_ERROR,
296
+ decision: null,
297
+ reason: message,
298
+ activeAttemptIds: listActiveIds(input.projectRoot, scopeId, targetId, workflowId),
299
+ });
300
+ }
301
+ }
302
+ /**
303
+ * Swarm pre-dispatch gate for implement leaves.
304
+ *
305
+ * - begin (default): evaluate #3143 gate; on allow, beginAttempt; exit 0 / 1 / 2
306
+ * - complete: terminal success/fail/blocked
307
+ * - cancel: terminal cancel (takeover step 1)
308
+ */
309
+ export function swarmPreDispatch(input) {
310
+ const action = input.action ?? "begin";
311
+ const { scopeId, targetId, workflowId } = unitFields(input);
312
+ if (scopeId.length === 0) {
313
+ return baseResult(input, action, {
314
+ exitCode: EXIT_CONFIG_ERROR,
315
+ decision: null,
316
+ reason: "--scope-id is required (story/issue or xBRIEF plan id)",
317
+ });
318
+ }
319
+ if (targetId.length === 0) {
320
+ return baseResult(input, action, {
321
+ exitCode: EXIT_CONFIG_ERROR,
322
+ decision: null,
323
+ reason: "--target-id is required (worktree path or branch)",
324
+ });
325
+ }
326
+ if (workflowId.length === 0) {
327
+ return baseResult(input, action, {
328
+ exitCode: EXIT_CONFIG_ERROR,
329
+ decision: null,
330
+ reason: "--workflow-id must be non-empty",
331
+ });
332
+ }
333
+ if (!PRE_DISPATCH_ACTIONS.includes(action)) {
334
+ return baseResult(input, "begin", {
335
+ exitCode: EXIT_CONFIG_ERROR,
336
+ decision: null,
337
+ reason: `--action must be one of: ${PRE_DISPATCH_ACTIONS.join(", ")}`,
338
+ });
339
+ }
340
+ if (action === "complete" &&
341
+ input.status !== undefined &&
342
+ !COMPLETE_STATUSES.includes(input.status)) {
343
+ return baseResult(input, action, {
344
+ exitCode: EXIT_CONFIG_ERROR,
345
+ decision: null,
346
+ reason: `--status must be one of: ${COMPLETE_STATUSES.join(", ")}`,
347
+ });
348
+ }
349
+ if (action === "begin")
350
+ return runBegin(input);
351
+ return runComplete(input, action);
352
+ }
353
+ /** Human-readable one-line report for CLI stdout. */
354
+ export function formatPreDispatchReport(result) {
355
+ const unit = `${result.scopeId} / ${result.targetId} / ${result.workflowId}`;
356
+ const decision = result.decision !== null ? result.decision : "n/a";
357
+ const active = result.activeAttemptIds.length > 0 ? result.activeAttemptIds.join(",") : "(none)";
358
+ const lines = [
359
+ `[swarm:pre-dispatch] action=${result.action} exit=${result.exitCode}`,
360
+ ` unit: ${unit}`,
361
+ ` decision: ${decision}`,
362
+ ` reason: ${result.reason}`,
363
+ ` active: ${active}`,
364
+ ];
365
+ if (result.attempt !== null) {
366
+ lines.push(` attempt: ${result.attempt.attemptId} status=${result.attempt.status}`);
367
+ }
368
+ if (result.exitCode === EXIT_GATE_FAILED && result.decision === "DENY_DUPLICATE_ACTIVE") {
369
+ lines.push(" hint: do not spawn; resume the live leaf, or takeover = cancel then pre-dispatch begin");
370
+ }
371
+ return lines.join("\n");
372
+ }
373
+ //# sourceMappingURL=pre-dispatch.js.map
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, unli
2
2
  import { basename, dirname, resolve } from "node:path";
3
3
  import { containedWrite } from "../fs/contained-write.js";
4
4
  import { assertProjectionContained, ProjectionContainmentError, } from "../fs/projection-containment.js";
5
+ import { evaluateEffortActivateGate } from "../scope/effort-activate-gate.js";
5
6
  import { utcNowIso } from "../scope/vbrief-json.js";
6
7
  import { pythonJsonPretty } from "../vbrief-build/json.js";
7
8
  import { ACTIVE_FOLDER, ELIGIBLE_STATUSES_FOR_FLIP, formatEligibleStatusList, SOURCE_FOLDERS, TARGET_STATUS, } from "./constants.js";
@@ -121,6 +122,11 @@ export function activate(vbriefPath, options = {}) {
121
122
  `'${TARGET_STATUS}'.`,
122
123
  };
123
124
  }
125
+ // #1581: fail closed when any plan item still has effort=XL (needs breakdown).
126
+ const effortGate = evaluateEffortActivateGate(planObj);
127
+ if (!effortGate.ok) {
128
+ return { exitCode: 1, message: effortGate.message };
129
+ }
124
130
  // Resolve destination early so containment can refuse before any mutation.
125
131
  // Parity with scope:activate / #2447: projectRoot is parent of the xbrief/ root.
126
132
  const vbriefDir = dirname(dirname(vbriefPath));
@@ -9,6 +9,8 @@ export declare const VALID_VBRIEF_VERSIONS: Set<string>;
9
9
  export declare const VALID_INFO_ROOT_KEYS: Set<string>;
10
10
  /** v0.8 PlanItem.type enum values (optional field). */
11
11
  export declare const VALID_PLAN_ITEM_TYPES: Set<string>;
12
+ /** Optional PlanItem.effort enum (#1581). Time anchors: S <2h, M 2-4h, L 1-2d, XL needs breakdown. */
13
+ export declare const VALID_PLAN_ITEM_EFFORTS: Set<string>;
12
14
  /** D13: status-to-folder mapping (#533 adds ``failed`` in completed/). */
13
15
  export declare const FOLDER_ALLOWED_STATUSES: Readonly<Record<string, ReadonlySet<string>>>;
14
16
  export declare const LIFECYCLE_FOLDERS: string[];
@@ -19,6 +19,8 @@ export const VALID_VBRIEF_VERSIONS = new Set(["0.6", "0.8"]);
19
19
  export const VALID_INFO_ROOT_KEYS = new Set(["vBRIEFInfo", "xBRIEFInfo"]);
20
20
  /** v0.8 PlanItem.type enum values (optional field). */
21
21
  export const VALID_PLAN_ITEM_TYPES = new Set(["task", "group", "milestone", "epic"]);
22
+ /** Optional PlanItem.effort enum (#1581). Time anchors: S <2h, M 2-4h, L 1-2d, XL needs breakdown. */
23
+ export const VALID_PLAN_ITEM_EFFORTS = new Set(["S", "M", "L", "XL"]);
22
24
  /** D13: status-to-folder mapping (#533 adds ``failed`` in completed/). */
23
25
  export const FOLDER_ALLOWED_STATUSES = {
24
26
  proposed: new Set(["draft", "proposed"]),
@@ -1,5 +1,5 @@
1
1
  import { pyStrRepr, pythonTypeName } from "../triage/scope/python-repr.js";
2
- import { PROJECT_DEF_EXPECTED_NARRATIVES, VALID_INFO_ROOT_KEYS, VALID_ITEM_STATUSES, VALID_PLAN_ITEM_TYPES, VALID_PLAN_STATUSES, VALID_VBRIEF_VERSIONS, } from "./constants.js";
2
+ import { PROJECT_DEF_EXPECTED_NARRATIVES, VALID_INFO_ROOT_KEYS, VALID_ITEM_STATUSES, VALID_PLAN_ITEM_EFFORTS, VALID_PLAN_ITEM_TYPES, VALID_PLAN_STATUSES, VALID_VBRIEF_VERSIONS, } from "./constants.js";
3
3
  function validateNarratives(narratives, path, errors) {
4
4
  if (typeof narratives !== "object" || narratives === null || Array.isArray(narratives)) {
5
5
  errors.push(`${path} must be an object`);
@@ -50,6 +50,9 @@ function validatePlanItem(item, path, errors) {
50
50
  if ("type" in item && !VALID_PLAN_ITEM_TYPES.has(String(item.type))) {
51
51
  errors.push(`${itemPath} invalid type: ${pyStrRepr(String(item.type))}`);
52
52
  }
53
+ if ("effort" in item && !VALID_PLAN_ITEM_EFFORTS.has(String(item.effort))) {
54
+ errors.push(`${itemPath} invalid effort: ${pyStrRepr(String(item.effort))}`);
55
+ }
53
56
  if ("summary" in item && typeof item.summary !== "string") {
54
57
  errors.push(`${itemPath}.summary must be a string, got ${pythonTypeName(item.summary)}`);
55
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deftai/directive-core",
3
- "version": "0.98.0",
3
+ "version": "0.99.0",
4
4
  "description": "TypeScript engine core for the Directive framework.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -131,6 +131,14 @@
131
131
  "types": "./dist/value/readback.d.ts",
132
132
  "default": "./dist/value/readback.js"
133
133
  },
134
+ "./decision": {
135
+ "types": "./dist/decision/index.d.ts",
136
+ "default": "./dist/decision/index.js"
137
+ },
138
+ "./decision/index": {
139
+ "types": "./dist/decision/index.d.ts",
140
+ "default": "./dist/decision/index.js"
141
+ },
134
142
  "./events": {
135
143
  "types": "./dist/events/attribution-ledger.d.ts",
136
144
  "default": "./dist/events/attribution-ledger.js"
@@ -147,6 +155,10 @@
147
155
  "types": "./dist/eval/report.d.ts",
148
156
  "default": "./dist/eval/report.js"
149
157
  },
158
+ "./eval/version-pin": {
159
+ "types": "./dist/eval/version-pin.d.ts",
160
+ "default": "./dist/eval/version-pin.js"
161
+ },
150
162
  "./triage": {
151
163
  "types": "./dist/triage/index.d.ts",
152
164
  "default": "./dist/triage/index.js"
@@ -354,8 +366,8 @@
354
366
  "provenance": true
355
367
  },
356
368
  "dependencies": {
357
- "@deftai/directive-content": "^0.98.0",
358
- "@deftai/directive-types": "^0.98.0",
369
+ "@deftai/directive-content": "^0.99.0",
370
+ "@deftai/directive-types": "^0.99.0",
359
371
  "archiver": "^8.0.0"
360
372
  },
361
373
  "scripts": {