@polydeukes/core 0.5.0 → 0.6.1

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,438 @@
1
+ /**
2
+ * `algebra.ts` — the shape of one algebra declaration: five blocks, a closed relation
3
+ * position, a closed binary combinator position, and an open unary extraction vocabulary.
4
+ *
5
+ * The module knows no judgment. It runs no extraction and evaluates no relation; the
6
+ * kernel expansion laws quoted on each relation branch below are comments, never code.
7
+ */
8
+ import { validateMechanism } from './catalogue.js';
9
+ import { isPlainObject } from './is-plain-object.js';
10
+ import { FIXED_SOURCE_NAMES } from './source-names.js';
11
+ import { ConfigValidationError, isNonEmptyString, isStringArray, rejectUncompilableRegex, rejectUnknownKeys, } from './validation.js';
12
+ /** The relation position, closed. This tuple is the single source of the list. */
13
+ export const RELATION_NAMES = [
14
+ 'empty',
15
+ 'nonEmpty',
16
+ 'equal',
17
+ 'subset',
18
+ 'implies',
19
+ 'ordered',
20
+ 'unchanged',
21
+ ];
22
+ /** The binary world-combining position, closed. Anything else is a unary step. */
23
+ export const BINARY_COMBINATOR_NAMES = ['union', 'onlyIn', 'intersect'];
24
+ /**
25
+ * What a missing source does: refuse the declaration, let it pass unjudged, or read the
26
+ * absence as an empty item list and judge on.
27
+ */
28
+ export const SUPPLY_POLICIES = ['error', 'pass', 'empty'];
29
+ /**
30
+ * The paired source name. `empty` is a property of a single source: `state` holds a
31
+ * before/after pair that only `unchanged` reads, and an absent pair is not a pair of empties.
32
+ */
33
+ const PAIRED_SOURCE_NAME = 'state';
34
+ /** The kind position of a `sources` entry, closed. Each entry carries exactly one of them. */
35
+ export const SOURCE_KINDS = ['file', 'sidecar', 'transcript'];
36
+ const DECLARATION_KEYS = new Set([
37
+ 'discipline',
38
+ 'mechanism',
39
+ 'scope',
40
+ 'sources',
41
+ 'supply',
42
+ 'extract',
43
+ 'relate',
44
+ 'witness',
45
+ ]);
46
+ const SCOPE_KEYS = new Set([
47
+ 'source',
48
+ 'include',
49
+ 'exclude',
50
+ 'excludeIgnoreCase',
51
+ ]);
52
+ const RELATE_ENTRY_KEYS = new Set([
53
+ 'id',
54
+ 'relation',
55
+ 'message',
56
+ 'messageBySide',
57
+ ]);
58
+ const MESSAGE_BY_SIDE_KEYS = new Set(['left', 'right']);
59
+ const WITNESS_KEYS = new Set(['extract', 'relate']);
60
+ /** The argument keys each relation branch admits, `op` included. */
61
+ const RELATION_KEYS = {
62
+ empty: new Set(['op', 'of']),
63
+ nonEmpty: new Set(['op', 'of']),
64
+ equal: new Set(['op', 'of']),
65
+ subset: new Set(['op', 'of', 'in']),
66
+ implies: new Set(['op', 'of', 'requires']),
67
+ ordered: new Set(['op', 'of', 'strict']),
68
+ unchanged: new Set(['op', 'of']),
69
+ };
70
+ const COMBINATOR_KEYS = {
71
+ union: new Set(['op', 'of']),
72
+ onlyIn: new Set(['op', 'of', 'notIn']),
73
+ intersect: new Set(['op', 'of']),
74
+ };
75
+ function isCombinatorName(op) {
76
+ return BINARY_COMBINATOR_NAMES.includes(op);
77
+ }
78
+ /** The names a rejection message lists so the author sees what is admitted. */
79
+ function quotedList(names) {
80
+ return names.map((name) => `'${name}'`).join(', ');
81
+ }
82
+ /**
83
+ * The source names a scope regex can be read over: the fixed names whose value is a string,
84
+ * plus this declaration's own `file` bindings. `changes` is a list and `state` a pair, and a
85
+ * channel is the surface's JSON text — a regex over any of them matches nothing, so a
86
+ * declaration scoped on one is refused here rather than answering zero worlds at runtime.
87
+ */
88
+ const STRING_VALUED_FIXED_SOURCES = ['target.path', 'pre', 'post', 'command'];
89
+ function validateScope(scope, sources, location) {
90
+ if (!isPlainObject(scope)) {
91
+ throw new ConfigValidationError(`${location} scope must be an object`);
92
+ }
93
+ rejectUnknownKeys(scope, SCOPE_KEYS, `${location} scope`);
94
+ if (!isNonEmptyString(scope.source)) {
95
+ throw new ConfigValidationError(`${location} scope.source must be a non-empty string`);
96
+ }
97
+ const binding = isPlainObject(sources) ? sources[scope.source] : undefined;
98
+ const isFileSource = isPlainObject(binding) && typeof binding.file === 'string';
99
+ if (!STRING_VALUED_FIXED_SOURCES.includes(scope.source) && !isFileSource) {
100
+ throw new ConfigValidationError(`${location} scope.source '${scope.source}' is not a string-valued source; scope admits ${quotedList(STRING_VALUED_FIXED_SOURCES)}, or a file source`);
101
+ }
102
+ for (const key of ['include', 'exclude']) {
103
+ const patterns = scope[key];
104
+ if (patterns === undefined)
105
+ continue;
106
+ if (!isStringArray(patterns)) {
107
+ throw new ConfigValidationError(`${location} scope.${key} must be an array of strings`);
108
+ }
109
+ for (const pattern of patterns) {
110
+ rejectUncompilableRegex(pattern, `${location} scope.${key}`);
111
+ }
112
+ }
113
+ if (scope.excludeIgnoreCase !== undefined && typeof scope.excludeIgnoreCase !== 'boolean') {
114
+ throw new ConfigValidationError(`${location} scope.excludeIgnoreCase must be a boolean`);
115
+ }
116
+ }
117
+ /**
118
+ * Validate one `sources` entry's path: a repo-relative string the supply layer can join
119
+ * onto the root. `..` is rejected as a whole SEGMENT, so a name that merely contains two
120
+ * dots (`a..b`) stays legal while `a/../b` cannot climb out of the repository.
121
+ */
122
+ function validateSourceFile(file, location) {
123
+ if (!isNonEmptyString(file)) {
124
+ throw new ConfigValidationError(`${location}.file must be a non-empty string`);
125
+ }
126
+ if (file.startsWith('/')) {
127
+ throw new ConfigValidationError(`${location}.file must be a repo-relative path, not '${file}'`);
128
+ }
129
+ if (file.split('/').includes('..')) {
130
+ throw new ConfigValidationError(`${location}.file carries a '..' segment: '${file}'`);
131
+ }
132
+ }
133
+ function validateSources(sources, location) {
134
+ if (!isPlainObject(sources)) {
135
+ throw new ConfigValidationError(`${location}.sources must be an object`);
136
+ }
137
+ for (const [name, entry] of Object.entries(sources)) {
138
+ if (name.length === 0) {
139
+ throw new ConfigValidationError(`${location}.sources carries an empty source name`);
140
+ }
141
+ if (FIXED_SOURCE_NAMES.includes(name)) {
142
+ throw new ConfigValidationError(`${location}.sources.${name} shadows the world's own source of that name — the fixed names are ${quotedList(FIXED_SOURCE_NAMES)}`);
143
+ }
144
+ const where = `${location}.sources.${name}`;
145
+ if (!isPlainObject(entry)) {
146
+ throw new ConfigValidationError(`${where} must be an object naming one kind`);
147
+ }
148
+ const kinds = Object.keys(entry).filter((key) => SOURCE_KINDS.includes(key));
149
+ if (kinds.length !== 1) {
150
+ throw new ConfigValidationError(`${where} must name exactly one kind, one of ${quotedList(SOURCE_KINDS)}`);
151
+ }
152
+ rejectUnknownKeys(entry, new Set(SOURCE_KINDS), where);
153
+ const kind = kinds[0];
154
+ if (kind === 'sidecar' || kind === 'transcript') {
155
+ // The marker carries no information beyond the kind, so anything but the literal
156
+ // `true` would be a value the supply layer has to interpret.
157
+ if (entry[kind] !== true) {
158
+ throw new ConfigValidationError(`${where}.${kind} must be the literal true`);
159
+ }
160
+ continue;
161
+ }
162
+ validateSourceFile(entry.file, where);
163
+ }
164
+ }
165
+ /**
166
+ * Validate the `supply` block: every key names a source this declaration can be missing.
167
+ *
168
+ * The universe of source names is the fixed seven plus whatever `sources` binds, and it is
169
+ * closed, so a key outside it is a name nothing supplies — a misspelling whose policy never
170
+ * applies while the real source falls to the default.
171
+ */
172
+ function validateSupply(supply, sources, location) {
173
+ if (!isPlainObject(supply)) {
174
+ throw new ConfigValidationError(`${location} supply must be an object`);
175
+ }
176
+ const declared = isPlainObject(sources) ? Object.keys(sources) : [];
177
+ for (const [source, policy] of Object.entries(supply)) {
178
+ if (!FIXED_SOURCE_NAMES.includes(source) && !declared.includes(source)) {
179
+ const bindings = declared.length === 0 ? 'and this declaration binds none' : `or ${quotedList(declared)}`;
180
+ throw new ConfigValidationError(`${location} supply names the source '${source}', which is neither one of ${quotedList(FIXED_SOURCE_NAMES)} ${bindings}`);
181
+ }
182
+ if (!SUPPLY_POLICIES.includes(policy)) {
183
+ throw new ConfigValidationError(`${location} supply.${source} is '${String(policy)}' — must be one of ${quotedList(SUPPLY_POLICIES)}`);
184
+ }
185
+ if (policy === 'empty' && source === PAIRED_SOURCE_NAME) {
186
+ throw new ConfigValidationError(`${location} supply.${source}: 'empty' does not apply to the paired source — it holds a before/after pair, and an absent pair is not two empty states`);
187
+ }
188
+ }
189
+ }
190
+ /**
191
+ * Validate one pipeline step and return the extract names it references.
192
+ *
193
+ * Combinator discrimination is name-first, shape-second: a step named by one of the three
194
+ * is read as that combinator whatever its arguments, so a reserved name can never leak into
195
+ * the open unary vocabulary. A name outside the three that references two extractions
196
+ * (an array `of`, or a `notIn`) is an unknown combinator, not a unary step.
197
+ */
198
+ function validateStep(step, location) {
199
+ if (!isPlainObject(step)) {
200
+ throw new ConfigValidationError(`${location} must be an object`);
201
+ }
202
+ if (!isNonEmptyString(step.op)) {
203
+ throw new ConfigValidationError(`${location} op must be a non-empty string`);
204
+ }
205
+ const op = step.op;
206
+ if (!isCombinatorName(op)) {
207
+ if (Array.isArray(step.of) || step.notIn !== undefined) {
208
+ throw new ConfigValidationError(`${location} op '${op}' references two extractions but is not one of ${quotedList(BINARY_COMBINATOR_NAMES)}`);
209
+ }
210
+ return [];
211
+ }
212
+ rejectUnknownKeys(step, COMBINATOR_KEYS[op], `${location} (${op})`);
213
+ if (op === 'onlyIn') {
214
+ if (!isNonEmptyString(step.of) || !isNonEmptyString(step.notIn)) {
215
+ throw new ConfigValidationError(`${location} '${op}' takes a string 'of' and a string 'notIn'`);
216
+ }
217
+ if (step.of === step.notIn) {
218
+ throw new ConfigValidationError(`${location} '${op}' names '${step.of}' on both sides — the result is always empty`);
219
+ }
220
+ return [step.of, step.notIn];
221
+ }
222
+ const pair = step.of;
223
+ if (!isStringArray(pair) || pair.length !== 2 || !pair.every(isNonEmptyString)) {
224
+ throw new ConfigValidationError(`${location} '${op}' takes 'of' as two extract names`);
225
+ }
226
+ const [left, right] = pair;
227
+ if (left === right) {
228
+ throw new ConfigValidationError(`${location} '${op}' names '${left}' on both sides — the result is that extraction itself`);
229
+ }
230
+ return [left, right];
231
+ }
232
+ /** Validate one extract block, returning each pipeline's outgoing references. */
233
+ function validateExtract(extract, location, label) {
234
+ if (!isPlainObject(extract)) {
235
+ throw new ConfigValidationError(`${location} ${label} must be an object`);
236
+ }
237
+ const references = new Map();
238
+ for (const [name, steps] of Object.entries(extract)) {
239
+ const where = `${location} ${label}.${name}`;
240
+ if (!Array.isArray(steps) || steps.length === 0) {
241
+ throw new ConfigValidationError(`${where} must be a non-empty array of steps`);
242
+ }
243
+ const outgoing = [];
244
+ steps.forEach((step, index) => {
245
+ const referenced = validateStep(step, `${where}[${index}]`);
246
+ if (referenced.length > 0 && index !== 0) {
247
+ throw new ConfigValidationError(`${where}[${index}] a combinator stands only as the first step of a pipeline`);
248
+ }
249
+ outgoing.push(...referenced);
250
+ });
251
+ references.set(name, outgoing);
252
+ }
253
+ return references;
254
+ }
255
+ /** Validate the relation and return the extract names it references. */
256
+ function validateRelation(relation, location) {
257
+ if (!isPlainObject(relation)) {
258
+ throw new ConfigValidationError(`${location} relation must be an object`);
259
+ }
260
+ const op = relation.op;
261
+ if (typeof op !== 'string' || !RELATION_NAMES.includes(op)) {
262
+ throw new ConfigValidationError(`${location} relation op is '${String(op)}' — must be one of ${quotedList(RELATION_NAMES)}`);
263
+ }
264
+ const name = op;
265
+ rejectUnknownKeys(relation, RELATION_KEYS[name], `${location} relation (${name})`);
266
+ if (name === 'equal') {
267
+ const pair = relation.of;
268
+ if (!isStringArray(pair) || pair.length !== 2 || !pair.every(isNonEmptyString)) {
269
+ throw new ConfigValidationError(`${location} relation equal takes 'of' as two extract names`);
270
+ }
271
+ const [left, right] = pair;
272
+ if (left === right) {
273
+ throw new ConfigValidationError(`${location} relation equal names '${left}' on both sides — it can never break`);
274
+ }
275
+ return { op: name, refs: [left, right] };
276
+ }
277
+ if (!isNonEmptyString(relation.of)) {
278
+ throw new ConfigValidationError(`${location} relation ${name} needs 'of' as an extract name`);
279
+ }
280
+ const refs = [relation.of];
281
+ if (name === 'subset') {
282
+ if (!isNonEmptyString(relation.in)) {
283
+ throw new ConfigValidationError(`${location} relation subset needs 'in' as an extract name`);
284
+ }
285
+ refs.push(relation.in);
286
+ }
287
+ if (name === 'implies') {
288
+ if (!isNonEmptyString(relation.requires)) {
289
+ throw new ConfigValidationError(`${location} relation implies needs 'requires' as an extract name`);
290
+ }
291
+ refs.push(relation.requires);
292
+ }
293
+ if (name === 'ordered' && relation.strict !== undefined && typeof relation.strict !== 'boolean') {
294
+ throw new ConfigValidationError(`${location} relation ordered strict must be a boolean`);
295
+ }
296
+ return { op: name, refs };
297
+ }
298
+ /** Validate one entry; `ids` accumulates across the body and the witness block. */
299
+ function validateRelateEntry(entry, location, known, ids) {
300
+ if (!isPlainObject(entry)) {
301
+ throw new ConfigValidationError(`${location} must be an object`);
302
+ }
303
+ rejectUnknownKeys(entry, RELATE_ENTRY_KEYS, location);
304
+ if (!isNonEmptyString(entry.id)) {
305
+ throw new ConfigValidationError(`${location} id must be a non-empty string`);
306
+ }
307
+ const id = entry.id;
308
+ if (ids.has(id)) {
309
+ throw new ConfigValidationError(`${location} duplicates the entry id '${id}'`);
310
+ }
311
+ ids.add(id);
312
+ const { op, refs } = validateRelation(entry.relation, `${location} '${id}'`);
313
+ for (const name of refs) {
314
+ if (!known.has(name)) {
315
+ throw new ConfigValidationError(`${location} '${id}' references '${name}', which no extract in scope defines`);
316
+ }
317
+ }
318
+ const hasMessage = entry.message !== undefined;
319
+ const hasBySide = entry.messageBySide !== undefined;
320
+ if (hasMessage === hasBySide) {
321
+ throw new ConfigValidationError(`${location} '${id}' needs exactly one of 'message' and 'messageBySide'`);
322
+ }
323
+ if (hasMessage && !isNonEmptyString(entry.message)) {
324
+ throw new ConfigValidationError(`${location} '${id}' message must be a non-empty string`);
325
+ }
326
+ if (hasBySide) {
327
+ if (op !== 'equal') {
328
+ throw new ConfigValidationError(`${location} '${id}' carries messageBySide on ${op} — only equal has two sides`);
329
+ }
330
+ const bySide = entry.messageBySide;
331
+ if (!isPlainObject(bySide)) {
332
+ throw new ConfigValidationError(`${location} '${id}' messageBySide must be an object`);
333
+ }
334
+ rejectUnknownKeys(bySide, MESSAGE_BY_SIDE_KEYS, `${location} '${id}' messageBySide`);
335
+ if (!isNonEmptyString(bySide.left) || !isNonEmptyString(bySide.right)) {
336
+ throw new ConfigValidationError(`${location} '${id}' messageBySide needs a non-empty 'left' and 'right'`);
337
+ }
338
+ }
339
+ }
340
+ function validateRelate(relate, location, known, ids) {
341
+ if (!Array.isArray(relate) || relate.length === 0) {
342
+ throw new ConfigValidationError(`${location} relate must be a non-empty array`);
343
+ }
344
+ relate.forEach((entry, index) => {
345
+ validateRelateEntry(entry, `${location} relate[${index}]`, known, ids);
346
+ });
347
+ }
348
+ /**
349
+ * Refuse a combinator reference that does not resolve, and any cycle it takes part in.
350
+ *
351
+ * A self-edge is the shortest cycle, so a plain existence check passes it; only the
352
+ * reachability walk below finds either that or a two-pipeline loop.
353
+ */
354
+ function checkExtractGraph(references, known, location) {
355
+ for (const [name, outgoing] of references) {
356
+ for (const target of outgoing) {
357
+ if (!known.has(target)) {
358
+ throw new ConfigValidationError(`${location} '${name}' references '${target}', which no extract defines`);
359
+ }
360
+ }
361
+ }
362
+ const visiting = new Set();
363
+ const settled = new Set();
364
+ const walk = (name) => {
365
+ if (settled.has(name))
366
+ return;
367
+ if (visiting.has(name)) {
368
+ throw new ConfigValidationError(`${location} '${name}' takes part in a reference cycle`);
369
+ }
370
+ visiting.add(name);
371
+ for (const target of references.get(name) ?? []) {
372
+ walk(target);
373
+ }
374
+ visiting.delete(name);
375
+ settled.add(name);
376
+ };
377
+ for (const name of references.keys()) {
378
+ walk(name);
379
+ }
380
+ }
381
+ /**
382
+ * Validate one algebra declaration's shape, returning it unchanged.
383
+ *
384
+ * Every violation throws {@link ConfigValidationError} with a message starting at
385
+ * `location`, so a caller validating many declarations sees which one failed.
386
+ */
387
+ export function validateAlgebraDeclaration(input, location = 'declaration') {
388
+ if (!isPlainObject(input)) {
389
+ throw new ConfigValidationError(`${location} must be an object`);
390
+ }
391
+ rejectUnknownKeys(input, DECLARATION_KEYS, location);
392
+ if (!isNonEmptyString(input.discipline)) {
393
+ throw new ConfigValidationError(`${location} discipline must be a non-empty string`);
394
+ }
395
+ if (!isNonEmptyString(input.mechanism)) {
396
+ throw new ConfigValidationError(`${location} mechanism must be a non-empty string`);
397
+ }
398
+ if (input.sources !== undefined)
399
+ validateSources(input.sources, location);
400
+ if (input.scope !== undefined)
401
+ validateScope(input.scope, input.sources, location);
402
+ if (input.supply !== undefined)
403
+ validateSupply(input.supply, input.sources, location);
404
+ if (input.extract === undefined) {
405
+ throw new ConfigValidationError(`${location} needs an extract block`);
406
+ }
407
+ const bodyReferences = validateExtract(input.extract, location, 'extract');
408
+ const bodyNames = new Set(bodyReferences.keys());
409
+ checkExtractGraph(bodyReferences, bodyNames, `${location} extract`);
410
+ const ids = new Set();
411
+ if (input.relate === undefined) {
412
+ throw new ConfigValidationError(`${location} needs a relate array`);
413
+ }
414
+ validateRelate(input.relate, location, bodyNames, ids);
415
+ if (input.witness !== undefined) {
416
+ const witness = input.witness;
417
+ if (!isPlainObject(witness)) {
418
+ throw new ConfigValidationError(`${location} witness must be an object`);
419
+ }
420
+ rejectUnknownKeys(witness, WITNESS_KEYS, `${location} witness`);
421
+ // The witness sees the body's extract names as well as its own; the body never sees
422
+ // the witness's.
423
+ const witnessReferences = witness.extract === undefined
424
+ ? new Map()
425
+ : validateExtract(witness.extract, location, 'witness.extract');
426
+ for (const name of witnessReferences.keys()) {
427
+ if (bodyNames.has(name)) {
428
+ throw new ConfigValidationError(`${location} witness.extract '${name}' shadows the body extract of the same name — the two blocks share one namespace`);
429
+ }
430
+ }
431
+ const witnessNames = new Set([...bodyNames, ...witnessReferences.keys()]);
432
+ checkExtractGraph(witnessReferences, witnessNames, `${location} witness.extract`);
433
+ validateRelate(witness.relate, `${location} witness`, witnessNames, ids);
434
+ }
435
+ const declaration = input;
436
+ validateMechanism(declaration, location);
437
+ return declaration;
438
+ }
@@ -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;