@polydeukes/core 0.4.0 → 0.6.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.
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `catalogue.ts` — the eighteen judgment-mechanism names and the shape each one admits.
3
+ *
4
+ * A mechanism name is a coordinate the machine checks, not a label: {@link deriveShape}
5
+ * reads a declaration's shape from its syntax alone — which sources its `source` steps
6
+ * name, which relations its body relates, whether it carries a witness block — and
7
+ * {@link validateMechanism} refuses a declaration whose derived shape falls outside the
8
+ * spec of the name it carries. Nothing here runs an extraction or opens a world.
9
+ */
10
+ import type { AlgebraDeclaration, RelationName } from './algebra.ts';
11
+ /** The four axes a declaration can read, closed. This tuple is the single source of the list. */
12
+ export declare const AXIS_NAMES: readonly ['change', 'actor', 'world', 'history'];
13
+ /** One of the four axes — the closed vocabulary of the axis position. */
14
+ export type Axis = (typeof AXIS_NAMES)[number];
15
+ /** The judgment mechanisms, closed. A name outside this tuple is refused, never coerced. */
16
+ export declare const MECHANISM_NAMES: readonly ['pairing', 'companion', 'monotonic-order', 'fingerprint-sync', 'producer-owned', 'self-absolution-ban', 'actor-scope', 'precedent', 'phase-order', 'turn-locality', 'stated-ground', 'controlled-vocabulary', 'naming', 'added-only', 'one-way-marker', 'delegated-scope', 'scoped-valve', 'forbidden-command'];
17
+ /** One of the eighteen mechanism names. */
18
+ export type MechanismName = (typeof MECHANISM_NAMES)[number];
19
+ /**
20
+ * What one mechanism name admits: the axes it may read, the relations it may relate, and
21
+ * the structural markers it requires.
22
+ *
23
+ * `requiresWitness` asks for the valve block; `scopeSource` pins what the declaration may
24
+ * scope on; `reserved` names what will define the name, and stands in place of a
25
+ * shape — a reserved name admits no declaration at all.
26
+ */
27
+ export type MechanismShape = {
28
+ axes: ReadonlySet<Axis>;
29
+ relations: ReadonlySet<RelationName>;
30
+ requiresWitness?: true;
31
+ scopeSource?: 'target.path' | 'command';
32
+ reserved?: string;
33
+ };
34
+ /**
35
+ * The blocks {@link deriveShape} reads. A declaration names itself with its `discipline`,
36
+ * and an entry under `declare` names itself with the entry id instead — the shape is the
37
+ * same either way, so the derivation takes the blocks rather than the whole document.
38
+ */
39
+ export type DerivableDeclaration = Pick<AlgebraDeclaration, 'extract' | 'relate' | 'sources' | 'witness'>;
40
+ /** The derived shape of one declaration: what its syntax says it reads and relates. */
41
+ export type DerivedShape = {
42
+ axes: ReadonlySet<Axis>;
43
+ relations: ReadonlySet<RelationName>;
44
+ witness: boolean;
45
+ };
46
+ /** Every name's spec. The `Record` type pins the keys to {@link MECHANISM_NAMES}. */
47
+ export declare const MECHANISM_SHAPES: Record<MechanismName, MechanismShape>;
48
+ /**
49
+ * Read one declaration's shape from its syntax (pure).
50
+ *
51
+ * The axis of a source name is where the name comes from: the fixed name `actor` is the
52
+ * actor axis and the other six fixed names the change axis, a name the declaration's own
53
+ * `sources` block binds is the world axis unless the binding is of the transcript kind,
54
+ * which is the history axis. A name that is neither is refused by
55
+ * {@link validateMechanism} — skipping it would derive the empty set, which is a subset of
56
+ * every spec, and an axis-restricted name would load on a typo. The witness block's
57
+ * `extract` reads a world too, so its source steps count; its `relate` does not, because the
58
+ * valve's relation is not the judgment's.
59
+ */
60
+ export declare function deriveShape(declaration: DerivableDeclaration): DerivedShape;
61
+ /**
62
+ * Check the declaration's `mechanism` against the catalogue (throws on a mismatch).
63
+ *
64
+ * The order is the author's repair order: an unknown name first (nothing else is
65
+ * meaningful without a spec), then the reserved name, then the structural markers, then
66
+ * the axes and relations the derived shape must stay inside. Membership is subset, not
67
+ * equality — a name admitting two relations accepts a declaration using one of them.
68
+ */
69
+ export declare function validateMechanism(declaration: AlgebraDeclaration, location: string): void;
@@ -0,0 +1,191 @@
1
+ /**
2
+ * `catalogue.ts` — the eighteen judgment-mechanism names and the shape each one admits.
3
+ *
4
+ * A mechanism name is a coordinate the machine checks, not a label: {@link deriveShape}
5
+ * reads a declaration's shape from its syntax alone — which sources its `source` steps
6
+ * name, which relations its body relates, whether it carries a witness block — and
7
+ * {@link validateMechanism} refuses a declaration whose derived shape falls outside the
8
+ * spec of the name it carries. Nothing here runs an extraction or opens a world.
9
+ */
10
+ import { isPlainObject } from './is-plain-object.js';
11
+ import { FIXED_SOURCE_NAMES } from './source-names.js';
12
+ import { ConfigValidationError } from './validation.js';
13
+ /** The four axes a declaration can read, closed. This tuple is the single source of the list. */
14
+ export const AXIS_NAMES = ['change', 'actor', 'world', 'history'];
15
+ /** The judgment mechanisms, closed. A name outside this tuple is refused, never coerced. */
16
+ export const MECHANISM_NAMES = [
17
+ 'pairing',
18
+ 'companion',
19
+ 'monotonic-order',
20
+ 'fingerprint-sync',
21
+ 'producer-owned',
22
+ 'self-absolution-ban',
23
+ 'actor-scope',
24
+ 'precedent',
25
+ 'phase-order',
26
+ 'turn-locality',
27
+ 'stated-ground',
28
+ 'controlled-vocabulary',
29
+ 'naming',
30
+ 'added-only',
31
+ 'one-way-marker',
32
+ 'delegated-scope',
33
+ 'scoped-valve',
34
+ 'forbidden-command',
35
+ ];
36
+ const CHANGE = new Set(['change']);
37
+ const WORLD = new Set(['world']);
38
+ const ACTOR = new Set(['actor']);
39
+ const HISTORY = new Set(['history']);
40
+ const HISTORY_WORLD = new Set(['history', 'world']);
41
+ const CHANGE_WORLD = new Set(['change', 'world']);
42
+ /** The one fixed source name whose value is the actor rather than the change. */
43
+ const ACTOR_SOURCE = 'actor';
44
+ /** Every name's spec. The `Record` type pins the keys to {@link MECHANISM_NAMES}. */
45
+ export const MECHANISM_SHAPES = {
46
+ pairing: { axes: WORLD, relations: new Set(['equal']) },
47
+ companion: { axes: CHANGE_WORLD, relations: new Set(['implies']) },
48
+ 'monotonic-order': { axes: CHANGE_WORLD, relations: new Set(['ordered']) },
49
+ 'fingerprint-sync': { axes: WORLD, relations: new Set(['equal']) },
50
+ 'producer-owned': { axes: ACTOR, relations: new Set(['empty', 'nonEmpty']) },
51
+ 'self-absolution-ban': {
52
+ axes: CHANGE,
53
+ relations: new Set(['unchanged', 'empty']),
54
+ },
55
+ 'actor-scope': { axes: ACTOR, relations: new Set(['empty', 'nonEmpty']) },
56
+ // The spawn sidecar is a world-axis channel carrying session history, so a precedent
57
+ // read off it is still a precedent; the transcript source carries the history axis.
58
+ precedent: { axes: HISTORY_WORLD, relations: new Set(['nonEmpty']) },
59
+ 'phase-order': { axes: HISTORY, relations: new Set(['ordered']) },
60
+ 'turn-locality': { axes: HISTORY, relations: new Set(['nonEmpty']) },
61
+ 'stated-ground': { axes: HISTORY, relations: new Set(['nonEmpty']) },
62
+ 'controlled-vocabulary': { axes: CHANGE_WORLD, relations: new Set(['subset']) },
63
+ naming: {
64
+ axes: CHANGE,
65
+ relations: new Set(['empty', 'nonEmpty']),
66
+ scopeSource: 'target.path',
67
+ },
68
+ 'added-only': { axes: CHANGE, relations: new Set(['empty']) },
69
+ 'one-way-marker': { axes: CHANGE, relations: new Set(['subset']) },
70
+ 'delegated-scope': {
71
+ axes: new Set(),
72
+ relations: new Set(),
73
+ reserved: 'the definition-time evaluator',
74
+ },
75
+ 'scoped-valve': {
76
+ axes: new Set(AXIS_NAMES),
77
+ relations: new Set([
78
+ 'empty',
79
+ 'nonEmpty',
80
+ 'equal',
81
+ 'subset',
82
+ 'implies',
83
+ 'ordered',
84
+ 'unchanged',
85
+ ]),
86
+ requiresWitness: true,
87
+ },
88
+ // A command-line ban scopes on the command it reads: a world with no shell call carries
89
+ // no `command`, and a scope-less reader would refuse every file-changing call as unjudgeable.
90
+ 'forbidden-command': {
91
+ axes: CHANGE,
92
+ relations: new Set(['empty']),
93
+ scopeSource: 'command',
94
+ },
95
+ };
96
+ /** The names a rejection message lists so the author sees what is admitted. */
97
+ function quotedList(names) {
98
+ return names.map((name) => `'${name}'`).join(', ');
99
+ }
100
+ /** Whether one `sources` binding names the session history rather than a world value. */
101
+ function isTranscriptSource(binding) {
102
+ return isPlainObject(binding) && binding.transcript === true;
103
+ }
104
+ /** The source names one extract block's `source` steps name, in declaration order. */
105
+ function sourceNamesOf(extract) {
106
+ if (extract === undefined)
107
+ return [];
108
+ const names = [];
109
+ for (const steps of Object.values(extract)) {
110
+ for (const step of steps) {
111
+ if (isPlainObject(step) && step.op === 'source' && typeof step.of === 'string') {
112
+ names.push(step.of);
113
+ }
114
+ }
115
+ }
116
+ return names;
117
+ }
118
+ /** Every source name the body and the valve read, in declaration order. */
119
+ function sourceNames(declaration) {
120
+ return [...sourceNamesOf(declaration.extract), ...sourceNamesOf(declaration.witness?.extract)];
121
+ }
122
+ /**
123
+ * Read one declaration's shape from its syntax (pure).
124
+ *
125
+ * The axis of a source name is where the name comes from: the fixed name `actor` is the
126
+ * actor axis and the other six fixed names the change axis, a name the declaration's own
127
+ * `sources` block binds is the world axis unless the binding is of the transcript kind,
128
+ * which is the history axis. A name that is neither is refused by
129
+ * {@link validateMechanism} — skipping it would derive the empty set, which is a subset of
130
+ * every spec, and an axis-restricted name would load on a typo. The witness block's
131
+ * `extract` reads a world too, so its source steps count; its `relate` does not, because the
132
+ * valve's relation is not the judgment's.
133
+ */
134
+ export function deriveShape(declaration) {
135
+ const bindings = declaration.sources ?? {};
136
+ const axes = new Set();
137
+ for (const name of sourceNames(declaration)) {
138
+ if (FIXED_SOURCE_NAMES.includes(name)) {
139
+ axes.add(name === ACTOR_SOURCE ? 'actor' : 'change');
140
+ }
141
+ else if (name in bindings) {
142
+ axes.add(isTranscriptSource(bindings[name]) ? 'history' : 'world');
143
+ }
144
+ }
145
+ const relations = new Set(declaration.relate.map((entry) => entry.relation.op));
146
+ return { axes, relations, witness: declaration.witness !== undefined };
147
+ }
148
+ /** Refuse a `source` step naming neither a fixed source nor a binding of this declaration. */
149
+ function checkSourceNames(declaration, location) {
150
+ const declared = new Set(Object.keys(declaration.sources ?? {}));
151
+ for (const name of sourceNames(declaration)) {
152
+ if (FIXED_SOURCE_NAMES.includes(name) || declared.has(name))
153
+ continue;
154
+ const bindings = declared.size === 0 ? 'and this declaration binds none' : `or ${quotedList([...declared])}`;
155
+ throw new ConfigValidationError(`${location} reads the source '${name}', which is neither one of ${quotedList(FIXED_SOURCE_NAMES)} ${bindings}`);
156
+ }
157
+ }
158
+ /**
159
+ * Check the declaration's `mechanism` against the catalogue (throws on a mismatch).
160
+ *
161
+ * The order is the author's repair order: an unknown name first (nothing else is
162
+ * meaningful without a spec), then the reserved name, then the structural markers, then
163
+ * the axes and relations the derived shape must stay inside. Membership is subset, not
164
+ * equality — a name admitting two relations accepts a declaration using one of them.
165
+ */
166
+ export function validateMechanism(declaration, location) {
167
+ const mechanism = declaration.mechanism;
168
+ if (!MECHANISM_NAMES.includes(mechanism)) {
169
+ throw new ConfigValidationError(`${location} mechanism is '${mechanism}' — must be one of ${quotedList(MECHANISM_NAMES)}`);
170
+ }
171
+ const spec = MECHANISM_SHAPES[mechanism];
172
+ if (spec.reserved !== undefined) {
173
+ throw new ConfigValidationError(`${location} mechanism '${mechanism}' is reserved for ${spec.reserved}, which does not exist yet`);
174
+ }
175
+ checkSourceNames(declaration, location);
176
+ const shape = deriveShape(declaration);
177
+ if (spec.requiresWitness === true && !shape.witness) {
178
+ throw new ConfigValidationError(`${location} mechanism '${mechanism}' needs a witness block — the valve is its whole shape`);
179
+ }
180
+ if (spec.scopeSource !== undefined && declaration.scope?.source !== spec.scopeSource) {
181
+ const actual = declaration.scope === undefined
182
+ ? 'this declaration has no scope block'
183
+ : `this declaration scopes on '${declaration.scope.source}'`;
184
+ throw new ConfigValidationError(`${location} mechanism '${mechanism}' scopes on '${spec.scopeSource}' — ${actual}`);
185
+ }
186
+ const outsideAxes = [...shape.axes].filter((axis) => !spec.axes.has(axis));
187
+ const outsideRelations = [...shape.relations].filter((relation) => !spec.relations.has(relation));
188
+ if (outsideAxes.length > 0 || outsideRelations.length > 0) {
189
+ throw new ConfigValidationError(`${location} mechanism '${mechanism}' expects relations ${quotedList([...spec.relations])} on axes ${quotedList([...spec.axes])}; this declaration relates ${quotedList([...shape.relations])} on ${quotedList([...shape.axes])}`);
190
+ }
191
+ }
package/dist/config.d.ts CHANGED
@@ -1,18 +1,17 @@
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
+ import { type AlgebraDeclaration } from './algebra.ts';
10
+ export { ConfigValidationError } from './validation.ts';
11
+ /** Conventional default telemetry log path — local-only observation data. */
13
12
  export declare const DEFAULT_TELEMETRY_LOG_PATH = ".polydeukes/roi.log";
