polydeukes 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.
@@ -92,16 +92,17 @@ shell axes, the session transcript, an assembly that cannot judge (missing or in
92
92
  config, unbuilt judge, unparseable payload, a routing that could not answer) — plus any
93
93
  entry promoted with `enforce: block`. Every other discipline entry lands `advised` there.
94
94
 
95
- **Context-family disciplines skip on the commit surface.** A commit has no session to look
96
- at, so a `requirePrecedent` entry cannot be judged there demanding evidence a commit
97
- cannot carry would block every matching commit with no legitimate way through.
98
-
99
- They are not filtered out, though. They assemble like any other discipline and become
100
- *skip registrations*: routing intact, no judge body. When one matches a staged change it
101
- records a `skipped` telemetry event and lets the commit proceed. The record carries the
102
- entry's `id` and the change it would have judged, so a gate that did nothing says so in
103
- the data — and it appears **only when the entry's scope actually matched**, so a commit
104
- touching nothing the entry cares about records nothing at all.
95
+ **Declarations that read the session skip on the commit surface.** A commit has no session
96
+ to look at, so a declaration whose `sources` bind the transcript — a `precedent`,
97
+ `phase-order`, `turn-locality` or `stated-ground` entry cannot be judged there; demanding
98
+ evidence a commit cannot carry would block every matching commit with no legitimate way
99
+ through. The declaration's own `supply: { session: 'pass' }` disposes of the absence: when
100
+ its scope matches a staged change it records a `skipped` telemetry event carrying the reason
101
+ token `supply-pass` and lets the commit proceed. The record carries the entry's `id` and the
102
+ change it would have judged, so a gate that did nothing says so in the data — and it appears
103
+ **only when the entry's scope actually matched**. A declaration scoped on the `command`
104
+ source records nothing at all there: a staged diff carries no command line, so no world it
105
+ observes is ever admitted.
105
106
 
106
107
  This is the same disposition the session surface uses whenever it has no transcript to
107
108
  read. One rule, both surfaces: evidence that cannot be evaluated is skipped and measured,
@@ -176,10 +177,10 @@ recorded as `witnessed`, never silent.
176
177
  ## `disciplines`
177
178
 
178
179
  Optional. Each entry is one discipline: a practice the team imposes on itself, declared as
179
- data. An entry carries exactly **one** predicate (zero or two is rejected), an `id` (the
180
- telemetry label), and optionally a `why` (the reason, which travels with the block message
181
- the agent reads) plus, on a `forbid` or `requirePrecedent` entry, `in` (the file globs it
182
- judges) and `except` (globs carved out of that scope).
180
+ data. A judged entry carries a `declare` block the one judged form, a declaration whose
181
+ `scope` lives inside the block — an `id` (the telemetry label), and optionally a `why` (the
182
+ reason, which travels with the block message the agent reads) and an `enforce` level. The
183
+ closed key set is `id` · `why` · `enforce` · `declare`; any other key is refused.
183
184
 
184
185
  **`draft` — an unpromoted entry.** The one shape that carries no predicate:
185
186
  `{ id, why, draft: true }` and nothing else. A draft registers a practice as prose ahead of
@@ -215,124 +216,241 @@ other value is rejected at load time. `pdks explain` prints the level an entry d
215
216
  the session header states the default.
216
217
 
217
218
  ```yaml
218
- - id: 'no-console-log'
219
- why: 'console output belongs to the logger; measure the habit before blocking it.'
220
- forbid: 'console\.log\('
219
+ - id: 'hooks-stay-armed'
220
+ why: 'a command that disarms or reroutes the git gate is a gate bypass in itself.'
221
221
  enforce: advise
222
+ declare:
223
+ mechanism: 'forbidden-command'
224
+ scope: { source: 'command' }
225
+ extract:
226
+ hits:
227
+ - { op: 'source', of: 'command' }
228
+ - { op: 'lines' }
229
+ - { op: 'matches', re: 'LEFTHOOK=(0|false|no|off)\b|core\.hooksPath' }
230
+ relate:
231
+ - { id: 'gates-armed', relation: { op: 'empty', of: 'hits' }, message: '{value}' }
222
232
  ```
