@polydeukes/core 0.4.0 → 0.5.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/config.d.ts CHANGED
@@ -1,18 +1,15 @@
1
1
  /**
2
- * Config schema v2 + `defineConfig()` validator — config as data (CONFIG-04).
2
+ * Config schema and the `defineConfig()` validator — config as data.
3
3
  *
4
- * This is the single settings surface the three areas share (covenant's `protectedPaths`,
5
- * ledger's `testCmd`, memory's ticket pattern all reference this shape). Since schema v2 the
6
- * input is pure JSON-representable data: `testCmd` is a `{scope}` template string, and
7
- * `defineConfig` is the runtime validator for parsed unknown data (the CONFIG-03 loader feeds
8
- * it values the compiler never saw). It stays a pure function — zero file I/O, zero runtime
9
- * dependencies (hand-rolled validation; the published JSON Schema is a sibling artifact the
10
- * source never reads).
4
+ * The single settings surface the areas share. The input is pure JSON-representable data,
5
+ * and `defineConfig` is the runtime validator for parsed unknown values the compiler never
6
+ * saw. It stays a pure function no file I/O and no runtime dependencies: validation is
7
+ * hand-rolled, and the published JSON Schema is a sibling artifact this source never reads.
11
8
  */
12
- /** Conventional default telemetry log path (PRD §4.3) — local-only observation data. */
9
+ /** Conventional default telemetry log path — local-only observation data. */
13
10
  export declare const DEFAULT_TELEMETRY_LOG_PATH = ".polydeukes/roi.log";
14
11
  /**
15
- * `LanguageProfile` — the unit of the language axis (PRD §4.1).
12
+ * `LanguageProfile` — the unit of the language axis.
16
13
  *
17
14
  * `testCmd` is a shell command template: every literal `{scope}` token is substituted at
18
15
  * resolve time, and the core only carries the resulting string — it never interprets it.
@@ -27,16 +24,22 @@ export type LanguageProfile = {
27
24
  testCmd: string;
28
25
  };
29
26
  /**
30
- * `DisciplineForbid` — the delta-family predicate value (COVENANT-10 §4.1).
27
+ * `DisciplineForbid` — the delta-family predicate value.
31
28
  *
32
- * The string shorthand is equivalent to `{ added }`; `removed`/`present` directions are
33
- * deferred (COVENANT-12) and rejected by validation.
29
+ * The string shorthand is equivalent to `{ added }`. It is the only direction that exists;
30
+ * anything else is rejected by validation.
34
31
  */
35
32
  export type DisciplineForbid = string | {
36
33
  added: string;
37
34
  };
38
35
  /**
39
- * `DisciplineEntry` — one user-declared discipline (COVENANT-10 §4.1). Pure JSON data.
36
+ * `EnforceLevel` — an entry's own rung on the promotion ladder. `advise` records a break
37
+ * without stopping it; `block` pins the entry at block whatever default the ladder later
38
+ * adopts. Absence means advise; `block` is the promotion rung.
39
+ */
40
+ export type EnforceLevel = 'block' | 'advise';
41
+ /**
42
+ * `DisciplineEntry` — one user-declared discipline. Pure JSON data.
40
43
  *
41
44
  * Exactly one predicate key (`forbid` | `immutable` | `forbidCommand` |
42
45
  * `requirePrecedent`) per entry; `in`/`except` scope the delta and context families.
@@ -46,8 +49,10 @@ export type DisciplineForbid = string | {
46
49
  export type DisciplineEntry = {
47
50
  /** unique handle — telemetry label and verdict reason prefix */
48
51
  id: string;
49
- /** prose rationale, documentation only — never judged */
52
+ /** prose rationale — never judged, and carried into the break message */
50
53
  why?: string;
54
+ /** the author's level; composes with the observer's surface level, lenient side winning */
55
+ enforce?: EnforceLevel;
51
56
  /** delta/context-family scope: glob(s) the file path must match (absent = every file change) */
52
57
  in?: string | string[];
53
58
  /** delta/context-family scope: glob(s) excluded after `in` */
@@ -63,44 +68,61 @@ export type DisciplineEntry = {
63
68
  /**
64
69
  * context family — the session evidence one edit requires beforehand. Exactly one
65
70
  * evidence key. The core owns and fully validates `command`; every other key is
66
- * adapter vocabulary whose value passes through verbatim (CONFIG-07 layering).
71
+ * adapter vocabulary whose value passes through verbatim.
67
72
  */
68
73
  requirePrecedent?: Record<string, unknown>;
69
74
  };