14
13
  /**
15
- * `LanguageProfile` — the unit of the language axis (PRD §4.1).
14
+ * `LanguageProfile` — the unit of the language axis.
16
15
  *
17
16
  * `testCmd` is a shell command template: every literal `{scope}` token is substituted at
18
17
  * resolve time, and the core only carries the resulting string — it never interprets it.
@@ -27,80 +26,86 @@ export type LanguageProfile = {
27
26
  testCmd: string;
28
27
  };
29
28
  /**
30
- * `DisciplineForbid` — the delta-family predicate value (COVENANT-10 §4.1).
29
+ * `AlgebraDeclarationBody` — an algebra declaration minus its name.
31
30
  *
32
- * The string shorthand is equivalent to `{ added }`; `removed`/`present` directions are
33
- * deferred (COVENANT-12) and rejected by validation.
31
+ * An entry names its declaration with the entry's `id`, so the block under `declare`
32
+ * carries every other key and never `discipline`.
34
33
  */
35
- export type DisciplineForbid = string | {
36
- added: string;
37
- };
34
+ export type AlgebraDeclarationBody = Omit<AlgebraDeclaration, 'discipline'>;
35
+ /**
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';
38
41
  /**
39
- * `DisciplineEntry` — one user-declared discipline (COVENANT-10 §4.1). Pure JSON data.
42
+ * `DisciplineEntry` — one user-declared discipline. Pure JSON data.
40
43
  *
41
- * Exactly one predicate key (`forbid` | `immutable` | `forbidCommand` |
42
- * `requirePrecedent`) per entry; `in`/`except` scope the delta and context families.
43
- * Compilation is the covenant package's job the core validates compilability of regex
44
- * strings but never executes them.
44
+ * The judgment is the `declare` block and nothing else; the other three keys name the
45
+ * entry, explain it, and set its level. Compilation is the covenant package's job — the
46
+ * core validates the block's grammar but never runs an extraction.
45
47
  */