223
233
 
224
- **`forbid` content delta.** Blocks an edit that *adds* a new match of the pattern.
225
- Existing occurrences are forgiven: adopting a discipline never blocks a legacy codebase,
226
- because the judgment direction is "what did this edit add", not "what does the file
227
- contain".
234
+ **Added-direction content is a declaration.** A promise about what an edit *adds* a
235
+ banned word, a stray `.only`, a citation that resolves nowhere is written as an
236
+ `added-only` declaration: `pre` and `post` are each cut into lines and keyed by the match,
237
+ `onlyIn` keeps what `post` has and `pre` lacks, and `empty` over that difference is the
238
+ verdict. Existing occurrences are forgiven, so adopting the discipline never blocks a legacy
239
+ codebase. `supply: empty` is what lets a file creation (no `pre`) count as all-added and a
240
+ deletion (no `post`) as adding nothing; the `scope` block replaces `in`/`except` with
241
+ regular expressions over the path.
228
242
 
229
243
  ```yaml
230
244
  disciplines:
231
245
  - id: 'covenant-vocabulary'
232
246
  why: 'control-framing vocabulary is banned in package sources.'
233
- in:
234
- - 'packages/*/src/**'
235
- forbid: '\b(guard|harness|kb)\b'
247
+ declare:
248
+ mechanism: 'added-only'
249
+ scope: { source: 'target.path', include: ['^packages/[^/]+/src/'] }
250
+ supply: { pre: 'empty', post: 'empty' }
251
+ extract:
252
+ before:
253
+ - { op: 'source', of: 'pre' }
254
+ - { op: 'lines' }
255
+ - { op: 'keyByPattern', re: '\b(guard|harness|kb)\b' }
256
+ after:
257
+ - { op: 'source', of: 'post' }
258
+ - { op: 'lines' }
259
+ - { op: 'keyByPattern', re: '\b(guard|harness|kb)\b' }
260
+ added:
261
+ - { op: 'onlyIn', of: 'after', notIn: 'before' }
262
+ relate:
263
+ - id: 'nothing-added'
264
+ relation: { op: 'empty', of: 'added' }
265
+ message: 'adds {key}: {value}'
236
266
  ```
237
267
 
238
- **`immutable` path family.** Blocks modification of existing files that match; creating
239
- new files is allowed.
268
+ The key is the match text, so a line carrying a word the file already has anywhere is
269
+ forgiven, and a line carrying two new words surfaces the first one now and the second on the
270
+ next judgment.
271
+
272
+ **A frozen path is a declaration too.** A file that may be created once and never modified
273
+ or deleted: `pre` present means a modification, `post` absent means a deletion, and either
274
+ breaks.
240
275
 
241
276
  ```yaml
242
277
  - id: 'archived-records-stay-frozen'
243
278
  why: 'an archive that can be edited is not an archive.'
244
- immutable: 'records/archive/**'
279
+ declare:
280
+ mechanism: 'self-absolution-ban'
281
+ scope: { source: 'target.path', include: ['^records/archive/'] }
282
+ supply: { pre: 'empty', post: 'empty' }
283
+ extract:
284
+ prior: [{ op: 'source', of: 'pre' }]
285
+ here: [{ op: 'source', of: 'target.path' }]
286
+ after: [{ op: 'source', of: 'post' }]
287
+ deleted: [{ op: 'onlyIn', of: 'here', notIn: 'after' }]
288
+ touched: [{ op: 'union', of: ['prior', 'deleted'] }]
289
+ relate:
290
+ - { id: 'frozen', relation: { op: 'empty', of: 'touched' }, message: '{value} is frozen' }
245
291
  ```
246
292
 
