polydeukes 0.6.0 → 0.7.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.
- package/README.ko.md +30 -70
- package/README.md +31 -84
- package/dist/baseline.d.ts +37 -1
- package/dist/baseline.js +68 -1
- package/dist/bin.d.ts +3 -4
- package/dist/bin.js +79 -93
- package/dist/covenant/bash-line.d.ts +130 -0
- package/dist/covenant/bash-line.js +566 -0
- package/dist/covenant/declaration-engine.d.ts +108 -0
- package/dist/covenant/declaration-engine.js +415 -0
- package/dist/covenant/discipline.d.ts +103 -0
- package/dist/covenant/discipline.js +667 -0
- package/dist/covenant/dispatch.d.ts +153 -0
- package/dist/covenant/dispatch.js +237 -0
- package/dist/covenant/extract-steps.d.ts +122 -0
- package/dist/covenant/extract-steps.js +0 -0
- package/dist/covenant/mention.d.ts +117 -0
- package/dist/covenant/mention.js +209 -0
- package/dist/covenant/module.d.ts +25 -0
- package/dist/covenant/module.js +23 -0
- package/dist/covenant/mutation-rules.d.ts +40 -0
- package/dist/covenant/mutation-rules.js +146 -0
- package/dist/covenant/relations.d.ts +46 -0
- package/dist/covenant/relations.js +68 -0
- package/dist/covenant/run-covenant.d.ts +95 -0
- package/dist/covenant/run-covenant.js +164 -0
- package/dist/covenant/self-mod.d.ts +55 -0
- package/dist/covenant/self-mod.js +84 -0
- package/dist/covenant/shell-evidence.d.ts +38 -0
- package/dist/covenant/shell-evidence.js +247 -0
- package/dist/covenant/shell-mod.d.ts +84 -0
- package/dist/covenant/shell-mod.js +213 -0
- package/dist/covenant/supply.d.ts +54 -0
- package/dist/covenant/supply.js +50 -0
- package/dist/covenant/transcript-mod.d.ts +60 -0
- package/dist/covenant/transcript-mod.js +254 -0
- package/dist/covenant/ttl-witness.d.ts +47 -0
- package/dist/covenant/ttl-witness.js +80 -0
- package/dist/covenant-check.d.ts +72 -39
- package/dist/covenant-check.js +260 -121
- package/dist/diff-ir.d.ts +28 -0
- package/dist/diff-ir.js +310 -0
- package/dist/docs/README.ko.md +60 -0
- package/dist/docs/README.md +64 -0
- package/dist/docs/catalog.json +480 -0
- package/dist/docs/concepts/judgment.ko.md +113 -0
- package/dist/docs/concepts/judgment.md +113 -0
- package/dist/docs/how-to/configure-project.ko.md +99 -0
- package/dist/docs/how-to/configure-project.md +98 -0
- package/dist/docs/how-to/connect-surfaces.ko.md +120 -0
- package/dist/docs/how-to/connect-surfaces.md +121 -0
- package/dist/docs/how-to/write-disciplines.ko.md +179 -0
- package/dist/docs/how-to/write-disciplines.md +181 -0
- package/dist/docs/index.json +2280 -0
- package/dist/docs/reference/cli/covenant-check.ko.md +148 -0
- package/dist/docs/reference/cli/covenant-check.md +153 -0
- package/dist/docs/reference/cli/docs.ko.md +97 -0
- package/dist/docs/reference/cli/docs.md +95 -0
- package/dist/docs/reference/cli/explain.ko.md +84 -0
- package/dist/docs/reference/cli/explain.md +89 -0
- package/dist/docs/reference/cli/init.ko.md +119 -0
- package/dist/docs/reference/cli/init.md +119 -0
- package/dist/docs/reference/configuration/index.ko.md +553 -0
- package/dist/docs/reference/{configuration.md → configuration/index.md} +215 -90
- package/dist/docs/reference/packages/adapter-claude-code.ko.md +110 -0
- package/dist/docs/reference/{adapter-claude-code.md → packages/adapter-claude-code.md} +47 -19
- package/dist/docs/reference/packages/adapter-grok.ko.md +77 -0
- package/dist/docs/reference/packages/adapter-grok.md +79 -0
- package/dist/docs/reference/packages/core.ko.md +174 -0
- package/dist/docs/reference/{core.md → packages/core.md} +74 -12
- package/dist/docs/reference/packages/polydeukes.ko.md +195 -0
- package/dist/docs/reference/packages/polydeukes.md +204 -0
- package/dist/docs/reference/packages/sdk-ts.ko.md +161 -0
- package/dist/docs/reference/packages/sdk-ts.md +164 -0
- package/dist/docs/troubleshooting.ko.md +144 -0
- package/dist/docs/troubleshooting.md +103 -151
- package/dist/docs/tutorials/first-judgment.ko.md +82 -0
- package/dist/docs/tutorials/first-judgment.md +81 -0
- package/dist/docs-catalog.d.ts +25 -0
- package/dist/docs-catalog.js +450 -0
- package/dist/docs-library.d.ts +23 -0
- package/dist/docs-library.js +347 -0
- package/dist/docs-markdown.d.ts +32 -0
- package/dist/docs-markdown.js +150 -0
- package/dist/docs-query.d.ts +11 -40
- package/dist/docs-query.js +28 -122
- package/dist/docs-types.d.ts +105 -0
- package/dist/docs-types.js +2 -0
- package/dist/explain.d.ts +1 -1
- package/dist/explain.js +37 -22
- package/dist/load-config.d.ts +1 -1
- package/dist/load-config.js +1 -1
- package/dist/pre-state-reader.d.ts +1 -1
- package/dist/pre-state-reader.js +1 -1
- package/dist/scaffold-project.d.ts +2 -2
- package/dist/scaffold-project.js +9 -3
- package/dist/schema/polydeukes.schema.json +12 -0
- package/dist/worktree-reader.d.ts +19 -0
- package/dist/worktree-reader.js +30 -0
- package/package.json +3 -17
- package/dist/claude-code-hook.d.ts +0 -79
- package/dist/claude-code-hook.js +0 -372
- package/dist/claude-code.d.ts +0 -6
- package/dist/claude-code.js +0 -6
- package/dist/covenant-module.d.ts +0 -25
- package/dist/covenant-module.js +0 -42
- package/dist/docs/configuration.md +0 -103
- package/dist/docs/installation.md +0 -241
- package/dist/docs/reference/adapter-git.md +0 -100
- package/dist/docs/reference/covenant.md +0 -116
- package/dist/docs/reference/polydeukes.md +0 -315
- package/dist/index.d.ts +0 -22
- package/dist/index.js +0 -21
- package/dist/init-claude-code.d.ts +0 -52
- package/dist/init-claude-code.js +0 -469
- package/dist/init-grok.d.ts +0 -51
- package/dist/init-grok.js +0 -242
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The declaration engine — `judge = relate ∘ extract` as a pure function of one `World`.
|
|
3
|
+
*
|
|
4
|
+
* `compileDeclaration` resolves a shape-validated declaration against the extract registry
|
|
5
|
+
* and answers either something judgeable or a config fault naming where the declaration is
|
|
6
|
+
* wrong; it never throws. `judgeDeclaration` then runs the extraction pipelines over a
|
|
7
|
+
* world value and evaluates the relations, answering a verdict. `witnessOpens` evaluates
|
|
8
|
+
* the witness block on its own, so a caller can let a satisfied condition stand after the
|
|
9
|
+
* verdict without re-reading it.
|
|
10
|
+
*
|
|
11
|
+
* The engine knows nothing but its two arguments: no files, no tools, no session. Whatever
|
|
12
|
+
* a host can express as named values in a `World` it can judge here.
|
|
13
|
+
*/
|
|
14
|
+
import { BINARY_COMBINATOR_NAMES } from '@polydeukes/core';
|
|
15
|
+
import { EXTRACT_STEPS, fault, SupplyFailure, UNARY_STEP_NAMES, } from './extract-steps.js';
|
|
16
|
+
import { relateEmpty, relateEqual, relateImplies, relateNonEmpty, relateOrdered, relateSubset, relateUnchanged, } from './relations.js';
|
|
17
|
+
export { EXTRACT_STEPS, UNARY_STEP_NAMES } from './extract-steps.js';
|
|
18
|
+
/** The world name a paired source reads: its value is `{ pre, post }`, one state each. */
|
|
19
|
+
const PAIRED_SOURCE = 'state';
|
|
20
|
+
function isFault(value) {
|
|
21
|
+
return (typeof value === 'object' &&
|
|
22
|
+
value !== null &&
|
|
23
|
+
value.kind === 'config-fault');
|
|
24
|
+
}
|
|
25
|
+
function isCombinator(step) {
|
|
26
|
+
return BINARY_COMBINATOR_NAMES.includes(step.op);
|
|
27
|
+
}
|
|
28
|
+
function combinatorReferences(step) {
|
|
29
|
+
return step.op === 'onlyIn' ? [step.of, step.notIn] : step.of;
|
|
30
|
+
}
|
|
31
|
+
/** Resolve one pipeline's steps against the registry, answering the fault of the first bad one. */
|
|
32
|
+
function compilePipeline(name, steps, location) {
|
|
33
|
+
const [first, ...rest] = steps;
|
|
34
|
+
if (first === undefined || (!isCombinator(first) && first.op !== 'source')) {
|
|
35
|
+
const found = first === undefined ? 'an empty pipeline' : `'${first.op}'`;
|
|
36
|
+
return fault(location, `a pipeline begins with 'source' or one of ${BINARY_COMBINATOR_NAMES.join(', ')} — found ${found}`);
|
|
37
|
+
}
|
|
38
|
+
const combinator = isCombinator(first) ? first : undefined;
|
|
39
|
+
const unarySteps = combinator === undefined ? steps : rest;
|
|
40
|
+
for (const step of unarySteps) {
|
|
41
|
+
const entry = EXTRACT_STEPS[step.op];
|
|
42
|
+
if (entry === undefined) {
|
|
43
|
+
return fault(location, `'${step.op}' is not a registered extract step — the registry carries ${UNARY_STEP_NAMES.join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
const bad = entry.validate(step, location);
|
|
46
|
+
if (bad !== undefined)
|
|
47
|
+
return bad;
|
|
48
|
+
}
|
|
49
|
+
const paired = combinator === undefined && first.op === 'source' && first.of === PAIRED_SOURCE;
|
|
50
|
+
return {
|
|
51
|
+
name,
|
|
52
|
+
paired,
|
|
53
|
+
references: combinator === undefined ? [] : combinatorReferences(combinator),
|
|
54
|
+
combinator,
|
|
55
|
+
steps: unarySteps,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function compileExtract(extract, known, location) {
|
|
59
|
+
const added = [];
|
|
60
|
+
for (const [name, steps] of Object.entries(extract)) {
|
|
61
|
+
const compiled = compilePipeline(name, steps, `${location} ${name}`);
|
|
62
|
+
if (isFault(compiled))
|
|
63
|
+
return compiled;
|
|
64
|
+
known.set(name, compiled);
|
|
65
|
+
added.push(compiled);
|
|
66
|
+
}
|
|
67
|
+
for (const pipeline of added) {
|
|
68
|
+
for (const reference of pipeline.references) {
|
|
69
|
+
if (known.get(reference)?.paired === true) {
|
|
70
|
+
return fault(`${location} ${pipeline.name}`, `'${pipeline.name}' combines '${reference}', which reads a before/after pair — a combinator takes single extractions`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
/** The extract names a relation reads, and whether it takes a paired extraction. */
|
|
77
|
+
function relationReferences(relation) {
|
|
78
|
+
if (relation.op === 'equal')
|
|
79
|
+
return relation.of;
|
|
80
|
+
if (relation.op === 'subset')
|
|
81
|
+
return [relation.of, relation.in];
|
|
82
|
+
if (relation.op === 'implies')
|
|
83
|
+
return [relation.of, relation.requires];
|
|
84
|
+
return [relation.of];
|
|
85
|
+
}
|
|
86
|
+
function checkRelateShapes(relate, pipelines, location) {
|
|
87
|
+
for (const entry of relate) {
|
|
88
|
+
const wantsPair = entry.relation.op === 'unchanged';
|
|
89
|
+
for (const name of relationReferences(entry.relation)) {
|
|
90
|
+
const pipeline = pipelines.get(name);
|
|
91
|
+
if (pipeline === undefined) {
|
|
92
|
+
return fault(`${location} '${entry.id}'`, `'${entry.relation.op}' references '${name}', which no extract in scope defines`);
|
|
93
|
+
}
|
|
94
|
+
const paired = pipeline.paired;
|
|
95
|
+
if (paired === wantsPair)
|
|
96
|
+
continue;
|
|
97
|
+
return fault(`${location} '${entry.id}'`, wantsPair
|
|
98
|
+
? `'unchanged' compares a before/after pair, and '${name}' reads a single state`
|
|
99
|
+
: `'${entry.relation.op}' takes a single extraction, and '${name}' reads a before/after pair`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
function compileRegexList(patterns, flags, location) {
|
|
105
|
+
const compiled = [];
|
|
106
|
+
for (const pattern of patterns ?? []) {
|
|
107
|
+
try {
|
|
108
|
+
compiled.push(new RegExp(pattern, flags));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return fault(location, `cannot compile the expression '${pattern}'`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return compiled;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a shape-validated declaration into something judgeable, or answer the first thing
|
|
118
|
+
* wrong with it: a step outside the registry, an argument outside a step's closed keys, an
|
|
119
|
+
* uncompilable expression, or a paired extraction where a single one belongs.
|
|
120
|
+
*/
|
|
121
|
+
export function compileDeclaration(spec) {
|
|
122
|
+
const decl = spec.declaration;
|
|
123
|
+
const pipelines = new Map();
|
|
124
|
+
const extractFault = compileExtract(decl.extract, pipelines, `${decl.discipline} extract`);
|
|
125
|
+
if (extractFault !== undefined)
|
|
126
|
+
return extractFault;
|
|
127
|
+
const relateFault = checkRelateShapes(decl.relate, pipelines, `${decl.discipline} relate`);
|
|
128
|
+
if (relateFault !== undefined)
|
|
129
|
+
return relateFault;
|
|
130
|
+
let scope;
|
|
131
|
+
if (decl.scope !== undefined) {
|
|
132
|
+
const where = `${decl.discipline} scope`;
|
|
133
|
+
const include = compileRegexList(decl.scope.include, '', where);
|
|
134
|
+
if (isFault(include))
|
|
135
|
+
return include;
|
|
136
|
+
const exclude = compileRegexList(decl.scope.exclude, decl.scope.excludeIgnoreCase === true ? 'i' : '', where);
|
|
137
|
+
if (isFault(exclude))
|
|
138
|
+
return exclude;
|
|
139
|
+
scope = { source: decl.scope.source, include, exclude };
|
|
140
|
+
}
|
|
141
|
+
const witness = compileWitness(decl.witness, pipelines, decl.discipline);
|
|
142
|
+
if (isFault(witness))
|
|
143
|
+
return witness;
|
|
144
|
+
return { pipelines, relate: decl.relate, supply: decl.supply ?? {}, scope, witness };
|
|
145
|
+
}
|
|
146
|
+
function compileWitness(block, bodyPipelines, discipline) {
|
|
147
|
+
if (block === undefined)
|
|
148
|
+
return undefined;
|
|
149
|
+
const pipelines = new Map(bodyPipelines);
|
|
150
|
+
if (block.extract !== undefined) {
|
|
151
|
+
const bad = compileExtract(block.extract, pipelines, `${discipline} witness.extract`);
|
|
152
|
+
if (bad !== undefined)
|
|
153
|
+
return bad;
|
|
154
|
+
}
|
|
155
|
+
const relateFault = checkRelateShapes(block.relate, pipelines, `${discipline} witness relate`);
|
|
156
|
+
if (relateFault !== undefined)
|
|
157
|
+
return relateFault;
|
|
158
|
+
return { pipelines, relate: block.relate };
|
|
159
|
+
}
|
|
160
|
+
class Extractor {
|
|
161
|
+
pipelines;
|
|
162
|
+
supply;
|
|
163
|
+
world;
|
|
164
|
+
cache = new Map();
|
|
165
|
+
problem;
|
|
166
|
+
constructor(pipelines, supply, world) {
|
|
167
|
+
this.pipelines = pipelines;
|
|
168
|
+
this.supply = supply;
|
|
169
|
+
this.world = world;
|
|
170
|
+
}
|
|
171
|
+
/** The pipeline's items, or `undefined` once a supply problem has ended the judgment. */
|
|
172
|
+
resolve(name) {
|
|
173
|
+
if (this.problem !== undefined)
|
|
174
|
+
return undefined;
|
|
175
|
+
const cached = this.cache.get(name);
|
|
176
|
+
if (cached !== undefined)
|
|
177
|
+
return cached;
|
|
178
|
+
const pipeline = this.pipelines.get(name);
|
|
179
|
+
if (pipeline === undefined)
|
|
180
|
+
return undefined;
|
|
181
|
+
const result = pipeline.paired ? this.runPaired(pipeline) : this.runSingle(pipeline);
|
|
182
|
+
if (result === undefined)
|
|
183
|
+
return undefined;
|
|
184
|
+
this.cache.set(name, result);
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
runPaired(pipeline) {
|
|
188
|
+
if (!(PAIRED_SOURCE in this.world)) {
|
|
189
|
+
this.problem = this.absent(PAIRED_SOURCE);
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
const state = this.world[PAIRED_SOURCE];
|
|
193
|
+
if (typeof state !== 'object' ||
|
|
194
|
+
state === null ||
|
|
195
|
+
Array.isArray(state) ||
|
|
196
|
+
!('pre' in state) ||
|
|
197
|
+
!('post' in state)) {
|
|
198
|
+
this.problem = {
|
|
199
|
+
kind: 'error',
|
|
200
|
+
source: PAIRED_SOURCE,
|
|
201
|
+
reason: `'${PAIRED_SOURCE}' must be a before/after pair — an object carrying 'pre' and 'post'`,
|
|
202
|
+
};
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
const sides = state;
|
|
206
|
+
const pre = this.runSteps(pipeline, sides.pre, PAIRED_SOURCE);
|
|
207
|
+
if (pre === undefined)
|
|
208
|
+
return undefined;
|
|
209
|
+
const post = this.runSteps(pipeline, sides.post, PAIRED_SOURCE);
|
|
210
|
+
if (post === undefined)
|
|
211
|
+
return undefined;
|
|
212
|
+
return { pair: { pre, post } };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* A pipeline reading one world value, or a combinator joining two other extractions —
|
|
216
|
+
* the two ways a pipeline can begin.
|
|
217
|
+
*/
|
|
218
|
+
runSingle(pipeline) {
|
|
219
|
+
const combinator = pipeline.combinator;
|
|
220
|
+
if (combinator !== undefined) {
|
|
221
|
+
const [leftName, rightName] = combinatorReferences(combinator);
|
|
222
|
+
const left = this.resolve(leftName)?.single;
|
|
223
|
+
const right = this.resolve(rightName)?.single;
|
|
224
|
+
if (left === undefined || right === undefined)
|
|
225
|
+
return undefined;
|
|
226
|
+
const items = this.runStepList(pipeline.steps, combine(combinator, left, right), pipeline.name);
|
|
227
|
+
return items === undefined ? undefined : { single: items };
|
|
228
|
+
}
|
|
229
|
+
const source = sourceName(pipeline);
|
|
230
|
+
if (!(source in this.world)) {
|
|
231
|
+
// `empty` is the one policy that continues the judgment: the absence becomes an empty
|
|
232
|
+
// item list, and what that means for the verdict is the pipeline's own arithmetic.
|
|
233
|
+
if (this.supply[source] === 'empty')
|
|
234
|
+
return { single: [] };
|
|
235
|
+
this.problem = this.absent(source);
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
const items = this.runSteps(pipeline, this.world[source], source);
|
|
239
|
+
return items === undefined ? undefined : { single: items };
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* What an absent source means under the declaration's supply policy: skip, or fail. The
|
|
243
|
+
* paired source reaches only this, never the empty reading — config validation refuses
|
|
244
|
+
* `empty` there, and an absent pair is not a pair of empty states either way.
|
|
245
|
+
*/
|
|
246
|
+
absent(source) {
|
|
247
|
+
return this.supply[source] === 'pass'
|
|
248
|
+
? { kind: 'pass', source }
|
|
249
|
+
: { kind: 'error', source, reason: `the world carries no source named '${source}'` };
|
|
250
|
+
}
|
|
251
|
+
/** The steps after the source; the paired path re-enters here once per state. */
|
|
252
|
+
runSteps(pipeline, value, source) {
|
|
253
|
+
return this.runStepList(pipeline.steps.slice(1), [{ key: '0', value }], source);
|
|
254
|
+
}
|
|
255
|
+
runStepList(steps, items, source) {
|
|
256
|
+
let current = items;
|
|
257
|
+
for (const step of steps) {
|
|
258
|
+
try {
|
|
259
|
+
current = EXTRACT_STEPS[step.op].run(current, step);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
if (!(error instanceof SupplyFailure))
|
|
263
|
+
throw error;
|
|
264
|
+
this.problem = { kind: 'error', source, reason: error.message };
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return current;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/** The world name a source-headed pipeline reads. */
|
|
272
|
+
function sourceName(pipeline) {
|
|
273
|
+
return String(pipeline.steps[0].of);
|
|
274
|
+
}
|
|
275
|
+
function combine(step, left, right) {
|
|
276
|
+
if (step.op === 'union')
|
|
277
|
+
return [...left, ...right];
|
|
278
|
+
const rightKeys = new Set(right.map((item) => item.key));
|
|
279
|
+
return step.op === 'onlyIn'
|
|
280
|
+
? left.filter((item) => !rightKeys.has(item.key))
|
|
281
|
+
: left.filter((item) => rightKeys.has(item.key));
|
|
282
|
+
}
|
|
283
|
+
/** The paired path runs the whole pipeline over one state; the single path skips the source. */
|
|
284
|
+
function evaluate(entry, extractor) {
|
|
285
|
+
const relation = entry.relation;
|
|
286
|
+
const items = (name) => extractor.resolve(name)?.single;
|
|
287
|
+
if (relation.op === 'unchanged') {
|
|
288
|
+
const pair = extractor.resolve(relation.of)?.pair;
|
|
289
|
+
return pair === undefined ? undefined : relateUnchanged(pair.pre, pair.post);
|
|
290
|
+
}
|
|
291
|
+
if (relation.op === 'equal') {
|
|
292
|
+
const left = items(relation.of[0]);
|
|
293
|
+
const right = items(relation.of[1]);
|
|
294
|
+
return left === undefined || right === undefined ? undefined : relateEqual(left, right);
|
|
295
|
+
}
|
|
296
|
+
if (relation.op === 'subset') {
|
|
297
|
+
const of = items(relation.of);
|
|
298
|
+
const inItems = items(relation.in);
|
|
299
|
+
return of === undefined || inItems === undefined ? undefined : relateSubset(of, inItems);
|
|
300
|
+
}
|
|
301
|
+
if (relation.op === 'implies') {
|
|
302
|
+
const of = items(relation.of);
|
|
303
|
+
const requires = items(relation.requires);
|
|
304
|
+
return of === undefined || requires === undefined ? undefined : relateImplies(of, requires);
|
|
305
|
+
}
|
|
306
|
+
const of = items(relation.of);
|
|
307
|
+
if (of === undefined)
|
|
308
|
+
return undefined;
|
|
309
|
+
if (relation.op === 'empty')
|
|
310
|
+
return relateEmpty(of);
|
|
311
|
+
if (relation.op === 'nonEmpty')
|
|
312
|
+
return relateNonEmpty(of, relation.of);
|
|
313
|
+
return relateOrdered(of, relation.strict === true);
|
|
314
|
+
}
|
|
315
|
+
/** The template of an entry, chosen by the side of the witness that will render it. */
|
|
316
|
+
function templateFor(entry, witness) {
|
|
317
|
+
if ('message' in entry)
|
|
318
|
+
return entry.message;
|
|
319
|
+
return witness.side === 'right' ? entry.messageBySide.right : entry.messageBySide.left;
|
|
320
|
+
}
|
|
321
|
+
function renderText(value) {
|
|
322
|
+
return typeof value === 'string' ? value : String(value);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* The break text: the first witness fills the template, and the witnesses beyond it are
|
|
326
|
+
* counted in a suffix rather than listed, so one break stays one line.
|
|
327
|
+
*/
|
|
328
|
+
function renderMessage(entry, witnesses) {
|
|
329
|
+
const first = witnesses[0];
|
|
330
|
+
const rendered = templateFor(entry, first)
|
|
331
|
+
.replaceAll('{key}', first.key)
|
|
332
|
+
.replaceAll('{value}', renderText(first.value))
|
|
333
|
+
.replaceAll('{before}', first.before === undefined ? '' : renderText(first.before));
|
|
334
|
+
return witnesses.length > 1 ? `${rendered} (+${witnesses.length - 1})` : rendered;
|
|
335
|
+
}
|
|
336
|
+
function stripBefore(witnesses) {
|
|
337
|
+
return witnesses.map(({ key, value, side }) => side === undefined ? { key, value } : { key, value, side });
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Whether the declaration's scope admits this world. An absent scope admits every world, and
|
|
341
|
+
* an absent `include` admits every path the `exclude` list does not name.
|
|
342
|
+
*/
|
|
343
|
+
function inScope(scope, world) {
|
|
344
|
+
if (scope === undefined)
|
|
345
|
+
return true;
|
|
346
|
+
const target = world[scope.source];
|
|
347
|
+
if (typeof target !== 'string')
|
|
348
|
+
return false;
|
|
349
|
+
const included = scope.include.length === 0 || scope.include.some((p) => p.test(target));
|
|
350
|
+
return included && !scope.exclude.some((pattern) => pattern.test(target));
|
|
351
|
+
}
|
|
352
|
+
function verdictFor(problem) {
|
|
353
|
+
return problem.kind === 'pass'
|
|
354
|
+
? { kind: 'not-applicable', reason: 'supply-pass', source: problem.source }
|
|
355
|
+
: { kind: 'supply-error', source: problem.source, reason: problem.reason };
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Whether the declaration's scope admits this world — the routing question, asked without
|
|
359
|
+
* judging. A surface routes on the same predicate the judgment starts with, so a world it
|
|
360
|
+
* sends to the body is never one the body answers `not-applicable` for.
|
|
361
|
+
*/
|
|
362
|
+
export function scopeAdmits(compiled, world) {
|
|
363
|
+
return inScope(compiled.scope, world);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Judge one compiled declaration against one world.
|
|
367
|
+
*
|
|
368
|
+
* The scope decides first, so a declaration that does not apply never reads a source it
|
|
369
|
+
* would have failed on. Then every relate entry is evaluated in declaration order, one
|
|
370
|
+
* break per entry that does not hold. The first supply failure ends the judgment: a
|
|
371
|
+
* partially read world produces no partial verdict.
|
|
372
|
+
*/
|
|
373
|
+
export function judgeDeclaration(spec) {
|
|
374
|
+
const { compiled, world } = spec;
|
|
375
|
+
if (!inScope(compiled.scope, world))
|
|
376
|
+
return { kind: 'not-applicable', reason: 'scope' };
|
|
377
|
+
const extractor = new Extractor(compiled.pipelines, compiled.supply, world);
|
|
378
|
+
const breaks = [];
|
|
379
|
+
for (const entry of compiled.relate) {
|
|
380
|
+
const witnesses = evaluate(entry, extractor);
|
|
381
|
+
if (extractor.problem !== undefined)
|
|
382
|
+
return verdictFor(extractor.problem);
|
|
383
|
+
if (witnesses === undefined || witnesses.length === 0)
|
|
384
|
+
continue;
|
|
385
|
+
breaks.push({
|
|
386
|
+
id: entry.id,
|
|
387
|
+
message: renderMessage(entry, witnesses),
|
|
388
|
+
witnesses: stripBefore(witnesses),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return breaks.length === 0 ? { kind: 'pass' } : { kind: 'broken', breaks };
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Whether the declaration's witness condition holds for this world — the valve that stands
|
|
395
|
+
* after the verdict. It is closed unless every witness entry holds: a declaration with no
|
|
396
|
+
* witness block, a world outside the scope, and a source the witness cannot read all leave
|
|
397
|
+
* it shut, so the valve opens only on a condition that was actually met.
|
|
398
|
+
*/
|
|
399
|
+
export function witnessOpens(spec) {
|
|
400
|
+
const { compiled, world } = spec;
|
|
401
|
+
const witness = compiled.witness;
|
|
402
|
+
if (witness === undefined)
|
|
403
|
+
return false;
|
|
404
|
+
if (!inScope(compiled.scope, world))
|
|
405
|
+
return false;
|
|
406
|
+
const extractor = new Extractor(witness.pipelines, compiled.supply, world);
|
|
407
|
+
for (const entry of witness.relate) {
|
|
408
|
+
const witnesses = evaluate(entry, extractor);
|
|
409
|
+
if (extractor.problem !== undefined || witnesses === undefined)
|
|
410
|
+
return false;
|
|
411
|
+
if (witnesses.length > 0)
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discipline registration compiler.
|
|
3
|
+
*
|
|
4
|
+
* `compileDisciplineRegistrations` turns validated entries into dispatcher registrations —
|
|
5
|
+
* one per entry, routed by the declaration's own scope (`matches`), judged by an in-process
|
|
6
|
+
* thunk that runs the compiled declaration over each world the input yields. Absolute paths
|
|
7
|
+
* are relativized against the repo root first, so a path outside the root never routes: a
|
|
8
|
+
* declaration's scope is written repo-relative.
|
|
9
|
+
*/
|
|
10
|
+
import { type CanonicalTranscript, type CovenantInput, type DisciplineEntry } from '@polydeukes/core';
|
|
11
|
+
import { type World } from './declaration-engine.ts';
|
|
12
|
+
import type { CovenantRegistration } from './dispatch.ts';
|
|
13
|
+
/**
|
|
14
|
+
* `CompileDisciplinesSpec` — validated entries plus the assembly values baked into each
|
|
15
|
+
* registration's judge thunk and matches closure.
|
|
16
|
+
*
|
|
17
|
+
* `transcript` is the session history a `transcript` binding reads. Absent means no session
|
|
18
|
+
* CHANNEL, not an empty one — the declaration's own `supply` policy disposes of it.
|
|
19
|
+
*
|
|
20
|
+
* `postIsWholeFile` says whether a change's `post` is the file the call leaves behind.
|
|
21
|
+
* Absent means true — a session call carries the whole text it is about to write, so a
|
|
22
|
+
* `file` binding naming the changed path reads that text. A diff-translated change carries
|
|
23
|
+
* only the hunk's added lines as `post`; on that surface the binding reads the tree instead,
|
|
24
|
+
* which holds the state the change set left.
|
|
25
|
+
*
|
|
26
|
+
* `observesPreState` says whether this surface has a pre-state channel at all. Absent means
|
|
27
|
+
* true — the reader answers per location, and its `undefined` is that one location failing.
|
|
28
|
+
* False is a surface that can never answer, so a shell write it derives yields no world
|
|
29
|
+
* rather than the unjudgeable exit: an absent channel is an environment fact, not a break.
|
|
30
|
+
* That write routes to the entry's skip arm, since an entry handed no world would otherwise
|
|
31
|
+
* answer pass for a file it never read.
|
|
32
|
+
*/
|
|
33
|
+
export type CompileDisciplinesSpec = {
|
|
34
|
+
disciplines: DisciplineEntry[];
|
|
35
|
+
rootDir: string;
|
|
36
|
+
shellTools: string[];
|
|
37
|
+
commandArgs: string[];
|
|
38
|
+
readPreState: (location: string) => string | null | undefined;
|
|
39
|
+
postIsWholeFile?: boolean;
|
|
40
|
+
observesPreState?: boolean;
|
|
41
|
+
witness?: CovenantRegistration['witness'];
|
|
42
|
+
transcript?: CanonicalTranscript;
|
|
43
|
+
};
|
|
44
|
+
/** One change as one world, under the repo-relative path the declaration's scope reads. */
|
|
45
|
+
export type SuppliedWorld = {
|
|
46
|
+
readonly path: string;
|
|
47
|
+
readonly world: World;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* `worldsFromInput` input — one observation, the root its paths are relativized against,
|
|
51
|
+
* and the shell surface that says which of the input's calls is a shell call.
|
|
52
|
+
*/
|
|
53
|
+
export type WorldsFromInputSpec = {
|
|
54
|
+
input: CovenantInput;
|
|
55
|
+
rootDir: string;
|
|
56
|
+
shellTools: string[];
|
|
57
|
+
commandArgs: string[];
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Turn each observation of the input into one world, in input order.
|
|
61
|
+
*
|
|
62
|
+
* The seven source names are fixed: `target.path`, `pre`, `post`, the paired `state`,
|
|
63
|
+
* `changes`, `command`, and `actor`. A side the change does not carry is an ABSENT key, never a
|
|
64
|
+
* fabricated default — what a missing source means is the declaration's own `supply` policy
|
|
65
|
+
* to state. `state` exists only where both sides do, so a declaration comparing before with
|
|
66
|
+
* after refuses a change that has no before. A path outside the root is dropped, as
|
|
67
|
+
* everywhere else in this module: a declaration's scope is written repo-relative.
|
|
68
|
+
*
|
|
69
|
+
* `changes` is the observation unit's change set, the same array instance in every world so
|
|
70
|
+
* a large commit costs one list rather than one per change. It is derived from this input
|
|
71
|
+
* unless the host supplied its own — a host whose observation is wider than the changes it
|
|
72
|
+
* dispatches at once has a set no derivation here could reach.
|
|
73
|
+
*
|
|
74
|
+
* `actor` is the input's actor object, carried by every world of the input — the file
|
|
75
|
+
* worlds and the call world alike. An input without one leaves the key absent, since `{}`
|
|
76
|
+
* is the positive value saying the host observed an actor that is not a subagent.
|
|
77
|
+
*
|
|
78
|
+
* `command` is the first shell call's command line with its heredoc bodies and herestring
|
|
79
|
+
* words deleted, carried by every world of the input. bash hands those bytes to the command
|
|
80
|
+
* as stdin data instead of executing them — except a body or word bash expands first, which
|
|
81
|
+
* stays — and whether they become a file write is the shell-evidence path's judgment, which
|
|
82
|
+
* reads the command as written.
|
|
83
|
+
* A shell call changing no in-scope file is still one observation, so it yields the single
|
|
84
|
+
* CALL WORLD — subject `'-'`, no `target.path`, so a path-scoped declaration finds no string
|
|
85
|
+
* and refuses it. A surface whose shell tools are empty observes no shell call, hence no
|
|
86
|
+
* `command` and no call world.
|
|
87
|
+
*/
|
|
88
|
+
export declare function worldsFromInput(spec: WorldsFromInputSpec): SuppliedWorld[];
|
|
89
|
+
/**
|
|
90
|
+
* Compile validated discipline entries into dispatcher registrations.
|
|
91
|
+
*
|
|
92
|
+
* One registration per entry: `label` = id (per-discipline telemetry), `protectedPaths`
|
|
93
|
+
* = [] (routing is the matches closure, not path mention), `body` = the judge thunk with
|
|
94
|
+
* the entry and the assembly values bound in. Each entry gains a second, body-less
|
|
95
|
+
* registration for its shell axis, and one common `shell-unjudgeable` registration is
|
|
96
|
+
* appended last whatever the entry count.
|
|
97
|
+
*
|
|
98
|
+
* An entry whose declaration does not compile becomes a **skip registration** that routes
|
|
99
|
+
* nothing and names its location on stderr. Assembly never throws — one bad entry taking
|
|
100
|
+
* down its siblings, the meta-covenants, and the witness valve would leave no way to fix
|
|
101
|
+
* the config that caused it.
|
|
102
|
+
*/
|
|
103
|
+
export declare function compileDisciplineRegistrations(spec: CompileDisciplinesSpec): CovenantRegistration[];
|