46
48
  export type DisciplineEntry = {
47
49
  /** unique handle — telemetry label and verdict reason prefix */
48
50
  id: string;
49
- /** prose rationale, documentation only — never judged */
51
+ /** prose rationale — never judged, and carried into the break message */
50
52
  why?: string;
51
- /** delta/context-family scope: glob(s) the file path must match (absent = every file change) */
52
- in?: string | string[];
53
- /** delta/context-family scope: glob(s) excluded after `in` */
54
- except?: string | string[];
55
- /** delta family — string shorthand = { added } */
56
- forbid?: DisciplineForbid;
57
- /** path familyits own glob is the scope */
58
- immutable?: string | string[];
59
- /** command family — regex over shell command strings */
60
- forbidCommand?: string;
61
- /** context-family trigger: added-direction delta regex (absent = every in-scope change) */
62
- when?: string;
63
- /**
64
- * context family — the session evidence one edit requires beforehand. Exactly one
65
- * evidence key. The core owns and fully validates `command`; every other key is
66
- * adapter vocabulary whose value passes through verbatim (CONFIG-07 layering).
67
- */
68
- requirePrecedent?: Record<string, unknown>;
53
+ /** the author's level; composes with the observer's surface level, lenient side winning */
54
+ enforce?: EnforceLevel;
55
+ /** one judgment written as data — the entry's `id` is the declaration's name */
56
+ declare: AlgebraDeclarationBody;
57
+ };
58
+ /**
59
+ * `DisciplineDraft`an unpromoted discipline: the promotion ladder's first rung,
60
+ * registered as prose ahead of any declaration.
61
+ *
62
+ * A draft is declared, never inferred — only the literal `draft: true` makes one, and an
63
+ * entry with neither a `declare` block nor the marker stays a validation error. It carries
64
+ * no declaration, produces no registration, no judgment, and no telemetry row;
65
+ * `pdks explain` renders it as unpromoted.
66
+ */
67
+ export type DisciplineDraft = {
68
+ /** unique handle in the same label space as judged entries and meta-covenant labels */
69
+ id: string;
70
+ /** the draft's whole body — required prose, unlike the judged families' optional why */
71
+ why: string;
72
+ /** the explicit marker; only the literal true exists (false is rejected as dead data) */
73
+ draft: true;
69
74
  };