247
- **`forbidCommand` command family.** Blocks shell commands matching the pattern, even
248
- when the command mentions no protected path. This is how gate-disarming commands are
249
- caught. A multi-line command is judged twice over the pattern is tested against each
250
- line and against the whole string, so `^` means the start of a line while a pattern
251
- spanning a line boundary still matches (the whole-content caution further down applies
252
- to the delta and context families). An empty pattern is rejected at load time, here and
253
- on `forbid` alike it would match every command.
293
+ **A command line is a source.** On the session surface a shell call carries its command
294
+ line as the fixed source `command`, and a call that changes no file is still one
295
+ observation it is judged as a world of its own, with subject `-`. A `forbidden-command`
296
+ declaration reads that source, cuts it into lines, keeps the lines a pattern matches, and
297
+ requires the result to be `empty`. It scopes on `command` so that only shell calls are
298
+ admitted: an Edit carries no command line, and a declaration reading a source its world
299
+ lacks is unjudgeable. A multi-line command is judged line by line, so `^` means the start
300
+ of a line; a pattern that would span a line boundary does not match.
254
301
 
255
302
  ```yaml
256
303
  - id: 'hooks-stay-armed'
257
304
  why: 'a command that disarms or reroutes the git gate is a gate bypass in itself.'
258
- forbidCommand: 'LEFTHOOK=(0|false|no|off)\b|core\.hooksPath'
305
+ declare:
306
+ mechanism: 'forbidden-command'
307
+ scope: { source: 'command' }
308
+ extract:
309
+ hits:
310
+ - { op: 'source', of: 'command' }
311
+ - { op: 'lines' }
312
+ - { op: 'matches', re: 'LEFTHOOK=(0|false|no|off)\b|core\.hooksPath' }
313
+ relate:
314
+ - { id: 'gates-armed', relation: { op: 'empty', of: 'hits' }, message: '{value}' }
259
315
  ```
260
316
 
261
- **`requirePrecedent` context family.** Blocks a change that arrives without a required
262
- step having happened earlier in the session. The other three families all ask "is this
263
- change itself bad"; this one asks something else. The change is legitimate what is
264
- missing is the procedure in front of it, so what gets judged is not the mutation but the
265
- session history.
266
-
267
- Evidence means an **execution**, not a request. A call the covenant blocked, one a human
268
- refused, and one that simply failed all leave the same trace in a session, and none of
269
- them is precedentthe transcript is read for what actually ran and reported success.
270
- That is what keeps the cheapest way through the gate being the thing the discipline
271
- asks for.
272
-
273
- Two consequences are worth knowing before you write one. The outcome is read per command
274
- LINE, so a chain where the required command ran but a later step failed does not count.
275
- And the pattern is matched at the start of a simple command, so the same words in an
276
- argument or a comment do not count either. **In both cases running the command on its own
277
- opens the gate** — the block message says so.
317
+ **A precedent is a declaration over the session.** Most declarations ask "is this change
318
+ itself bad"; a `precedent` asks whether a required step happened earlier in the session.
319
+ The change is legitimate what is missing is the procedure in front of it, so what gets
320
+ judged is the session history: `sources: { session: { transcript: true } }` hands the
321
+ declaration the user turns and tool calls as one snapshot, `toolUses` picks the calls,
322
+ `filter` keeps the ones that ran and **succeeded**, `select` reaches the command line, and
323
+ `matches` finds the required one; `nonEmpty` is the verdict. A call the covenant blocked,
324
+ one a human refused, and one that simply failed are not precedent. The pattern is matched
325
+ anywhere in a command line a line that merely mentions the command counts, a declared
326
+ limit. `supply: { session: 'pass' }` is what makes the commit surface record `skipped`
327
+ instead of blocking every matching commit.
278
328
 
