@polydeukes/core 0.5.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.
- package/README.ko.md +2 -1
- package/README.md +2 -1
- package/dist/algebra.d.ts +174 -0
- package/dist/algebra.js +438 -0
- package/dist/catalogue.d.ts +69 -0
- package/dist/catalogue.js +191 -0
- package/dist/config.d.ts +15 -42
- package/dist/config.js +26 -164
- package/dist/exit-codes.d.ts +3 -3
- package/dist/exit-codes.js +3 -3
- package/dist/index.d.ts +10 -94
- package/dist/index.js +4 -53
- package/dist/is-plain-object.d.ts +3 -0
- package/dist/is-plain-object.js +3 -0
- package/dist/protocol.d.ts +152 -0
- package/dist/protocol.js +121 -0
- package/dist/source-names.d.ts +10 -0
- package/dist/source-names.js +18 -0
- package/dist/telemetry.d.ts +32 -5
- package/dist/telemetry.js +59 -8
- package/dist/transcript.d.ts +2 -9
- package/dist/transcript.js +1 -6
- package/dist/validation.d.ts +24 -0
- package/dist/validation.js +44 -0
- package/package.json +3 -2
- package/schema/algebra-declaration.schema.json +402 -0
- package/schema/polydeukes.schema.json +38 -91
|
@@ -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
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* saw. It stays a pure function — no file I/O and no runtime dependencies: validation is
|
|
7
7
|
* hand-rolled, and the published JSON Schema is a sibling artifact this source never reads.
|
|
8
8
|
*/
|
|
9
|
+
import { type AlgebraDeclaration } from './algebra.ts';
|
|
10
|
+
export { ConfigValidationError } from './validation.ts';
|
|
9
11
|
/** Conventional default telemetry log path — local-only observation data. */
|
|
10
12
|
export declare const DEFAULT_TELEMETRY_LOG_PATH = ".polydeukes/roi.log";
|
|
11
13
|
/**
|
|
@@ -24,14 +26,12 @@ export type LanguageProfile = {
|
|
|
24
26
|
testCmd: string;
|
|
25
27
|
};
|
|
26
28
|
/**
|
|
27
|
-
* `
|
|
29
|
+
* `AlgebraDeclarationBody` — an algebra declaration minus its name.
|
|
28
30
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
+
* An entry names its declaration with the entry's `id`, so the block under `declare`
|
|
32
|
+
* carries every other key and never `discipline`.
|
|
31
33
|
*/
|
|
32
|
-
export type
|
|
33
|
-
added: string;
|
|
34
|
-
};
|
|
34
|
+
export type AlgebraDeclarationBody = Omit<AlgebraDeclaration, 'discipline'>;
|
|
35
35
|
/**
|
|
36
36
|
* `EnforceLevel` — an entry's own rung on the promotion ladder. `advise` records a break
|
|
37
37
|
* without stopping it; `block` pins the entry at block whatever default the ladder later
|
|
@@ -41,10 +41,9 @@ export type EnforceLevel = 'block' | 'advise';
|
|
|
41
41
|
/**
|
|
42
42
|
* `DisciplineEntry` — one user-declared discipline. Pure JSON data.
|
|
43
43
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* 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.
|
|
48
47
|
*/
|
|
49
48
|
export type DisciplineEntry = {
|
|
50
49
|
/** unique handle — telemetry label and verdict reason prefix */
|
|
@@ -53,33 +52,17 @@ export type DisciplineEntry = {
|
|
|
53
52
|
why?: string;
|
|
54
53
|
/** the author's level; composes with the observer's surface level, lenient side winning */
|
|
55
54
|
enforce?: EnforceLevel;
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
/** delta/context-family scope: glob(s) excluded after `in` */
|
|
59
|
-
except?: string | string[];
|
|
60
|
-
/** delta family — string shorthand = { added } */
|
|
61
|
-
forbid?: DisciplineForbid;
|
|
62
|
-
/** path family — its own glob is the scope */
|
|
63
|
-
immutable?: string | string[];
|
|
64
|
-
/** command family — regex over shell command strings */
|
|
65
|
-
forbidCommand?: string;
|
|
66
|
-
/** context-family trigger: added-direction delta regex (absent = every in-scope change) */
|
|
67
|
-
when?: string;
|
|
68
|
-
/**
|
|
69
|
-
* context family — the session evidence one edit requires beforehand. Exactly one
|
|
70
|
-
* evidence key. The core owns and fully validates `command`; every other key is
|
|
71
|
-
* adapter vocabulary whose value passes through verbatim.
|
|
72
|
-
*/
|
|
73
|
-
requirePrecedent?: Record<string, unknown>;
|
|
55
|
+
/** one judgment written as data — the entry's `id` is the declaration's name */
|
|
56
|
+
declare: AlgebraDeclarationBody;
|
|
74
57
|
};
|
|
75
58
|
/**
|
|
76
59
|
* `DisciplineDraft` — an unpromoted discipline: the promotion ladder's first rung,
|
|
77
|
-
* registered as prose ahead of any
|
|
60
|
+
* registered as prose ahead of any declaration.
|
|
78
61
|
*
|
|
79
62
|
* A draft is declared, never inferred — only the literal `draft: true` makes one, and an
|
|
80
|
-
* entry with neither a
|
|
81
|
-
*
|
|
82
|
-
*
|
|
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.
|
|
83
66
|
*/
|
|
84
67
|
export type DisciplineDraft = {
|
|
85
68
|
/** unique handle in the same label space as judged entries and meta-covenant labels */
|
|
@@ -166,16 +149,6 @@ export type ResolvedConfig = {
|
|
|
166
149
|
ttlMinutes: number;
|
|
167
150
|
};
|
|
168
151
|
};
|
|
169
|
-
/**
|
|
170
|
-
* `ConfigValidationError` — raised when a config fails structural validation.
|
|
171
|
-
*
|
|
172
|
-
* The message names the offending field path so the developer sees exactly what is wrong.
|
|
173
|
-
* This throw is a developer-time error (config authoring), a different axis from the
|
|
174
|
-
* covenant runtime's fail-closed exit code — a bad config should fail loud and early.
|
|
175
|
-
*/
|
|
176
|
-
export declare class ConfigValidationError extends Error {
|
|
177
|
-
constructor(message: string);
|
|
178
|
-
}
|
|
179
152
|
/**
|
|
180
153
|
* Validate parsed unknown data as a {@link PolydeukesConfig} and return a
|
|
181
154
|
* {@link ResolvedConfig} with defaults filled and templates compiled. Pure — no file I/O.
|
package/dist/config.js
CHANGED
|
@@ -6,22 +6,12 @@
|
|
|
6
6
|
* saw. It stays a pure function — no file I/O and no runtime dependencies: validation is
|
|
7
7
|
* hand-rolled, and the published JSON Schema is a sibling artifact this source never reads.
|
|
8
8
|
*/
|
|
9
|
+
import { validateAlgebraDeclaration } from './algebra.js';
|
|
9
10
|
import { isPlainObject } from './is-plain-object.js';
|
|
11
|
+
import { ConfigValidationError, isNonEmptyString, isStringArray, rejectUnknownKeys, } from './validation.js';
|
|
12
|
+
export { ConfigValidationError } from './validation.js';
|
|
10
13
|
/** Conventional default telemetry log path — local-only observation data. */
|
|
11
14
|
export const DEFAULT_TELEMETRY_LOG_PATH = '.polydeukes/roi.log';
|
|
12
|
-
/**
|
|
13
|
-
* `ConfigValidationError` — raised when a config fails structural validation.
|
|
14
|
-
*
|
|
15
|
-
* The message names the offending field path so the developer sees exactly what is wrong.
|
|
16
|
-
* This throw is a developer-time error (config authoring), a different axis from the
|
|
17
|
-
* covenant runtime's fail-closed exit code — a bad config should fail loud and early.
|
|
18
|
-
*/
|
|
19
|
-
export class ConfigValidationError extends Error {
|
|
20
|
-
constructor(message) {
|
|
21
|
-
super(message);
|
|
22
|
-
this.name = 'ConfigValidationError';
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
15
|
/** Labels the assembly reserves for the judging chain's own registrations. */
|
|
26
16
|
const META_COVENANT_LABELS = ['self-mod', 'shell-mod', 'transcript-mod'];
|
|
27
17
|
const TOP_LEVEL_KEYS = new Set([
|
|
@@ -36,44 +26,15 @@ const TOP_LEVEL_KEYS = new Set([
|
|
|
36
26
|
const PROFILE_KEYS = new Set(['productionGlob', 'testCmd']);
|
|
37
27
|
const TELEMETRY_KEYS = new Set(['logPath']);
|
|
38
28
|
const WITNESS_KEYS = new Set(['token', 'ttlMinutes']);
|
|
39
|
-
const DISCIPLINE_KEYS = new Set([
|
|
40
|
-
'id',
|
|
41
|
-
'why',
|
|
42
|
-
'enforce',
|
|
43
|
-
'in',
|
|
44
|
-
'except',
|
|
45
|
-
'forbid',
|
|
46
|
-
'immutable',
|
|
47
|
-
'forbidCommand',
|
|
48
|
-
'when',
|
|
49
|
-
'requirePrecedent',
|
|
50
|
-
]);
|
|
29
|
+
const DISCIPLINE_KEYS = new Set(['id', 'why', 'enforce', 'declare']);
|
|
51
30
|
const DRAFT_KEYS = new Set(['id', 'why', 'draft']);
|
|
52
31
|
const ENFORCE_LEVELS = new Set(['block', 'advise']);
|
|
53
|
-
const PREDICATE_KEYS = ['forbid', 'immutable', 'forbidCommand', 'requirePrecedent'];
|
|
54
|
-
/** Predicate families that `in`/`except` may scope — delta and context. */
|
|
55
|
-
const SCOPED_PREDICATE_KEYS = new Set(['forbid', 'requirePrecedent']);
|
|
56
|
-
/** Throw on the first key outside the allowed vocabulary, naming the key and its location. */
|
|
57
|
-
function rejectUnknownKeys(record, allowed, location) {
|
|
58
|
-
for (const key of Object.keys(record)) {
|
|
59
|
-
if (!allowed.has(key)) {
|
|
60
|
-
throw new ConfigValidationError(`unknown key '${key}' in ${location}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
/** True when the value is an array whose every element is a string. */
|
|
65
|
-
function isStringArray(value) {
|
|
66
|
-
return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
|
|
67
|
-
}
|
|
68
32
|
/** True when the glob value is a present, non-empty string or a non-empty array of non-empty strings. */
|
|
69
33
|
function isValidGlob(glob) {
|
|
70
34
|
if (typeof glob === 'string') {
|
|
71
|
-
return glob
|
|
72
|
-
}
|
|
73
|
-
if (Array.isArray(glob)) {
|
|
74
|
-
return glob.length > 0 && glob.every((entry) => typeof entry === 'string' && entry.length > 0);
|
|
35
|
+
return isNonEmptyString(glob);
|
|
75
36
|
}
|
|
76
|
-
return
|
|
37
|
+
return Array.isArray(glob) && glob.length > 0 && glob.every(isNonEmptyString);
|
|
77
38
|
}
|
|
78
39
|
/**
|
|
79
40
|
* Compile a `{scope}` template into the callable consumers use.
|
|
@@ -87,45 +48,11 @@ function compileTestCmd(template) {
|
|
|
87
48
|
// via GetSubstitution, breaking literal insertion for scopes containing `$`.
|
|
88
49
|
return (scope) => template.replaceAll('{scope}', () => scope);
|
|
89
50
|
}
|
|
90
|
-
/** Throw unless the pattern string compiles with `new RegExp` — compilability only, never run. */
|
|
91
|
-
function rejectUncompilableRegex(pattern, location) {
|
|
92
|
-
try {
|
|
93
|
-
new RegExp(pattern);
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
throw new ConfigValidationError(`${location} must be a compilable regular expression`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Validate a context-family `requirePrecedent` value.
|
|
101
|
-
*
|
|
102
|
-
* Evidence vocabulary is layered: the container (a flat object holding exactly one
|
|
103
|
-
* evidence key) is the core's, and so is the `command` key — a shell command is the
|
|
104
|
-
* agent-crossing surface, fully validated here. Every other key belongs to an adapter,
|
|
105
|
-
* whose own validator judges the value; the core passes it through verbatim and never
|
|
106
|
-
* inspects it. An unrecognized evidence key fails closed at assembly time, not here.
|
|
107
|
-
*/
|
|
108
|
-
function validateRequirePrecedent(evidence, location) {
|
|
109
|
-
if (!isPlainObject(evidence)) {
|
|
110
|
-
throw new ConfigValidationError(`${location} requirePrecedent must be an object`);
|
|
111
|
-
}
|
|
112
|
-
const keys = Object.keys(evidence);
|
|
113
|
-
if (keys.length !== 1) {
|
|
114
|
-
throw new ConfigValidationError(`${location} requirePrecedent must have exactly one evidence key`);
|
|
115
|
-
}
|
|
116
|
-
if (keys[0] === 'command') {
|
|
117
|
-
const command = evidence.command;
|
|
118
|
-
if (typeof command !== 'string' || command.length === 0) {
|
|
119
|
-
throw new ConfigValidationError(`${location} requirePrecedent.command must be a non-empty string pattern`);
|
|
120
|
-
}
|
|
121
|
-
rejectUncompilableRegex(command, `${location} requirePrecedent.command`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
51
|
/** Validate a draft entry and return it as data. */
|
|
125
52
|
function validateDraft(entry, id, location) {
|
|
126
53
|
for (const key of Object.keys(entry)) {
|
|
127
54
|
if (!DRAFT_KEYS.has(key)) {
|
|
128
|
-
// Named as the draft rule, not as an unknown key: `
|
|
55
|
+
// Named as the draft rule, not as an unknown key: `declare` et al. are legal
|
|
129
56
|
// discipline keys, just not on a draft.
|
|
130
57
|
throw new ConfigValidationError(`${location} allows only id, why, draft on a draft entry (found '${key}')`);
|
|
131
58
|
}
|
|
@@ -138,8 +65,8 @@ function validateDraft(entry, id, location) {
|
|
|
138
65
|
}
|
|
139
66
|
return { id, why: entry.why, draft: true };
|
|
140
67
|
}
|
|
141
|
-
/** Validate the
|
|
142
|
-
function
|
|
68
|
+
/** Validate the head of a judged entry — the closed key set, `why`, and `enforce`. */
|
|
69
|
+
function validateEntryHead(entry, location) {
|
|
143
70
|
rejectUnknownKeys(entry, DISCIPLINE_KEYS, location);
|
|
144
71
|
if (entry.why !== undefined && typeof entry.why !== 'string') {
|
|
145
72
|
throw new ConfigValidationError(`${location} why must be a string`);
|
|
@@ -148,90 +75,25 @@ function validateJudgedHead(entry, location) {
|
|
|
148
75
|
(typeof entry.enforce !== 'string' || !ENFORCE_LEVELS.has(entry.enforce))) {
|
|
149
76
|
throw new ConfigValidationError(`${location} enforce must be 'block' or 'advise'`);
|
|
150
77
|
}
|
|
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
78
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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`);
|
|
79
|
+
/**
|
|
80
|
+
* Validate a judged entry's declaration by delegating the block to the algebra validator.
|
|
81
|
+
*
|
|
82
|
+
* The entry's `id` supplies the declaration's name, so the block carrying its own
|
|
83
|
+
* `discipline` is refused rather than silently overwritten. The delegated messages arrive
|
|
84
|
+
* with the entry's location in front of them, which is what places a failure among many
|
|
85
|
+
* entries.
|
|
86
|
+
*/
|
|
87
|
+
function validateDeclareEntry(entry, location) {
|
|
88
|
+
const block = entry.declare;
|
|
89
|
+
if (!isPlainObject(block)) {
|
|
90
|
+
throw new ConfigValidationError(`${location} declare must be an object`);
|
|
211
91
|
}
|
|
212
|
-
|
|
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`);
|
|
92
|
+
if ('discipline' in block) {
|
|
93
|
+
throw new ConfigValidationError(`${location} declare must not carry discipline — the entry id is the name`);
|
|
225
94
|
}
|
|
226
|
-
|
|
95
|
+
validateAlgebraDeclaration({ discipline: entry.id, ...block }, `${location} declare`);
|
|
227
96
|
}
|
|
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
|
-
};
|
|
235
97
|
/**
|
|
236
98
|
* Validate the `disciplines` array and split judged entries from drafts. Throws
|
|
237
99
|
* {@link ConfigValidationError} naming the offending entry/key; the validated data passes
|
|
@@ -268,8 +130,8 @@ function validateDisciplines(disciplines) {
|
|
|
268
130
|
drafts.push(validateDraft(entry, entry.id, location));
|
|
269
131
|
return;
|
|
270
132
|
}
|
|
271
|
-
|
|
272
|
-
|
|
133
|
+
validateEntryHead(entry, location);
|
|
134
|
+
validateDeclareEntry(entry, location);
|
|
273
135
|
judged.push(entry);
|
|
274
136
|
});
|
|
275
137
|
return { judged, drafts };
|
package/dist/exit-codes.d.ts
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* The three codes are distinct and ordered by severity. The covenant *body* only ever
|
|
5
5
|
* emits `0` (uphold) or `1` (break, non-blocking); translating a break into the blocking
|
|
6
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
|
|
7
|
+
* `2` is the fail-closed parse path in `protocol.ts`.
|
|
8
8
|
*
|
|
9
|
-
* These live in their own module
|
|
10
|
-
* them: importing them from the barrel, which re-exports
|
|
9
|
+
* These live in their own leaf module because `fail-policy.ts` and `protocol.ts` both need
|
|
10
|
+
* them: importing them from the barrel, which re-exports both, is an initialization
|
|
11
11
|
* cycle — the constants read as `undefined` depending on which module the runtime evaluates
|
|
12
12
|
* first. The barrel re-exports them, so every consumer outside core still reaches them at
|
|
13
13
|
* the same path.
|
package/dist/exit-codes.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* The three codes are distinct and ordered by severity. The covenant *body* only ever
|
|
5
5
|
* emits `0` (uphold) or `1` (break, non-blocking); translating a break into the blocking
|
|
6
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
|
|
7
|
+
* `2` is the fail-closed parse path in `protocol.ts`.
|
|
8
8
|
*
|
|
9
|
-
* These live in their own module
|
|
10
|
-
* them: importing them from the barrel, which re-exports
|
|
9
|
+
* These live in their own leaf module because `fail-policy.ts` and `protocol.ts` both need
|
|
10
|
+
* them: importing them from the barrel, which re-exports both, is an initialization
|
|
11
11
|
* cycle — the constants read as `undefined` depending on which module the runtime evaluates
|
|
12
12
|
* first. The barrel re-exports them, so every consumer outside core still reaches them at
|
|
13
13
|
* the same path.
|