70
75
  /**
71
- * `PolydeukesConfig` — the input shape a user writes (PRD §4.1). JSON-serializable data.
76
+ * `PolydeukesConfig` — the input shape a user writes. JSON-serializable data.
72
77
  *
73
78
  * Language keys (`typescript`, `python`, …) are user *values*, not the core's vocabulary —
74
79
  * no language or tool literal appears in the core source.
75
80
  */
76
81
  export type PolydeukesConfig = {
77
- /** IDE schema reference (CONFIG-03) — accepted and ignored, never part of the resolution */
82
+ /** IDE schema reference — accepted and ignored, never part of the resolution */
78
83
  $schema?: string;
79
84
  /** language axis, first-class. keys are user values ('typescript', 'python', …) */
80
85
  languages: Record<string, LanguageProfile>;
81
- /** raw protected path patterns — normalization is CONFIG-02's job */
86
+ /** raw protected path patterns — normalized downstream, never here */
82
87
  protectedPaths?: string[];
83
88
  /**
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)
89
+ * adapter namespaces — keys are ecosystem values (never validated), each value is that
90
+ * adapter's own settings object, passed through verbatim (the vocabulary belongs to the
91
+ * adapter, whose own validator judges the contents)
87
92
  */
88
93
  adapters?: Record<string, Record<string, unknown>>;
89
94
  telemetry?: {
90
- /** conventional default applies when omitted (§4.3) */
95
+ /** conventional default applies when omitted */
91
96
  logPath?: string;
92
97
  };
93
98
  /** user-declared disciplines — validated here, compiled by the covenant package */
94
- disciplines?: DisciplineEntry[];
99
+ disciplines?: (DisciplineEntry | DisciplineDraft)[];
95
100
  /**
96
- * TTL witness values for the covenant valve seam (CONFIG-05) — consumed at
97
- * assembly time, validated here
101
+ * TTL witness values for the covenant valve seam — consumed at assembly time,
102
+ * validated here
98
103
  */
99
104
  witness?: {
100
105
  /**
101
106
  * 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)
107
+ * mid-sentence is a mention, not an invocation. Non-empty after trimming; the value
108
+ * itself is free, since provenance rather than secrecy is the defence
104
109
  */
105
110
  token: string;
106
111
  /** validity window in minutes from the user message's timestamp — finite and > 0 */
@@ -110,11 +115,11 @@ export type PolydeukesConfig = {
110
115
  /**
111
116
  * `ResolvedLanguageProfile` — a {@link LanguageProfile} with its template compiled.
112
117
  *
113
- * Consumers keep the callable shape (`testCmd(scope)`), identical to schema v1 (LEDGER-05).
118
+ * Consumers keep the callable shape (`testCmd(scope)`).
114
119
  */
115
120
  export type ResolvedLanguageProfile = {
116
121
  productionGlob: string | string[];
117
- /** compiled from the template — consumers keep the callable shape (LEDGER-05) */
122
+ /** compiled from the template — consumers keep the callable shape */
118
123
  testCmd: (scope: string) => string;
119
124
  };
120
125
  /**
@@ -130,28 +135,23 @@ export type ResolvedConfig = {
130
135
  telemetry: {
131
136
  logPath: string;
132
137
  };
133
- /** validated discipline data, passed through verbatim (absent stays absent) */
138
+ /**
139
+ * validated judged entries only — drafts are split out at resolution time so the
140
+ * covenant compiler has no path that receives one. Present whenever the input
141
+ * declared a `disciplines` array, holding exactly its judged entries in order.
142
+ */
134
143
  disciplines?: DisciplineEntry[];
144
+ /** validated drafts in declaration order (absent when the input carries none) */
145
+ drafts?: DisciplineDraft[];
135
146
  /** validated witness data, passed through verbatim (absent stays absent) */
136
147
  witness?: {
137
148
  token: string;
138
149
  ttlMinutes: number;
139
150
  };
140
151
  };
141
- /**
142
- * `ConfigValidationError` — raised when a config fails structural validation (PRD §4.3).
143
- *
144
- * The message names the offending field path so the developer sees exactly what is wrong.
145
- * This throw is a developer-time error (config authoring), a different axis from the
146
- * covenant runtime's fail-closed exit code — a bad config should fail loud and early.
147
- */
148
- export declare class ConfigValidationError extends Error {
149
- constructor(message: string);
150
- }
151
152
  /**
152
153
  * 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.
154
+ * {@link ResolvedConfig} with defaults filled and templates compiled. Pure — no file I/O.
155
155
  *
156
156
  * Throws {@link ConfigValidationError} (naming the offending field path) when the top level
157
157
  * is not a plain object, any object level carries an unknown key, `languages` is