279
329
  ```yaml
280
330
  - id: 'dependency-needs-npm-view'
281
331
  why: 'a dependency version must be measured before it is written.'
282
- in:
283
- - 'package.json'
284
- - 'packages/*/package.json'
285
- when: '(^|\n)\s*"[^"]+"\s*:\s*"[~^]?\d[^"]*"'
286
- requirePrecedent:
287
- command: 'npm view '
332
+ declare:
333
+ mechanism: 'precedent'
334
+ scope: { source: 'target.path', include: ['^(packages/[^/]+/)?package\.json$'] }
335
+ sources: { session: { transcript: true } }
336
+ supply: { session: 'pass' }
337
+ extract:
338
+ npmView:
339
+ - { op: 'source', of: 'session' }
340
+ - { op: 'toolUses', names: ['Bash'] }
341
+ - { op: 'filter', when: [{ field: 'succeeded', eq: true }] }
342
+ - { op: 'select', path: 'args.command' }
343
+ - { op: 'matches', re: '\bnpm view ' }
344
+ relate:
345
+ - { id: 'npm-view', relation: { op: 'nonEmpty', of: 'npmView' }, message: 'no successful npm view precedes this manifest edit' }
346
+ ```
347
+
348
+ A tool call is evidence the same way: `toolUses` without `names`, then `field name` and
349
+ `matches` over the tool's name, or `toolUses` with `subagentType` for a spawn of one agent
350
+ kind. The other history mechanisms read the same snapshot — `phase-order` relates two
351
+ spawn ordinals with `ordered`, `turn-locality` keeps the user turns inside a time window
352
+ (`userTexts → ageMs → filter lte`), and `stated-ground` requires a user turn matching a
353
+ pattern; the last two are usually scoped on `command`, so that only the shell call they
354
+ apply to is judged.
355
+
356
+ **A caution on line anchors.** A declaration's `lines` step splits the text first, so `^`
357
+ inside `keyByPattern` or `matches` after it is the start of a line. A pattern that stops
358
+ mid-value — say at the first digit of a version — keys `4.0.5` and `4.0.6` alike, so a bump
359
+ adds nothing to an added-only difference and the discipline silently passes: make the
360
+ pattern span the whole value that can change. Both failure shapes compile, run, and answer
361
+ `passed`, so measure a new entry against a real file and a realistic edit.
362
+
363
+ **The cheap way through is the honest one.** Unlike the witness, session evidence lives on
364
+ the AI's own surface, so it is not forgery-proof. It does not need to be: the least
365
+ effortful way to open this gate is to actually run the command, and that is exactly the
366
+ behaviour the discipline exists to induce.
367
+
368
+ **`declare` — declaration family.** One judgment written as data, in the algebra grammar
369
+ the core publishes as `algebra-declaration.schema.json`: `judge = relate ∘ extract`. The
370
+ block carries the declaration's `scope`, `sources`, `supply`, `extract`, `relate`, and optional
371
+ `witness`; the entry's `id` is the declaration's name, so the block never carries a
372
+ `discipline` key, and `in`/`except`/`when` are refused — the `scope` block is the scope.
373
+
374
+ ```yaml
375
+ - id: 'db-only-under-knowledge'
376
+ why: 'a *.db file may exist only under _docs/knowledge/'
377
+ declare:
378
+ mechanism: 'naming'
379
+ scope: { source: 'target.path', include: ['\.db$'] }
380
+ extract:
381
+ outside:
382
+ - { op: 'source', of: 'target.path' }
383
+ - { op: 'matches', re: '^(?!_docs/knowledge/)' }
384
+ relate:
385
+ - id: 'placed'
386
+ relation: { op: 'empty', of: 'outside' }
387
+ message: '{value} is outside _docs/knowledge/'
288
388
  ```
289
389
 
