@deftai/directive-core 0.98.1 → 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 (55) hide show
  1. package/dist/authz/classify.js +265 -73
  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/session/session-start.d.ts +24 -1
  44. package/dist/session/session-start.js +183 -26
  45. package/dist/swarm/index.d.ts +2 -0
  46. package/dist/swarm/index.js +2 -0
  47. package/dist/swarm/pre-dispatch-cli.d.ts +19 -0
  48. package/dist/swarm/pre-dispatch-cli.js +143 -0
  49. package/dist/swarm/pre-dispatch.d.ts +87 -0
  50. package/dist/swarm/pre-dispatch.js +373 -0
  51. package/dist/vbrief-activate/activate.js +6 -0
  52. package/dist/vbrief-validate/constants.d.ts +2 -0
  53. package/dist/vbrief-validate/constants.js +2 -0
  54. package/dist/vbrief-validate/schema.js +4 -1
  55. package/package.json +15 -3
@@ -0,0 +1,250 @@
1
+ /**
2
+ * decision:list — find structured decision records (#1396).
3
+ */
4
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
5
+ import { join, resolve } from "node:path";
6
+ import { resolveProjectRoot } from "../scope/project-context.js";
7
+ import { DECISION_FILE_SUFFIX, DECISIONS_DIR_REL, sanitizeForTerminal, validateDecisionRecord, } from "./schema.js";
8
+ function isDecisionFile(name) {
9
+ return name.endsWith(DECISION_FILE_SUFFIX);
10
+ }
11
+ function loadRecord(absPath, relPath) {
12
+ try {
13
+ const raw = readFileSync(absPath, "utf8");
14
+ const parsed = JSON.parse(raw);
15
+ const validated = validateDecisionRecord(parsed);
16
+ if (!validated.ok || validated.record === undefined) {
17
+ return {
18
+ path: relPath,
19
+ id: "(invalid)",
20
+ decision: `unparseable: ${validated.errors.map((e) => e.message).join("; ")}`,
21
+ confidence: "?",
22
+ timestamp: "",
23
+ revisitTrigger: "",
24
+ activeScopeRefs: [],
25
+ tags: [],
26
+ relatedIssues: [],
27
+ };
28
+ }
29
+ const r = validated.record;
30
+ return {
31
+ path: relPath,
32
+ id: r.id,
33
+ decision: r.decision,
34
+ confidence: r.confidence,
35
+ timestamp: r.timestamp,
36
+ revisitTrigger: r.revisitTrigger,
37
+ activeScopeRefs: r.activeScopeRefs,
38
+ tags: r.tags ?? [],
39
+ relatedIssues: r.relatedIssues ?? [],
40
+ };
41
+ }
42
+ catch (err) {
43
+ return {
44
+ path: relPath,
45
+ id: "(error)",
46
+ decision: err instanceof Error ? err.message : String(err),
47
+ confidence: "?",
48
+ timestamp: "",
49
+ revisitTrigger: "",
50
+ activeScopeRefs: [],
51
+ tags: [],
52
+ relatedIssues: [],
53
+ };
54
+ }
55
+ }
56
+ function scanDecisionsDir(projectRoot) {
57
+ const dir = resolve(projectRoot, DECISIONS_DIR_REL);
58
+ if (!existsSync(dir)) {
59
+ return [];
60
+ }
61
+ let names;
62
+ try {
63
+ names = readdirSync(dir).filter(isDecisionFile);
64
+ }
65
+ catch {
66
+ return [];
67
+ }
68
+ const entries = [];
69
+ for (const name of names) {
70
+ const abs = join(dir, name);
71
+ try {
72
+ if (!statSync(abs).isFile())
73
+ continue;
74
+ }
75
+ catch {
76
+ continue;
77
+ }
78
+ const rel = `${DECISIONS_DIR_REL}/${name}`.replace(/\\/g, "/");
79
+ const entry = loadRecord(abs, rel);
80
+ if (entry !== null)
81
+ entries.push(entry);
82
+ }
83
+ entries.sort((a, b) => {
84
+ if (a.timestamp === b.timestamp)
85
+ return a.path.localeCompare(b.path);
86
+ return a.timestamp < b.timestamp ? 1 : -1;
87
+ });
88
+ return entries;
89
+ }
90
+ function matchesFilters(entry, options) {
91
+ if (options.query !== undefined && options.query !== null && options.query.trim().length > 0) {
92
+ const q = options.query.trim().toLowerCase();
93
+ const hay = [
94
+ entry.id,
95
+ entry.decision,
96
+ entry.revisitTrigger,
97
+ ...entry.tags,
98
+ ...entry.activeScopeRefs,
99
+ ]
100
+ .join(" ")
101
+ .toLowerCase();
102
+ if (!hay.includes(q))
103
+ return false;
104
+ }
105
+ if (options.scope !== undefined && options.scope !== null && options.scope.trim().length > 0) {
106
+ const s = options.scope.trim().replace(/\\/g, "/").toLowerCase();
107
+ if (!entry.activeScopeRefs.some((r) => r.toLowerCase().includes(s)))
108
+ return false;
109
+ }
110
+ if (options.issue !== undefined && options.issue !== null) {
111
+ if (!entry.relatedIssues.includes(options.issue))
112
+ return false;
113
+ }
114
+ return true;
115
+ }
116
+ /** List decision records under xbrief/decisions/. */
117
+ export function runDecisionList(options = {}) {
118
+ const projectRootRaw = resolveProjectRoot(options.projectRoot ?? undefined);
119
+ if (projectRootRaw === null) {
120
+ return {
121
+ exitCode: 2,
122
+ entries: [],
123
+ message: "Error: could not resolve project root. Pass --project-root or run from a directive repo.\n",
124
+ };
125
+ }
126
+ const projectRoot = resolve(projectRootRaw);
127
+ let entries = scanDecisionsDir(projectRoot).filter((e) => matchesFilters(e, options));
128
+ if (options.limit !== undefined && options.limit !== null && options.limit > 0) {
129
+ entries = entries.slice(0, options.limit);
130
+ }
131
+ if (entries.length === 0) {
132
+ return {
133
+ exitCode: 0,
134
+ entries: [],
135
+ message: `No decision records under ${DECISIONS_DIR_REL}/.\n`,
136
+ };
137
+ }
138
+ const lines = entries.map((e) => {
139
+ const scopes = e.activeScopeRefs.length > 0
140
+ ? ` scope=${sanitizeForTerminal(e.activeScopeRefs.join(","))}`
141
+ : "";
142
+ const tags = e.tags.length > 0 ? ` tags=${sanitizeForTerminal(e.tags.join(","))}` : "";
143
+ return (`${sanitizeForTerminal(e.path)}\n` +
144
+ ` ${sanitizeForTerminal(e.decision)}\n` +
145
+ ` confidence=${sanitizeForTerminal(e.confidence)} ts=${sanitizeForTerminal(e.timestamp)}` +
146
+ `${scopes}${tags}\n` +
147
+ ` revisit: ${sanitizeForTerminal(e.revisitTrigger)}`);
148
+ });
149
+ return {
150
+ exitCode: 0,
151
+ entries,
152
+ message: `${lines.join("\n\n")}\n`,
153
+ };
154
+ }
155
+ /** Parse argv for decision:list. */
156
+ export function parseDecisionListArgs(argv) {
157
+ const out = {};
158
+ for (let i = 0; i < argv.length; i += 1) {
159
+ const arg = argv[i];
160
+ if (arg === "--json")
161
+ out.json = true;
162
+ else if (arg === "--query" || arg.startsWith("--query=")) {
163
+ if (arg === "--query")
164
+ out.query = argv[++i];
165
+ else
166
+ out.query = arg.slice("--query=".length);
167
+ }
168
+ else if (arg === "--scope" || arg.startsWith("--scope=")) {
169
+ if (arg === "--scope")
170
+ out.scope = argv[++i];
171
+ else
172
+ out.scope = arg.slice("--scope=".length);
173
+ }
174
+ else if (arg === "--issue" || arg.startsWith("--issue=")) {
175
+ const raw = arg === "--issue" ? argv[++i] : arg.slice("--issue=".length);
176
+ if (raw === undefined || raw.trim().length === 0 || raw.startsWith("-")) {
177
+ return { ...out, error: "--issue requires a positive integer" };
178
+ }
179
+ if (!/^\d+$/.test(raw.trim())) {
180
+ return { ...out, error: `--issue must be a positive integer, got: ${raw}` };
181
+ }
182
+ const n = Number(raw.trim());
183
+ if (!Number.isSafeInteger(n) || n <= 0) {
184
+ return {
185
+ ...out,
186
+ error: `--issue must be a safe positive integer (<= ${Number.MAX_SAFE_INTEGER}), got: ${raw}`,
187
+ };
188
+ }
189
+ out.issue = n;
190
+ }
191
+ else if (arg === "--limit" || arg.startsWith("--limit=")) {
192
+ const raw = arg === "--limit" ? argv[++i] : arg.slice("--limit=".length);
193
+ if (raw === undefined || raw.trim().length === 0 || raw.startsWith("-")) {
194
+ return { ...out, error: "--limit requires a positive integer" };
195
+ }
196
+ if (!/^\d+$/.test(raw.trim())) {
197
+ return { ...out, error: `--limit must be a positive integer, got: ${raw}` };
198
+ }
199
+ const n = Number(raw.trim());
200
+ if (!Number.isSafeInteger(n) || n <= 0) {
201
+ return {
202
+ ...out,
203
+ error: `--limit must be a safe positive integer (<= ${Number.MAX_SAFE_INTEGER}), got: ${raw}`,
204
+ };
205
+ }
206
+ out.limit = n;
207
+ }
208
+ else if (arg === "--project-root" || arg.startsWith("--project-root=")) {
209
+ if (arg === "--project-root")
210
+ out.projectRoot = argv[++i];
211
+ else
212
+ out.projectRoot = arg.slice("--project-root=".length);
213
+ }
214
+ else if (arg.startsWith("-")) {
215
+ return { ...out, error: `unrecognized argument: ${arg}` };
216
+ }
217
+ else if (out.query === undefined) {
218
+ out.query = arg;
219
+ }
220
+ }
221
+ return out;
222
+ }
223
+ /** CLI entry for decision:list. */
224
+ export function decisionListMain(argv) {
225
+ const args = parseDecisionListArgs(argv);
226
+ if (args.error !== undefined) {
227
+ process.stderr.write(`decision:list: ${args.error}\n`);
228
+ return 2;
229
+ }
230
+ const result = runDecisionList({
231
+ projectRoot: args.projectRoot,
232
+ query: args.query,
233
+ scope: args.scope,
234
+ issue: args.issue,
235
+ limit: args.limit,
236
+ json: args.json,
237
+ });
238
+ if (args.json) {
239
+ process.stdout.write(`${JSON.stringify({
240
+ exit_code: result.exitCode,
241
+ count: result.entries.length,
242
+ entries: result.entries,
243
+ }, null, 2)}\n`);
244
+ }
245
+ else {
246
+ process.stdout.write(result.message);
247
+ }
248
+ return result.exitCode;
249
+ }
250
+ //# sourceMappingURL=list.js.map
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Lightweight structured agent decision log schema (#1396).
3
+ *
4
+ * Intent-debt records for significant choices only. Not a full lifecycle xBRIEF
5
+ * and not a replacement for docs/decisions/ADR-*.md.
6
+ */
7
+ export declare const DECISION_SCHEMA_VERSION: "deft.decision.v1";
8
+ /** Directory under project root for standalone (cross-cutting) decision files. */
9
+ export declare const DECISIONS_DIR_REL = "xbrief/decisions";
10
+ /** Filename suffix for decision records. */
11
+ export declare const DECISION_FILE_SUFFIX = ".decision.json";
12
+ export type DecisionConfidence = "low" | "medium" | "high";
13
+ /** Governing rule or constraint that informed the decision. */
14
+ export interface DecisionGoverningRule {
15
+ /** Human-readable description of the rule/constraint. */
16
+ readonly description: string;
17
+ /** Path to the rule source (skill, docs, AGENTS.md section, issue). */
18
+ readonly path?: string | null;
19
+ /** RFC2119 tier when the rule is MUST/SHOULD/MAY style. */
20
+ readonly rfc2119?: "MUST" | "SHOULD" | "MAY" | "MUST NOT" | "SHOULD NOT" | null;
21
+ }
22
+ /** One alternative considered and not chosen. */
23
+ export interface DecisionAlternative {
24
+ readonly option: string;
25
+ readonly whyNot?: string | null;
26
+ }
27
+ /**
28
+ * Validated decision record (v1).
29
+ *
30
+ * Required fields per #1396 design lock: decision, governing rule/constraint,
31
+ * alternatives considered, why winner, confidence, active scope ref(s) if any,
32
+ * timestamp, revisit trigger.
33
+ */
34
+ export interface DecisionRecord {
35
+ readonly schemaVersion: typeof DECISION_SCHEMA_VERSION;
36
+ /** Stable slug used in the filename (kebab-case). */
37
+ readonly id: string;
38
+ /** One-line or short paragraph of what was decided. */
39
+ readonly decision: string;
40
+ readonly governingRule: DecisionGoverningRule;
41
+ readonly alternativesConsidered: readonly DecisionAlternative[];
42
+ /** Why the chosen option won over the alternatives. */
43
+ readonly whyWinner: string;
44
+ readonly confidence: DecisionConfidence;
45
+ /** Relative path(s) to related scope xBRIEF(s), if any. */
46
+ readonly activeScopeRefs: readonly string[];
47
+ /** ISO-8601 UTC timestamp (second precision preferred). */
48
+ readonly timestamp: string;
49
+ /** When/why a later agent should re-open this decision. */
50
+ readonly revisitTrigger: string;
51
+ /** Optional tags for list filtering. */
52
+ readonly tags?: readonly string[];
53
+ /** Optional related issue numbers (without #). */
54
+ readonly relatedIssues?: readonly number[];
55
+ /** Relative path of the written file (filled after write). */
56
+ readonly path?: string;
57
+ }
58
+ export interface DecisionValidationError {
59
+ readonly field: string;
60
+ readonly message: string;
61
+ }
62
+ export interface DecisionValidationResult {
63
+ readonly ok: boolean;
64
+ readonly errors: readonly DecisionValidationError[];
65
+ readonly record?: DecisionRecord;
66
+ }
67
+ /** Normalize timestamps to second-precision UTC with trailing Z. */
68
+ export declare function normalizeTimestamp(raw?: string | null): string;
69
+ /**
70
+ * Derive a kebab-case slug from free text (max 64 chars).
71
+ * Linear-time (no polynomial regex) for CodeQL safety on library input.
72
+ */
73
+ export declare function slugifyDecision(text: string): string;
74
+ /**
75
+ * Strip terminal / bidi control chars for safe plaintext list rendering.
76
+ * Drops C0 (except tab/LF/CR → space), DEL, C1 (U+0080–U+009F), and
77
+ * Unicode bidi overrides / isolates / embeddings used for deceptive reordering.
78
+ */
79
+ export declare function sanitizeForTerminal(text: string): string;
80
+ /** Date prefix YYYY-MM-DD from an ISO timestamp. */
81
+ export declare function datePrefixFromTimestamp(timestamp: string): string;
82
+ /** Build standalone filename: YYYY-MM-DD-<slug>.decision.json */
83
+ export declare function decisionFilename(id: string, timestamp: string): string;
84
+ /** Validate and normalize an unknown JSON value into a DecisionRecord. */
85
+ export declare function validateDecisionRecord(input: unknown): DecisionValidationResult;
86
+ /** Format validation errors for CLI stderr. */
87
+ export declare function formatDecisionValidationErrors(errors: readonly DecisionValidationError[]): string;
88
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Lightweight structured agent decision log schema (#1396).
3
+ *
4
+ * Intent-debt records for significant choices only. Not a full lifecycle xBRIEF
5
+ * and not a replacement for docs/decisions/ADR-*.md.
6
+ */
7
+ export const DECISION_SCHEMA_VERSION = "deft.decision.v1";
8
+ /** Directory under project root for standalone (cross-cutting) decision files. */
9
+ export const DECISIONS_DIR_REL = "xbrief/decisions";
10
+ /** Filename suffix for decision records. */
11
+ export const DECISION_FILE_SUFFIX = ".decision.json";
12
+ const CONFIDENCE_VALUES = new Set(["low", "medium", "high"]);
13
+ const RFC_VALUES = new Set([
14
+ "MUST",
15
+ "SHOULD",
16
+ "MAY",
17
+ "MUST NOT",
18
+ "SHOULD NOT",
19
+ ]);
20
+ function isPlainObject(value) {
21
+ return value !== null && typeof value === "object" && !Array.isArray(value);
22
+ }
23
+ function nonEmptyString(value) {
24
+ return typeof value === "string" && value.trim().length > 0;
25
+ }
26
+ /** Normalize timestamps to second-precision UTC with trailing Z. */
27
+ export function normalizeTimestamp(raw) {
28
+ if (raw !== undefined && raw !== null && raw.trim().length > 0) {
29
+ const parsed = new Date(raw.trim());
30
+ if (!Number.isNaN(parsed.getTime())) {
31
+ return parsed.toISOString().replace(/\.\d{3}Z$/, "Z");
32
+ }
33
+ }
34
+ return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
35
+ }
36
+ /**
37
+ * Derive a kebab-case slug from free text (max 64 chars).
38
+ * Linear-time (no polynomial regex) for CodeQL safety on library input.
39
+ */
40
+ export function slugifyDecision(text) {
41
+ const lower = text.trim().toLowerCase();
42
+ let out = "";
43
+ let pendingHyphen = false;
44
+ for (let i = 0; i < lower.length && out.length < 64; i += 1) {
45
+ const ch = lower[i];
46
+ const code = ch.charCodeAt(0);
47
+ const isAlnum = (code >= 48 && code <= 57) || // 0-9
48
+ (code >= 97 && code <= 122); // a-z
49
+ if (isAlnum) {
50
+ if (pendingHyphen && out.length > 0) {
51
+ out += "-";
52
+ if (out.length >= 64)
53
+ break;
54
+ }
55
+ out += ch;
56
+ pendingHyphen = false;
57
+ }
58
+ else {
59
+ pendingHyphen = true;
60
+ }
61
+ }
62
+ return out.length > 0 ? out : "decision";
63
+ }
64
+ /**
65
+ * Strip terminal / bidi control chars for safe plaintext list rendering.
66
+ * Drops C0 (except tab/LF/CR → space), DEL, C1 (U+0080–U+009F), and
67
+ * Unicode bidi overrides / isolates / embeddings used for deceptive reordering.
68
+ */
69
+ export function sanitizeForTerminal(text) {
70
+ let out = "";
71
+ for (let i = 0; i < text.length; i += 1) {
72
+ const code = text.charCodeAt(i);
73
+ // Tab / LF / CR → space (never pass raw control layout to the terminal).
74
+ if (code === 9 || code === 10 || code === 13) {
75
+ out += " ";
76
+ continue;
77
+ }
78
+ // C0 + DEL
79
+ if (code < 32 || code === 127) {
80
+ continue;
81
+ }
82
+ // C1 controls (including when encoded as U+0080–U+009F)
83
+ if (code >= 0x80 && code <= 0x9f) {
84
+ continue;
85
+ }
86
+ // Bidi control characters (deceptive reordering)
87
+ // LRE/RLE/PDF/LRO/RLO, LRI/RLI/FSI/PDI, and related marks.
88
+ if (code === 0x061c || // ALM
89
+ code === 0x200e || // LRM
90
+ code === 0x200f || // RLM
91
+ (code >= 0x202a && code <= 0x202e) || // LRE..RLO
92
+ (code >= 0x2066 && code <= 0x2069) // LRI..PDI
93
+ ) {
94
+ continue;
95
+ }
96
+ out += text[i];
97
+ }
98
+ return out;
99
+ }
100
+ /** Date prefix YYYY-MM-DD from an ISO timestamp. */
101
+ export function datePrefixFromTimestamp(timestamp) {
102
+ const m = timestamp.match(/^(\d{4}-\d{2}-\d{2})/);
103
+ return m?.[1] ?? new Date().toISOString().slice(0, 10);
104
+ }
105
+ /** Build standalone filename: YYYY-MM-DD-<slug>.decision.json */
106
+ export function decisionFilename(id, timestamp) {
107
+ const date = datePrefixFromTimestamp(timestamp);
108
+ const slug = slugifyDecision(id);
109
+ return `${date}-${slug}${DECISION_FILE_SUFFIX}`;
110
+ }
111
+ /** Validate and normalize an unknown JSON value into a DecisionRecord. */
112
+ export function validateDecisionRecord(input) {
113
+ const errors = [];
114
+ if (!isPlainObject(input)) {
115
+ return { ok: false, errors: [{ field: "", message: "decision record must be a JSON object" }] };
116
+ }
117
+ const schemaVersion = input.schemaVersion;
118
+ if (schemaVersion !== undefined && schemaVersion !== DECISION_SCHEMA_VERSION) {
119
+ errors.push({
120
+ field: "schemaVersion",
121
+ message: `expected ${DECISION_SCHEMA_VERSION}, got ${String(schemaVersion)}`,
122
+ });
123
+ }
124
+ const decision = input.decision;
125
+ if (!nonEmptyString(decision)) {
126
+ errors.push({ field: "decision", message: "required non-empty string" });
127
+ }
128
+ let governingRule = null;
129
+ if (!isPlainObject(input.governingRule) && !nonEmptyString(input.governingRule)) {
130
+ errors.push({
131
+ field: "governingRule",
132
+ message: "required object { description, path?, rfc2119? } or non-empty string",
133
+ });
134
+ }
135
+ else if (nonEmptyString(input.governingRule)) {
136
+ governingRule = { description: input.governingRule.trim() };
137
+ }
138
+ else if (isPlainObject(input.governingRule)) {
139
+ const gr = input.governingRule;
140
+ if (!nonEmptyString(gr.description)) {
141
+ errors.push({ field: "governingRule.description", message: "required non-empty string" });
142
+ }
143
+ else {
144
+ let rfc2119 = null;
145
+ if (gr.rfc2119 !== undefined && gr.rfc2119 !== null) {
146
+ if (typeof gr.rfc2119 === "string" && RFC_VALUES.has(gr.rfc2119)) {
147
+ rfc2119 = gr.rfc2119;
148
+ }
149
+ else {
150
+ errors.push({
151
+ field: "governingRule.rfc2119",
152
+ message: `must be one of ${[...RFC_VALUES].join(", ")}`,
153
+ });
154
+ }
155
+ }
156
+ governingRule = {
157
+ description: gr.description.trim(),
158
+ path: typeof gr.path === "string" && gr.path.trim().length > 0 ? gr.path.trim() : null,
159
+ rfc2119,
160
+ };
161
+ }
162
+ }
163
+ const alternativesRaw = input.alternativesConsidered ?? input.alternatives;
164
+ const alternatives = [];
165
+ if (!Array.isArray(alternativesRaw)) {
166
+ errors.push({
167
+ field: "alternativesConsidered",
168
+ message: "required array of { option, whyNot? } or strings",
169
+ });
170
+ }
171
+ else if (alternativesRaw.length === 0) {
172
+ errors.push({
173
+ field: "alternativesConsidered",
174
+ message: 'must include at least one alternative (use [{option:"none"}] if truly sole path)',
175
+ });
176
+ }
177
+ else {
178
+ for (let i = 0; i < alternativesRaw.length; i += 1) {
179
+ const alt = alternativesRaw[i];
180
+ if (nonEmptyString(alt)) {
181
+ alternatives.push({ option: alt.trim() });
182
+ }
183
+ else if (isPlainObject(alt) && nonEmptyString(alt.option)) {
184
+ alternatives.push({
185
+ option: alt.option.trim(),
186
+ whyNot: typeof alt.whyNot === "string" && alt.whyNot.trim().length > 0
187
+ ? alt.whyNot.trim()
188
+ : null,
189
+ });
190
+ }
191
+ else {
192
+ errors.push({
193
+ field: `alternativesConsidered[${i}]`,
194
+ message: "must be a non-empty string or { option, whyNot? }",
195
+ });
196
+ }
197
+ }
198
+ }
199
+ const whyWinner = input.whyWinner ?? input.why_winner;
200
+ if (!nonEmptyString(whyWinner)) {
201
+ errors.push({ field: "whyWinner", message: "required non-empty string" });
202
+ }
203
+ const confidenceRaw = input.confidence;
204
+ let confidence = null;
205
+ if (!nonEmptyString(confidenceRaw) ||
206
+ !CONFIDENCE_VALUES.has(confidenceRaw.trim().toLowerCase())) {
207
+ errors.push({ field: "confidence", message: "required: low | medium | high" });
208
+ }
209
+ else {
210
+ confidence = confidenceRaw.trim().toLowerCase();
211
+ }
212
+ const scopeRaw = input.activeScopeRefs ?? input.active_scope_refs ?? input.scopeRefs;
213
+ const activeScopeRefs = [];
214
+ if (scopeRaw === undefined || scopeRaw === null) {
215
+ // optional
216
+ }
217
+ else if (Array.isArray(scopeRaw)) {
218
+ for (let i = 0; i < scopeRaw.length; i += 1) {
219
+ const s = scopeRaw[i];
220
+ if (nonEmptyString(s)) {
221
+ activeScopeRefs.push(s.trim().replace(/\\/g, "/"));
222
+ }
223
+ else {
224
+ errors.push({ field: `activeScopeRefs[${i}]`, message: "must be a non-empty path string" });
225
+ }
226
+ }
227
+ }
228
+ else if (nonEmptyString(scopeRaw)) {
229
+ activeScopeRefs.push(scopeRaw.trim().replace(/\\/g, "/"));
230
+ }
231
+ else {
232
+ errors.push({
233
+ field: "activeScopeRefs",
234
+ message: "must be a string path, array of paths, or omitted",
235
+ });
236
+ }
237
+ const revisitTrigger = input.revisitTrigger ?? input.revisit_trigger;
238
+ if (!nonEmptyString(revisitTrigger)) {
239
+ errors.push({ field: "revisitTrigger", message: "required non-empty string" });
240
+ }
241
+ let timestamp = normalizeTimestamp(typeof input.timestamp === "string" ? input.timestamp : null);
242
+ if (typeof input.timestamp === "string" && input.timestamp.trim().length > 0) {
243
+ const parsed = new Date(input.timestamp.trim());
244
+ if (Number.isNaN(parsed.getTime())) {
245
+ errors.push({ field: "timestamp", message: "invalid ISO-8601 timestamp" });
246
+ }
247
+ else {
248
+ timestamp = normalizeTimestamp(input.timestamp);
249
+ }
250
+ }
251
+ const id = typeof input.id === "string" && input.id.trim().length > 0
252
+ ? slugifyDecision(input.id)
253
+ : slugifyDecision(typeof decision === "string" ? decision : "decision");
254
+ const tags = [];
255
+ if (Array.isArray(input.tags)) {
256
+ for (const t of input.tags) {
257
+ if (nonEmptyString(t))
258
+ tags.push(t.trim());
259
+ }
260
+ }
261
+ const relatedIssues = [];
262
+ if (Array.isArray(input.relatedIssues)) {
263
+ for (const n of input.relatedIssues) {
264
+ if (typeof n === "number" && Number.isInteger(n) && n > 0)
265
+ relatedIssues.push(n);
266
+ else if (typeof n === "string" && /^\d+$/.test(n.trim()))
267
+ relatedIssues.push(Number(n.trim()));
268
+ }
269
+ }
270
+ if (errors.length > 0) {
271
+ return { ok: false, errors };
272
+ }
273
+ const record = {
274
+ schemaVersion: DECISION_SCHEMA_VERSION,
275
+ id,
276
+ decision: decision.trim(),
277
+ governingRule: governingRule,
278
+ alternativesConsidered: alternatives,
279
+ whyWinner: whyWinner.trim(),
280
+ confidence: confidence,
281
+ activeScopeRefs,
282
+ timestamp,
283
+ revisitTrigger: revisitTrigger.trim(),
284
+ ...(tags.length > 0 ? { tags } : {}),
285
+ ...(relatedIssues.length > 0 ? { relatedIssues } : {}),
286
+ };
287
+ return { ok: true, errors: [], record };
288
+ }
289
+ /** Format validation errors for CLI stderr. */
290
+ export function formatDecisionValidationErrors(errors) {
291
+ return errors.map((e) => (e.field ? `${e.field}: ${e.message}` : e.message)).join("\n");
292
+ }
293
+ //# sourceMappingURL=schema.js.map