70
75
  /**
71
- * `PolydeukesConfig` — the input shape a user writes (PRD §4.1). JSON-serializable data.
76
+ * `DisciplineDraft` — an unpromoted discipline: the promotion ladder's first rung,
77
+ * registered as prose ahead of any predicate.
78
+ *
79
+ * A draft is declared, never inferred — only the literal `draft: true` makes one, and an
80
+ * entry with neither a predicate nor the marker stays a validation error. It carries no
81
+ * predicate, scope, or trigger key, produces no registration, no judgment, and no
82
+ * telemetry row; `pdks explain` renders it as unpromoted.
83
+ */
84
+ export type DisciplineDraft = {
85
+ /** unique handle in the same label space as judged entries and meta-covenant labels */
86
+ id: string;
87
+ /** the draft's whole body — required prose, unlike the judged families' optional why */
88
+ why: string;
89
+ /** the explicit marker; only the literal true exists (false is rejected as dead data) */
90
+ draft: true;
91
+ };
92
+ /**
93
+ * `PolydeukesConfig` — the input shape a user writes. JSON-serializable data.
72
94
  *
73
95
  * Language keys (`typescript`, `python`, …) are user *values*, not the core's vocabulary —
74
96
  * no language or tool literal appears in the core source.
75
97
  */