290
- The evidence vocabulary is layered. `command` is the core's own key a shell call is a surface
291
- every agent shares — and the core validates it fully, rejecting an empty string or a pattern that
292
- does not compile. It is matched **at the start of a simple command**, not anywhere in the command
293
- line, so `echo "npm view yaml"` and a mention parked behind a `#` are not evidence while `cd pkg &&
294
- npm view yaml` is. Every other key belongs to an adapter: the core checks the container only (a flat
295
- object carrying exactly one evidence key) and passes the value through verbatim, and the adapter
296
- that owns the word validates and judges it. The Claude Code adapter brings two: `subagent` (exact
297
- match on a spawn kind) and `tool` (a regex over tool names) so "query the docs tool before
298
- touching this" is expressible today. Both follow the same execution rule as `command`. An evidence
299
- key no assembled adapter recognizes cannot be judged, so the entry compiles to a skip registration:
300
- routing stays, the body is dropped, assembly names the fault once on stderr, and every matching
301
- change afterwards records `skipped` rather than a verdict. A typo therefore never passes itself off
302
- as adapter vocabulary but it does leave the discipline inert, and the `skipped` rows are where
303
- that shows.
304
-
305
- `when` (optional) is the trigger: an added-direction delta regex, combinable with
306
- `requirePrecedent` and with nothing else. When it is absent, every change inside `in`
307
- scope triggers the discipline. The two keys divide the work `in` says which files are
308
- watched, `when` says which change in them demands the precedent.
309
-
310
- **A caution on line anchors.** These patterns are matched against the file's whole content
311
- as a single string, and the config schema takes a regex string with no flags. `^` therefore
312
- anchors to the start of the *file*, not the start of a line, so a line-shaped pattern
313
- written with `^` matches only the first line and the discipline silently stops firing
314
- the regex still compiles, the judgment still runs, and the verdict is `passed`. Write
315
- `(^|\n)` when you mean the start of a line. This is why the example above carries
316
- `(^|\n)\s*"[^"]+"…` rather than `^\s*"[^"]+"…`.
317
-
318
- **And a caution on match length.** The delta keys on the matched *text*: a change is only
319
- seen as added when the matched string itself differs between the file's before and after.
320
- A pattern that stops mid-value say at the first digit of a version produces the same
321
- match text for `4.0.5` and `4.0.6`, so a version bump adds nothing to the delta and the
322
- discipline silently passes. Make the pattern span the whole value that can change; the
323
- example above runs through the closing quote (`\d[^"]*"`) for exactly this reason. Both
324
- failure shapes are the same class: the regex compiles, the verdict says `passed`, and
325
- nothing tells you the discipline is inert so when you add an entry, measure it against
326
- a real file and a realistic edit, not a one-line snippet.
327
-
328
- The kind of change matters at the trigger. With `when` present, a deletion never triggers
329
- deleting adds no content. With `when` absent, deletion triggers like any other change in
330
- scope, since the declared scope is the whole mutation.
331
-
332
- **The cheap way through is the honest one.** Unlike the witness, this evidence lives on the
333
- AI's own surface, so it is not forgery-proof. It does not need to be: the least effortful
334
- way to open this gate is to actually call the tool, and that is exactly the behaviour the
335
- discipline exists to induce.
390
+ This repository's live config carries the same declaration as `sqlite-only-under-knowledge`.
391
+
392
+ Each observation is judged as one **world** with seven source names: `target.path` (the
393
+ repo-relative path), `pre` and `post` (the file's text on the side the change carries
394
+ a creation has no `pre`, a deletion no `post`), `state` (`{ pre, post }`, present only
395
+ on a modification), and `changes` (every path the observation changes — the one call on the
396
+ session surface, the whole staged set on the commit surface), and `command` (the shell
397
+ call's command line present on a shell call only, and a shell call that changes no file
398
+ is one world of its own, so a declaration scoped on `command` sees it while one scoped on
399
+ `target.path` does not), and `actor` (the observation's actor described below). A
400
+ declaration that reads `changes` is judged only where the whole change set is observed: the
401
+ session surface records it `skipped`, the same disposition the commit surface gives a
402
+ declaration that reads the session, because one call can never carry the other half of a
403
+ pair. This repository's live config
404
+ carries one — `docs-stay-bilingual`, an `implies` over the `.md`/`.ko.md` pair, advised
405
+ on the commit surface when one side is staged without the other. A declaration that needs a
406
+ file outside the target names it in a `sources` block, `sources: { en: { file:
407
+ 'locales/en.json' } }`, and reads it as `{ op: 'source', of: 'en' }`; the path is
408
+ repo-relative (no leading `/`, no `..` segment) and the name may not be one of the seven. The
409
+ surface reads the file the way it observes the tree — the disk in a session, the index for a
410
+ staged commit, the `<to>` commit for a range except that a named file the change itself
411
+ touches is read from the change's `post`, so both surfaces judge the same text. A second
412
+ kind, `sources: { spawns: { sidecar: true } }`, names the session's spawn-record channel
413
+ instead of a path the subagent records the host keeps beside the transcript, supplied as
414
+ one JSON array; where the channel lives is the surface's fact, so the value is the marker
415
+ `true`, and on the commit surface (which has no session) the channel is always absent. A
416
+ third kind, `sources: { session: { transcript: true } }`, names the session's own
417
+ conversation history — the user turns and tool calls the surface reads, handed to the
418
+ declaration as one snapshot whose entries carry their
419
+ observation ordinal; the history steps (`toolUses`, `userTexts`, `first`, `ageMs`) read it, and
420
+ `agentType` reads the parsed sidecar. This repository's live config carries one
421
+ `tests-before-implementation`, an `ordered` over the ordinals of two subagent spawns, which
422
+ the commit surface (no session) records `skipped`. The seventh fixed name, `actor`, is the
423
+ observation's actor `{ agentType }` inside a subagent, `{}` in the main session, absent
424
+ where the surface proves none (the commit surface) read as `{ op: 'source', of: 'actor' }`
425
+ followed by `select` on `agentType`; it derives the `actor` axis the `producer-owned` and
426
+ `actor-scope` mechanisms require, and this repository's live config carries one of each
427
+ (`tests-are-the-writers`, `commits-come-from-the-main-session`). A `supply` key must name
428
+ one of the seven fixed sources or one of the declaration's own `sources`; any other key is
429
+ refused. A source the change does not carry is absent, and the declaration's
430
+ `supply` block says what that means: `error` (the default) makes the call unjudgeable —
431
+ recorded `blocked` at either enforce level — `pass` leaves it unjudged, and `empty` reads
432
+ the absent side as an empty item list and judges on. `empty` is what lets an added-only
433
+ declaration see a creation as all-added and a deletion as adding nothing; it does not apply
434
+ to `state`, the paired source. A declaration comparing before with after therefore needs
435
+ `supply: { state: pass }` to let a file creation through.
436
+
437
+ A break is recorded like any other family's, with one addition: the telemetry row carries a
438
+ fifth field naming the elements the relation failed on (at most eight per relate entry, with
439
+ the true count beside them). A `skipped` row uses the same field for a reason token instead —
440
+ `no-observation` (the surface has no channel for what the entry reads), `config-fault` (the
441
+ block could not be assembled), or `supply-pass` (the declaration's own `supply: pass` let an
442
+ absent source through). Every declaration also names its `mechanism` — one of eighteen
443
+ catalogue names such as `naming`, `companion`, or `pairing` — and the validator refuses a
444
+ name whose shape the declaration does not match: the axes its sources derive (`change` for
445
+ the fixed names other than `actor`, `actor` for `actor`, `world` for a `file` or `sidecar`
446
+ source, `history` for a `transcript` source) and the relations it relates must fall inside
447
+ what that name admits. A block the compiler cannot resolve — a step name outside the
448
+ registry, an argument outside a step's keys — becomes a skip registration that names its
449
+ location on stderr and routes nothing. A shell write into the declaration's scope whose
450
+ result the judge can compute (a redirect, a heredoc, an append) is judged as the file change
451
+ it makes; one it cannot compute (`sed -i`, an opaque command) records `skipped`. The
452
+ declaration's own `witness` block joins the
453
+ human's witness as a second way to open a blocked verdict.
336
454
 
337
455
  Adding a discipline is a data edit — no code, no plumbing. Custom judge bodies remain the
338
456
  escape layer for the few rules data cannot express.
@@ -27,6 +27,13 @@ hand-rolled and the published JSON Schema is a sibling artifact the source never
27
27
  *values* supplied by configs and adapters, so the core's agent-neutrality is a claim a grep
28
28
  can check. Every other package depends on this one; this one depends on none of them.
29
29
 
30
+ **The judge and the two adapters name it as a `peerDependency`, not a dependency of their
31
+ own.** They share the vocabulary rather than each installing a copy — `SOURCE_KINDS` and
32
+ `parseInput` have to be one set of values for the validator and the engine to agree, and two
33
+ copies would disagree silently instead of failing at install time. The umbrella carries the
34
+ ordinary dependency that satisfies that peer, which is why a consumer still installs one
35
+ package and gets core transitively.
36
+
30
37
  ## The judged protocol
31
38
 
32
39
  This is the contract the shipped judges speak: a judge receives a `CovenantInput` — parsed
@@ -40,6 +47,7 @@ type CovenantInput = {
40
47
  toolCalls: { name: string; args?: Record<string, unknown>; fileChange?: FileChange }[];
41
48
  subagentSpawns: { kind: string }[];
42
49
  userMessages: { text: string }[];
50
+ actor?: { agentType?: string };
43
51
  };
44
52
 
45
53
  type FileChange =
@@ -55,7 +63,9 @@ fills into `name`; `kind` on a spawn is likewise a value. `FileChange` is a disc
55
63
  union so that a deletion is first-class evidence rather than an unrepresentable case, and
56
64
  impossible states — a deletion carrying resulting content, a creation carrying a baseline —
57
65
  cannot be written down. `delete.pre` is absent when the baseline was a binary blob, because
58
- a deletion needs no content to be judged.
66
+ a deletion needs no content to be judged. `actor` is the observation's actor as the host
67
+ envelope proves it — `agentType` inside a subagent, `{}` in the main session — and is absent
68
+ when the surface proves none; the judge never defaults it.
59
69
 
60
70
  **Evidence has exactly one home: the call it belongs to.** `fileChange` absent means *this
61
71
  call is unproven*, and no sibling call's evidence stands in for it.
@@ -102,10 +112,6 @@ Everything else here is reached through `polydeukes`.
102
112
  not check that a namespace *name* is one anybody implements, and it does not look inside
103
113
  the namespace at all. Unknown vocabulary inside `adapters.git` is rejected by the git
104
114
  adapter's own validator, at its own layer — not here.
105
- - **`requirePrecedent` evidence is layered the same way.** The core fully validates the
106
- `command` key, because a shell command is the surface where an agent crosses into the
107
- system. Every other key is validated for container shape alone — a flat object holding
108
- exactly one key — and its value passes through verbatim for the owning adapter to judge.
109
115
  - **The default transcript is a noop.** A consumer that injects no real transcript
110
116
  converges on "nothing happened", which is the safe direction for a valve: it never opens.
111
117
  Real transcripts live behind adapters.
@@ -13,6 +13,11 @@
13
13
 
14
14
  The judge. Everything that turns a declared promise into a verdict lives here.
15
15
 
16
+ **This is the install unit for judgment.** An embedder that wants verdicts without the
17
+ umbrella's surfaces installs this package and provides `@polydeukes/core` alongside it — core
18
+ is a `peerDependency` here, so the vocabulary is shared rather than duplicated. The umbrella
19
+ is what satisfies that peer for an ordinary consumer.
20
+
16
21
  | Unit | What it does |
17
22
  |---|---|
18
23
  | `runCovenant` wrapper | Runs a judge body, translates its non-blocking `1` into the blocking `2`, and logs every call. No covenant runs unmeasured |
@@ -24,20 +29,20 @@ The judge. Everything that turns a declared promise into a verdict lives here.
24
29
 
25
30
  ## Discipline families and meta-covenants
26
31
 
27
- **A `disciplines:` entry belongs to exactly one family**, decided by which predicate key it
28
- carries. The family determines what evidence the judgment needs which is also what
29
- determines whether it can be judged on a given surface.
32
+ **A `disciplines:` entry is one declaration** `judge = relate extract` over the
33
+ observation as a world. What the declaration's sources bind decides what evidence the
34
+ judgment needs, which is also what decides whether it can be judged on a given surface.
30
35
 
31
- | Family | Key | Judges | Evidence needed |
32
- |---|---|---|---|
33
- | delta | `forbid` | Added-direction content of a file change. Existing debt is forgiven; only new occurrences break | File change |
34
- | path | `immutable` | Any change to a file that already existsmodification or deletion alike. Creation passes | File change |
35
- | command | `forbidCommand` | The command line itself | None |
36
- | context | `requirePrecedent` | Session history was a qualifying call actually executed *before* this one | A transcript |
36
+ | Sources | Judges | Evidence needed |
37
+ |---|---|---|
38
+ | the fixed names `target.path` · `pre` · `post` · `state` · `changes` | The change itself | A file change |
39
+ | the fixed name `command` | The shell call's command line | A shell call an Edit carries none |
40
+ | `{ transcript: true }` | Session history was a qualifying call actually executed *before* this one | A session |
41
+ | `{ file: … }` · `{ sidecar: true }` | Another file, or the spawn-record channel | The surface's reader for it |
37
42
 
38
- `when` is a trigger, not a family: it narrows a `requirePrecedent` entry and combines with
39
- nothing else. The writing guide for these entries — the four predicate forms, the two
40
- pitfalls — is [the configuration reference's `disciplines` section](./configuration.md#disciplines).
43
+ The writing guide for these entries is [the configuration reference's `disciplines`
44
+ section](./configuration.md#disciplines); the declaration grammar is the core's
45
+ `algebra-declaration.schema.json`.
41
46
 
42
47
  **Three meta-covenants** protect the judging chain. They are covenants like any other; the
43
48
  vocabulary below applies to them unchanged.
@@ -86,18 +91,21 @@ No import. The umbrella assembles this package for both surfaces.
86
91
  not "nothing gets through" — it is that **no call passes unrecorded**. A new spelling
87
92
  landing in `skipped` is the declared limit showing itself. A pass with no row at all, or
88
93
  one recorded `passed` without a judgment, is the defect class.
89
- - **The context family cannot be judged without a session.** On the commit surface there is
90
- none, so a matching `requirePrecedent` entry always records `skipped`. That is a permanent
94
+ - **A declaration that reads the session cannot be judged without one.** On the commit
95
+ surface there is none, so a matching `precedent` (or any other transcript-reading)
96
+ declaration records `skipped` with the reason `supply-pass`. That is a permanent
91
97
  condition of that surface.
92
- - **The command family is absent from the commit surface, and absent without a row.**
93
- `forbidCommand` entries are filtered out before compilation there, since a staged diff
94
- carries no command line to judge. Unlike the context family this leaves nothing in
95
- `.polydeukes/roi.log`, so the log cannot separate a command discipline that never
96
- triggered from one that was never registered on that surface.
97
- - **An unjudgeable entry compiles to a skip registration** routing intact, no body. A
98
- pattern that does not compile skips the same way. Assembly therefore never throws: one
99
- unresolvable entry cannot take down its siblings, the meta-covenants, and the valve,
100
- which would leave no way to fix the config that caused it.
98
+ - **A declaration scoped on `command` is absent from the commit surface, and absent without
99
+ a row.** A staged diff carries no command line, so no world such a declaration observes is
100
+ admitted there. This leaves nothing in `.polydeukes/roi.log`, so the log cannot separate a
101
+ command discipline that never triggered from one whose surface never observed a command.
102
+ - **A declaration the compiler cannot resolve compiles to a skip registration** — routing
103
+ intact, no body: a step outside the registry, an argument outside a step's keys, a pattern
104
+ that does not compile, a mechanism whose shape the syntax does not fit. Assembly therefore
105
+ never throws: one unresolvable entry cannot take down its siblings, the meta-covenants, and
106
+ the valve, which would leave no way to fix the config that caused it. A source the world
107
+ lacks at judgment time is a different case — the declaration's own `supply` policy
108
+ disposes of it, and with no policy the body answers unjudgeable (exit 2), never upheld.
101
109
  - **Complete containment is a non-goal.** There are no blocklists here — enumerating bypass
102
110
  spellings is always one step behind, so the logic is inverted: a mention of a protected
103
111
  path blocks unless proven safe. Residual vectors such as indirect path computation are