76
98
  export type PolydeukesConfig = {
77
- /** IDE schema reference (CONFIG-03) — accepted and ignored, never part of the resolution */
99
+ /** IDE schema reference — accepted and ignored, never part of the resolution */
78
100
  $schema?: string;
79
101
  /** language axis, first-class. keys are user values ('typescript', 'python', …) */
80
102
  languages: Record<string, LanguageProfile>;
81
- /** raw protected path patterns — normalization is CONFIG-02's job */
103
+ /** raw protected path patterns — normalized downstream, never here */
82
104
  protectedPaths?: string[];
83
105
  /**
84
- * adapter namespaces (CONFIG-07) — keys are ecosystem values (never validated), each
85
- * value is that adapter's own settings object, passed through verbatim (the vocabulary
86
- * belongs to the adapter, whose own validator judges the contents)
106
+ * adapter namespaces — keys are ecosystem values (never validated), each value is that
107
+ * adapter's own settings object, passed through verbatim (the vocabulary belongs to the
108
+ * adapter, whose own validator judges the contents)
87
109
  */
88
110
  adapters?: Record<string, Record<string, unknown>>;
89
111
  telemetry?: {
90
- /** conventional default applies when omitted (§4.3) */
112
+ /** conventional default applies when omitted */
91
113
  logPath?: string;
92
114
  };
93
115
  /** user-declared disciplines — validated here, compiled by the covenant package */
94
- disciplines?: DisciplineEntry[];
116
+ disciplines?: (DisciplineEntry | DisciplineDraft)[];
95
117
  /**
96
- * TTL witness values for the covenant valve seam (CONFIG-05) — consumed at
97
- * assembly time, validated here
118
+ * TTL witness values for the covenant valve seam — consumed at assembly time,
119
+ * validated here
98
120
  */
99
121
  witness?: {
100
122
  /**
101
123
  * the agreed phrase a human types alone on a message's first line — quoting it
102
- * mid-sentence is a mention, not an invocation (COVENANT-15). Non-empty after
103
- * trimming; the value itself is free (provenance, not secrecy, is the defence)
124
+ * mid-sentence is a mention, not an invocation. Non-empty after trimming; the value
125
+ * itself is free, since provenance rather than secrecy is the defence
104
126
  */
105
127
  token: string;
106
128
  /** validity window in minutes from the user message's timestamp — finite and > 0 */
@@ -110,11 +132,11 @@ export type PolydeukesConfig = {
110
132
  /**
111
133
  * `ResolvedLanguageProfile` — a {@link LanguageProfile} with its template compiled.
112
134
  *
113
- * Consumers keep the callable shape (`testCmd(scope)`), identical to schema v1 (LEDGER-05).
135
+ * Consumers keep the callable shape (`testCmd(scope)`).
114
136
  */
115
137
  export type ResolvedLanguageProfile = {
116
138
  productionGlob: string | string[];
117
- /** compiled from the template — consumers keep the callable shape (LEDGER-05) */
139
+ /** compiled from the template — consumers keep the callable shape */
118
140
  testCmd: (scope: string) => string;
119
141
  };
120
142
  /**
@@ -130,8 +152,14 @@ export type ResolvedConfig = {
130
152
  telemetry: {
131
153
  logPath: string;
132
154
  };
133
- /** validated discipline data, passed through verbatim (absent stays absent) */
155
+ /**
156
+ * validated judged entries only — drafts are split out at resolution time so the
157
+ * covenant compiler has no path that receives one. Present whenever the input
158
+ * declared a `disciplines` array, holding exactly its judged entries in order.
159
+ */
134
160
  disciplines?: DisciplineEntry[];
161
+ /** validated drafts in declaration order (absent when the input carries none) */
162
+ drafts?: DisciplineDraft[];
135
163
  /** validated witness data, passed through verbatim (absent stays absent) */
136
164
  witness?: {
137
165
  token: string;
@@ -139,7 +167,7 @@ export type ResolvedConfig = {
139
167
  };
140
168
  };
141
169
  /**
142
- * `ConfigValidationError` — raised when a config fails structural validation (PRD §4.3).
170
+ * `ConfigValidationError` — raised when a config fails structural validation.
143
171
  *
144
172
  * The message names the offending field path so the developer sees exactly what is wrong.
145
173
  * This throw is a developer-time error (config authoring), a different axis from the
@@ -150,8 +178,7 @@ export declare class ConfigValidationError extends Error {
150
178
  }
151
179
  /**
152
180
  * Validate parsed unknown data as a {@link PolydeukesConfig} and return a
153
- * {@link ResolvedConfig} with defaults filled and templates compiled (PRD §4.3).
154
- * Pure — no file I/O.
181
+ * {@link ResolvedConfig} with defaults filled and templates compiled. Pure — no file I/O.
155
182
  *
156
183
  * Throws {@link ConfigValidationError} (naming the offending field path) when the top level
157
184
  * is not a plain object, any object level carries an unknown key, `languages` is
package/dist/config.js CHANGED
@@ -1,19 +1,16 @@
1
1
  /**
2
- * Config schema v2 + `defineConfig()` validator — config as data (CONFIG-04).
2
+ * Config schema and the `defineConfig()` validator — config as data.
3
3
  *
4
- * This is the single settings surface the three areas share (covenant's `protectedPaths`,
5
- * ledger's `testCmd`, memory's ticket pattern all reference this shape). Since schema v2 the
6
- * input is pure JSON-representable data: `testCmd` is a `{scope}` template string, and
7
- * `defineConfig` is the runtime validator for parsed unknown data (the CONFIG-03 loader feeds
8
- * it values the compiler never saw). It stays a pure function — zero file I/O, zero runtime
9
- * dependencies (hand-rolled validation; the published JSON Schema is a sibling artifact the
10
- * source never reads).
4
+ * The single settings surface the areas share. The input is pure JSON-representable data,
5
+ * and `defineConfig` is the runtime validator for parsed unknown values the compiler never
6
+ * saw. It stays a pure function no file I/O and no runtime dependencies: validation is
7
+ * hand-rolled, and the published JSON Schema is a sibling artifact this source never reads.
11
8
  */
12
9
  import { isPlainObject } from './is-plain-object.js';
13
- /** Conventional default telemetry log path (PRD §4.3) — local-only observation data. */
10
+ /** Conventional default telemetry log path — local-only observation data. */
14
11
  export const DEFAULT_TELEMETRY_LOG_PATH = '.polydeukes/roi.log';
15
12
  /**
16
- * `ConfigValidationError` — raised when a config fails structural validation (PRD §4.3).
13
+ * `ConfigValidationError` — raised when a config fails structural validation.
17
14
  *
18
15
  * The message names the offending field path so the developer sees exactly what is wrong.
19
16
  * This throw is a developer-time error (config authoring), a different axis from the
@@ -25,7 +22,8 @@ export class ConfigValidationError extends Error {
25
22
  this.name = 'ConfigValidationError';
26
23
  }
27
24
  }
28
- /** The exact key vocabulary of each object level anything else is a typo, rejected loudly. */
25
+ /** Labels the assembly reserves for the judging chain's own registrations. */
26
+ const META_COVENANT_LABELS = ['self-mod', 'shell-mod', 'transcript-mod'];
29
27
  const TOP_LEVEL_KEYS = new Set([
30
28
  '$schema',
31
29
  'languages',
@@ -41,6 +39,7 @@ const WITNESS_KEYS = new Set(['token', 'ttlMinutes']);
41
39
  const DISCIPLINE_KEYS = new Set([
42
40
  'id',
43
41
  'why',
42
+ 'enforce',
44
43
  'in',
45
44
  'except',
46
45
  'forbid',
@@ -49,8 +48,10 @@ const DISCIPLINE_KEYS = new Set([
49
48
  'when',
50
49
  'requirePrecedent',
51
50
  ]);
51
+ const DRAFT_KEYS = new Set(['id', 'why', 'draft']);
52
+ const ENFORCE_LEVELS = new Set(['block', 'advise']);
52
53
  const PREDICATE_KEYS = ['forbid', 'immutable', 'forbidCommand', 'requirePrecedent'];
53
- /** Predicate families that `in`/`except` may scope — delta and context (COVENANT-13 §4.1). */
54
+ /** Predicate families that `in`/`except` may scope — delta and context. */
54
55
  const SCOPED_PREDICATE_KEYS = new Set(['forbid', 'requirePrecedent']);
55
56
  /** Throw on the first key outside the allowed vocabulary, naming the key and its location. */
56
57
  function rejectUnknownKeys(record, allowed, location) {
@@ -75,7 +76,7 @@ function isValidGlob(glob) {
75
76
  return false;
76
77
  }
77
78
  /**
78
- * Compile a `{scope}` template into the callable consumers use (PRD §4.2).
79
+ * Compile a `{scope}` template into the callable consumers use.
79
80
  *
80
81
  * Exactly the literal token `{scope}` is substituted, at every occurrence (`replaceAll`
81
82
  * semantics). Other braces (`${VAR}`, `{a,b}`, `awk '{print}'`) are the shell's own
@@ -96,14 +97,13 @@ function rejectUncompilableRegex(pattern, location) {
96
97
  }
97
98
  }
98
99
  /**
99
- * Validate a context-family `requirePrecedent` value (COVENANT-13 §4.1).
100
+ * Validate a context-family `requirePrecedent` value.
100
101
  *
101
102
  * Evidence vocabulary is layered: the container (a flat object holding exactly one
102
103
  * evidence key) is the core's, and so is the `command` key — a shell command is the
103
104
  * agent-crossing surface, fully validated here. Every other key belongs to an adapter,
104
105
  * whose own validator judges the value; the core passes it through verbatim and never
105
- * inspects it (CONFIG-07 layering). An unrecognized evidence key fails closed at
106
- * assembly time, not here.
106
+ * inspects it. An unrecognized evidence key fails closed at assembly time, not here.
107
107
  */
108
108
  function validateRequirePrecedent(evidence, location) {
109
109
  if (!isPlainObject(evidence)) {
@@ -121,14 +121,128 @@ function validateRequirePrecedent(evidence, location) {
121
121
  rejectUncompilableRegex(command, `${location} requirePrecedent.command`);
122
122
  }
123
123
  }
124
+ /** Validate a draft entry and return it as data. */
125
+ function validateDraft(entry, id, location) {
126
+ for (const key of Object.keys(entry)) {
127
+ if (!DRAFT_KEYS.has(key)) {
128
+ // Named as the draft rule, not as an unknown key: `forbid` et al. are legal
129
+ // discipline keys, just not on a draft.
130
+ throw new ConfigValidationError(`${location} allows only id, why, draft on a draft entry (found '${key}')`);
131
+ }
132
+ }
133
+ if (entry.draft !== true) {
134
+ throw new ConfigValidationError(`${location} draft must be the literal true`);
135
+ }
136
+ if (typeof entry.why !== 'string' || entry.why.length === 0) {
137
+ throw new ConfigValidationError(`${location} why must be a non-empty string on a draft entry — the prose is its whole body`);
138
+ }
139
+ return { id, why: entry.why, draft: true };
140
+ }
141
+ /** Validate the family-independent rules of a judged entry and return its predicate key. */
142
+ function validateJudgedHead(entry, location) {
143
+ rejectUnknownKeys(entry, DISCIPLINE_KEYS, location);
144
+ if (entry.why !== undefined && typeof entry.why !== 'string') {
145
+ throw new ConfigValidationError(`${location} why must be a string`);
146
+ }
147
+ if (entry.enforce !== undefined &&
148
+ (typeof entry.enforce !== 'string' || !ENFORCE_LEVELS.has(entry.enforce))) {
149
+ throw new ConfigValidationError(`${location} enforce must be 'block' or 'advise'`);
150
+ }
151
+ const predicates = PREDICATE_KEYS.filter((key) => entry[key] !== undefined);
152
+ if (predicates.length !== 1) {
153
+ throw new ConfigValidationError(`${location} must have exactly one predicate key ` +
154
+ `(forbid | immutable | forbidCommand | requirePrecedent)`);
155
+ }
156
+ const predicate = predicates[0];
157
+ if (!SCOPED_PREDICATE_KEYS.has(predicate) &&
158
+ (entry.in !== undefined || entry.except !== undefined)) {
159
+ throw new ConfigValidationError(`${location} allows in/except only on a forbid or requirePrecedent entry`);
160
+ }
161
+ // `when` is the context family's trigger; on any other family it would be dead data
162
+ // implying a trigger that is never applied.
163
+ if (entry.when !== undefined && predicate !== 'requirePrecedent') {
164
+ throw new ConfigValidationError(`${location} allows when only on a requirePrecedent entry`);
165
+ }
166
+ if (entry.in !== undefined && !isValidGlob(entry.in)) {
167
+ throw new ConfigValidationError(`${location} in must be a non-empty glob or glob array`);
168
+ }
169
+ if (entry.except !== undefined && !isValidGlob(entry.except)) {
170
+ throw new ConfigValidationError(`${location} except must be a non-empty glob or glob array`);
171
+ }
172
+ return predicate;
173
+ }
174
+ function validateForbid(entry, location) {
175
+ const forbid = entry.forbid;
176
+ if (typeof forbid === 'string') {
177
+ // An empty pattern matches at every position, so the entry would break every
178
+ // in-scope change — rejected like every sibling pattern field.
179
+ if (forbid.length === 0) {
180
+ throw new ConfigValidationError(`${location} forbid must be a non-empty string pattern`);
181
+ }
182
+ rejectUncompilableRegex(forbid, `${location} forbid`);
183
+ }
184
+ else if (isPlainObject(forbid)) {
185
+ const keys = Object.keys(forbid);
186
+ if (keys.length !== 1 || keys[0] !== 'added' || typeof forbid.added !== 'string') {
187
+ throw new ConfigValidationError(`${location} forbid object must have exactly one key 'added' with a string pattern`);
188
+ }
189
+ if (forbid.added.length === 0) {
190
+ throw new ConfigValidationError(`${location} forbid.added must be a non-empty string pattern`);
191
+ }
192
+ rejectUncompilableRegex(forbid.added, `${location} forbid.added`);
193
+ }
194
+ else {
195
+ throw new ConfigValidationError(`${location} forbid must be a string pattern or an { added } object`);
196
+ }
197
+ }
198
+ function validateImmutable(entry, location) {
199
+ if (!isValidGlob(entry.immutable)) {
200
+ throw new ConfigValidationError(`${location} immutable must be a non-empty glob or glob array`);
201
+ }
202
+ }
203
+ function validateForbidCommand(entry, location) {
204
+ if (typeof entry.forbidCommand !== 'string') {
205
+ throw new ConfigValidationError(`${location} forbidCommand must be a string pattern`);
206
+ }
207
+ if (entry.forbidCommand.length === 0) {
208
+ // An empty pattern matches every command line — one typo would block every
209
+ // shell call the entry sees.
210
+ throw new ConfigValidationError(`${location} forbidCommand must be a non-empty string pattern`);
211
+ }
212
+ rejectUncompilableRegex(entry.forbidCommand, `${location} forbidCommand`);
213
+ }
214
+ function validateContextEntry(entry, location) {
215
+ if (entry.when !== undefined) {
216
+ if (typeof entry.when !== 'string') {
217
+ throw new ConfigValidationError(`${location} when must be a string pattern`);
218
+ }
219
+ if (entry.when.length === 0) {
220
+ // An empty pattern matches at every position, so the trigger would fire on any
221
+ // file that merely grows — reject it like every sibling pattern field.
222
+ throw new ConfigValidationError(`${location} when must be a non-empty string pattern`);
223
+ }
224
+ rejectUncompilableRegex(entry.when, `${location} when`);
225
+ }
226
+ validateRequirePrecedent(entry.requirePrecedent, location);
227
+ }
228
+ /** One validator per family, keyed by the predicate that selects the family. */
229
+ const PREDICATE_VALIDATORS = {
230
+ forbid: validateForbid,
231
+ immutable: validateImmutable,
232
+ forbidCommand: validateForbidCommand,
233
+ requirePrecedent: validateContextEntry,
234
+ };
124
235
  /**
125
- * Validate the `disciplines` array (COVENANT-10 §4.1). Throws {@link ConfigValidationError}
126
- * naming the offending entry/key; the validated data passes through verbatim.
236
+ * Validate the `disciplines` array and split judged entries from drafts. Throws
237
+ * {@link ConfigValidationError} naming the offending entry/key; the validated data passes
238
+ * through verbatim, in declaration order.
127
239
  */
128
240
  function validateDisciplines(disciplines) {
129
241
  if (!Array.isArray(disciplines)) {
130
242
  throw new ConfigValidationError('disciplines must be an array');
131
243
  }
244
+ const judged = [];
245
+ const drafts = [];
132
246
  const seenIds = new Set();
133
247
  disciplines.forEach((entry, index) => {
134
248
  if (!isPlainObject(entry)) {
@@ -141,93 +255,26 @@ function validateDisciplines(disciplines) {
141
255
  if (seenIds.has(entry.id)) {
142
256
  throw new ConfigValidationError(`${location} duplicates the id of an earlier entry`);
143
257
  }
144
- seenIds.add(entry.id);
145
- rejectUnknownKeys(entry, DISCIPLINE_KEYS, location);
146
- if (entry.why !== undefined && typeof entry.why !== 'string') {
147
- throw new ConfigValidationError(`${location} why must be a string`);
148
- }
149
- const predicates = PREDICATE_KEYS.filter((key) => entry[key] !== undefined);
150
- if (predicates.length !== 1) {
151
- throw new ConfigValidationError(`${location} must have exactly one predicate key ` +
152
- `(forbid | immutable | forbidCommand | requirePrecedent)`);
258
+ // The three meta-covenant registrations share the telemetry label space with
259
+ // discipline ids; a colliding id would make gain aggregation and any label-keyed
260
+ // reader (pdks explain) ambiguous.
261
+ if (META_COVENANT_LABELS.includes(entry.id)) {
262
+ throw new ConfigValidationError(`${location} id collides with a meta-covenant label`);
153
263
  }
154
- const predicate = predicates[0];
155
- if (!SCOPED_PREDICATE_KEYS.has(predicate) &&
156
- (entry.in !== undefined || entry.except !== undefined)) {
157
- throw new ConfigValidationError(`${location} allows in/except only on a forbid or requirePrecedent entry`);
158
- }
159
- // `when` is the context family's trigger; on any other family it would be dead data
160
- // implying a trigger that is never applied.
161
- if (entry.when !== undefined && predicate !== 'requirePrecedent') {
162
- throw new ConfigValidationError(`${location} allows when only on a requirePrecedent entry`);
163
- }
164
- if (entry.in !== undefined && !isValidGlob(entry.in)) {
165
- throw new ConfigValidationError(`${location} in must be a non-empty glob or glob array`);
166
- }
167
- if (entry.except !== undefined && !isValidGlob(entry.except)) {
168
- throw new ConfigValidationError(`${location} except must be a non-empty glob or glob array`);
169
- }
170
- if (predicate === 'forbid') {
171
- const forbid = entry.forbid;
172
- if (typeof forbid === 'string') {
173
- // An empty pattern matches at every position, so the entry would break every
174
- // in-scope change — rejected like every sibling pattern field.
175
- if (forbid.length === 0) {
176
- throw new ConfigValidationError(`${location} forbid must be a non-empty string pattern`);
177
- }
178
- rejectUncompilableRegex(forbid, `${location} forbid`);
179
- }
180
- else if (isPlainObject(forbid)) {
181
- // Only the { added } direction exists before COVENANT-12.
182
- const keys = Object.keys(forbid);
183
- if (keys.length !== 1 || keys[0] !== 'added' || typeof forbid.added !== 'string') {
184
- throw new ConfigValidationError(`${location} forbid object must have exactly one key 'added' with a string pattern`);
185
- }
186
- if (forbid.added.length === 0) {
187
- throw new ConfigValidationError(`${location} forbid.added must be a non-empty string pattern`);
188
- }
189
- rejectUncompilableRegex(forbid.added, `${location} forbid.added`);
190
- }
191
- else {
192
- throw new ConfigValidationError(`${location} forbid must be a string pattern or an { added } object`);
193
- }
194
- }
195
- else if (predicate === 'immutable') {
196
- if (!isValidGlob(entry.immutable)) {
197
- throw new ConfigValidationError(`${location} immutable must be a non-empty glob or glob array`);
198
- }
199
- }
200
- else if (predicate === 'forbidCommand') {
201
- if (typeof entry.forbidCommand !== 'string') {
202
- throw new ConfigValidationError(`${location} forbidCommand must be a string pattern`);
203
- }
204
- if (entry.forbidCommand.length === 0) {
205
- // An empty pattern matches every command line — one typo would block every
206
- // shell call the entry sees.
207
- throw new ConfigValidationError(`${location} forbidCommand must be a non-empty string pattern`);
208
- }
209
- rejectUncompilableRegex(entry.forbidCommand, `${location} forbidCommand`);
210
- }
211
- else {
212
- if (entry.when !== undefined) {
213
- if (typeof entry.when !== 'string') {
214
- throw new ConfigValidationError(`${location} when must be a string pattern`);
215
- }
216
- if (entry.when.length === 0) {
217
- // An empty pattern matches at every position, so the trigger would fire on any
218
- // file that merely grows — reject it like every sibling pattern field.
219
- throw new ConfigValidationError(`${location} when must be a non-empty string pattern`);
220
- }
221
- rejectUncompilableRegex(entry.when, `${location} when`);
222
- }
223
- validateRequirePrecedent(entry.requirePrecedent, location);
264
+ seenIds.add(entry.id);
265
+ // Selected by the marker's value, so an explicit `draft: undefined` is absence,
266
+ // like every other optional key in this validator.
267
+ if (entry.draft !== undefined) {
268
+ drafts.push(validateDraft(entry, entry.id, location));
269
+ return;
224
270
  }
271
+ const predicate = validateJudgedHead(entry, location);
272
+ PREDICATE_VALIDATORS[predicate](entry, location);
273
+ judged.push(entry);
225
274
  });
226
- return disciplines;
275
+ return { judged, drafts };
227
276
  }
228
- /**
229
- * Validate the `languages` map and compile each profile's `{scope}` template (PRD §4.1).
230
- */
277
+ /** Validate the `languages` map and compile each profile's `{scope}` template. */
231
278
  function validateLanguages(languages) {
232
279
  if (!isPlainObject(languages) || Object.keys(languages).length === 0) {
233
280
  throw new ConfigValidationError('languages must be a non-empty object');
@@ -268,8 +315,8 @@ function validateProtectedPaths(protectedPaths) {
268
315
  return protectedPaths;
269
316
  }
270
317
  /**
271
- * Validate the `adapters` map (CONFIG-07 layering) — each namespace is a plain object whose
272
- * contents belong to that adapter's own validator, so they pass through verbatim.
318
+ * Validate the `adapters` map — each namespace is a plain object whose contents belong to
319
+ * that adapter's own validator, so they pass through verbatim.
273
320
  */
274
321
  function validateAdapters(adapters) {
275
322
  // Array first: the removed directory-list form deserves a migration hint, not a
@@ -302,7 +349,7 @@ function validateTelemetry(telemetry) {
302
349
  }
303
350
  return telemetry.logPath;
304
351
  }
305
- /** Validate the `witness` section (CONFIG-05) — both values are consumed at assembly time. */
352
+ /** Validate the `witness` section — both values are consumed at assembly time. */
306
353
  function validateWitness(witness) {
307
354
  if (!isPlainObject(witness)) {
308
355
  throw new ConfigValidationError('witness must be an object');
@@ -319,8 +366,7 @@ function validateWitness(witness) {
319
366
  }
320
367
  /**
321
368
  * Validate parsed unknown data as a {@link PolydeukesConfig} and return a
322
- * {@link ResolvedConfig} with defaults filled and templates compiled (PRD §4.3).
323
- * Pure — no file I/O.
369
+ * {@link ResolvedConfig} with defaults filled and templates compiled. Pure — no file I/O.
324
370
  *
325
371
  * Throws {@link ConfigValidationError} (naming the offending field path) when the top level
326
372
  * is not a plain object, any object level carries an unknown key, `languages` is
@@ -334,15 +380,17 @@ export function defineConfig(config) {
334
380
  throw new ConfigValidationError('config must be a plain object');
335
381
  }
336
382
  rejectUnknownKeys(config, TOP_LEVEL_KEYS, 'config');
337
- // `$schema` is an IDE schema reference (CONFIG-03): accepted, type-checked, and
338
- // ignored — it never appears in the resolution output.
383
+ // `$schema` is an IDE schema reference: accepted, type-checked, and ignored — it never
384
+ // appears in the resolution output.
339
385
  if (config.$schema !== undefined && typeof config.$schema !== 'string') {
340
386
  throw new ConfigValidationError('$schema must be a string');
341
387
  }
342
388
  const resolvedLanguages = validateLanguages(config.languages);
343
389
  const protectedPaths = config.protectedPaths !== undefined ? validateProtectedPaths(config.protectedPaths) : undefined;
344
390
  const adapters = config.adapters !== undefined ? validateAdapters(config.adapters) : undefined;
345
- const disciplines = config.disciplines !== undefined ? validateDisciplines(config.disciplines) : undefined;
391
+ const split = config.disciplines !== undefined ? validateDisciplines(config.disciplines) : undefined;
392
+ const disciplines = split?.judged;
393
+ const drafts = split !== undefined && split.drafts.length > 0 ? split.drafts : undefined;
346
394
  const logPath = config.telemetry !== undefined ? validateTelemetry(config.telemetry) : undefined;
347
395
  const witness = config.witness !== undefined ? validateWitness(config.witness) : undefined;
348
396
  return {
@@ -353,6 +401,7 @@ export function defineConfig(config) {
353
401
  logPath: logPath ?? DEFAULT_TELEMETRY_LOG_PATH,
354
402
  },
355
403
  ...(disciplines !== undefined && { disciplines }),
404
+ ...(drafts !== undefined && { drafts }),
356
405
  ...(witness !== undefined && { witness }),
357
406
  };
358
407
  }
@@ -1,16 +1,16 @@
1
1
  /**
2
- * exit-codes — the covenant protocol's exit-code vocabulary (CORE-01 PRD §4.1).
2
+ * exit-codes — the covenant protocol's exit-code vocabulary.
3
3
  *
4
- * The three codes are distinct and ordered by severity. The covenant *body* only
5
- * ever emits `0` (uphold) or `1` (break, non-blocking); translating a break into the
6
- * blocking `2` is the wrapper's job (COVENANT-01), never the core's. The sole place
7
- * the core itself reaches for `2` is the fail-closed parse path in the barrel.
4
+ * The three codes are distinct and ordered by severity. The covenant *body* only ever
5
+ * emits `0` (uphold) or `1` (break, non-blocking); translating a break into the blocking
6
+ * `2` is the wrapper's job, never the core's. The sole place the core itself reaches for
7
+ * `2` is the fail-closed parse path in the barrel.
8
8
  *
9
9
  * These live in their own module rather than the barrel because `fail-policy.ts` needs
10
- * them: importing them from the barrel, which re-exports fail-policy, is an
11
- * initialization cycle — the constants read as `undefined` depending on which module
12
- * the runtime evaluates first (CLEANUP-01 F1). The barrel re-exports them, so every
13
- * consumer outside core still reaches them at the same path.
10
+ * them: importing them from the barrel, which re-exports fail-policy, is an initialization
11
+ * cycle — the constants read as `undefined` depending on which module the runtime evaluates
12
+ * first. The barrel re-exports them, so every consumer outside core still reaches them at
13
+ * the same path.
14
14
  */
15
15
  /** Promise upheld — no violation, the edit/push passes. */
16
16
  export declare const EXIT_UPHOLD = 0;
@@ -1,16 +1,16 @@
1
1
  /**
2
- * exit-codes — the covenant protocol's exit-code vocabulary (CORE-01 PRD §4.1).
2
+ * exit-codes — the covenant protocol's exit-code vocabulary.
3
3
  *
4
- * The three codes are distinct and ordered by severity. The covenant *body* only
5
- * ever emits `0` (uphold) or `1` (break, non-blocking); translating a break into the
6
- * blocking `2` is the wrapper's job (COVENANT-01), never the core's. The sole place
7
- * the core itself reaches for `2` is the fail-closed parse path in the barrel.
4
+ * The three codes are distinct and ordered by severity. The covenant *body* only ever
5
+ * emits `0` (uphold) or `1` (break, non-blocking); translating a break into the blocking
6
+ * `2` is the wrapper's job, never the core's. The sole place the core itself reaches for
7
+ * `2` is the fail-closed parse path in the barrel.
8
8
  *
9
9
  * These live in their own module rather than the barrel because `fail-policy.ts` needs
10
- * them: importing them from the barrel, which re-exports fail-policy, is an
11
- * initialization cycle — the constants read as `undefined` depending on which module
12
- * the runtime evaluates first (CLEANUP-01 F1). The barrel re-exports them, so every
13
- * consumer outside core still reaches them at the same path.
10
+ * them: importing them from the barrel, which re-exports fail-policy, is an initialization
11
+ * cycle — the constants read as `undefined` depending on which module the runtime evaluates
12
+ * first. The barrel re-exports them, so every consumer outside core still reaches them at
13
+ * the same path.
14
14
  */
15
15
  /** Promise upheld — no violation, the edit/push passes. */
16
16
  export const EXIT_UPHOLD = 0;