polydeukes 0.4.0 → 0.5.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `runClaudeCodeHook` — the assembled session-surface judgment runner (DIST-01 §3-c).
2
+ * `runClaudeCodeHook` — the assembled session-surface judgment runner.
3
3
  *
4
4
  * The session counterpart of {@link runCovenantCheck}, and the one place where the Claude
5
5
  * Code adapter (tool vocabulary, up-translation) and the covenant package (dispatcher +
@@ -8,17 +8,14 @@
8
8
  * a delegator that calls this function. That is what makes the session surface installable:
9
9
  * a consumer registers a hook that resolves this package instead of copying assembly.
10
10
  *
11
- * Wiring shape: COVENANT-03 §4.4 + COVENANT-04d §4.5 registrations consumed through
12
- * ADAPTER-03 §4.1 `runAdapterPath`, with `dispatchCovenants` bound to the injected dispatch
13
- * seam. The protection-policy data (protectedPaths / disciplines / witness) is read from the
14
- * root data config through {@link loadConfig} (CONFIG-03), which also attaches the config
15
- * file to its own surface.
11
+ * The protection-policy data (protectedPaths / disciplines / witness) is read from the root
12
+ * data config through {@link loadConfig}, which also attaches the config file to its own
13
+ * surface.
16
14
  *
17
- * The valve is the TTL witness (COVENANT-06, moved behind the verdict by COVENANT-17)
18
- * judged over the JSONL transcript provider (ADAPTER-04). The judge body always spawns, and
19
- * only an outcome that translated to blocked consults the witness `witnessed` rows are
20
- * would-block only. Its defence is provenance rather than secrecy: only a real human
21
- * utterance carries the transcript marking `findUserMessages()` admits.
15
+ * The valve is the TTL witness, judged over the JSONL transcript provider. The judge body
16
+ * always spawns, and only an outcome that translated to blocked consults the witness —
17
+ * `witnessed` rows are would-block only. Its defence is provenance rather than secrecy: only
18
+ * a real human utterance carries the transcript marking `findUserMessages()` admits.
22
19
  *
23
20
  * fail-closed: ANY failure — an unbuilt judge body, an unreadable stdin, a missing or
24
21
  * invalid config file — resolves to `{ exitCode: 2 }` with one `blocked` record under the
@@ -26,33 +23,19 @@
26
23
  * the cheapest bypass vector there is. Recovery from an unbuilt clone is `pnpm build` (it
27
24
  * mentions no protected path, so it is never blocked).
28
25
  */
29
- import { existsSync, mkdirSync, readFileSync } from 'node:fs';
30
- import { createRequire } from 'node:module';
26
+ import { mkdirSync, readFileSync } from 'node:fs';
31
27
  import { homedir } from 'node:os';
32
- import { dirname, join, resolve } from 'node:path';
28
+ import { join, resolve } from 'node:path';
33
29
  import { COMMAND_ARGS, evaluatePrecedent, MUTATING_TOOLS, runAdapterPath, SHELL_TOOLS, transcriptFromJsonlFile, transcriptPathFromPayload, } from '@polydeukes/adapter-claude-code';
34
30
  import { appendRecordFailOpen, DEFAULT_TELEMETRY_LOG_PATH, normalizeProtectedPaths, readRecords, } from '@polydeukes/core';
35
- import { compileDisciplineRegistrations, dispatchCovenants, findUnattributed, readBaseline, snapshotBaseline, transcriptModRegistration, ttlWitness, writeBaseline, } from '@polydeukes/covenant';
31
+ import { findUnattributed, readBaseline, snapshotBaseline, ttlWitness, writeBaseline, } from '@polydeukes/covenant';
32
+ import { loadCovenantModule, resolveCovenantDist } from './covenant-module.js';
36
33
  import { loadConfig } from './load-config.js';
37
- /**
38
- * Compose a judge body path and prove it exists (CONFIG-06b §4.2). A body module that was
39
- * never built makes node exit 1 — the same code a real break verdict returns — so nothing
40
- * downstream can separate an unjudgeable run from a judged one. The proof therefore belongs
41
- * to the act of composing the path, and a body this assembly composes no path for is never
42
- * proven: the throw lands in the fail-closed catch below, one blocked record and exit 2.
43
- */
44
- function provenBodyPath(distDir, fileName) {
45
- const modulePath = join(distDir, fileName);
46
- if (!existsSync(modulePath)) {
47
- throw new Error(`judge body ${modulePath} is missing — run 'pnpm build' to rebuild it`);
48
- }
49
- return modulePath;
50
- }
51
- /** The label every post-hoc state comparison row carries (COVENANT-14 §2-d). */
34
+ /** The label every post-hoc state comparison row carries. */
52
35
  const BASELINE_LABEL = 'baseline';
53
36
  /**
54
37
  * Compare the protected entries' on-disk state against the stored baseline and record what
55
- * moved with no judgment explaining it (COVENANT-14 §2-f).
38
+ * moved with no judgment explaining it.
56
39
  *
57
40
  * Runs at hook call START, before this call's own judgment rows land, so the window it reads
58
41
  * is the one the previous comparison left open. Returns the record count as of right now —
@@ -68,9 +51,9 @@ function compareBaseline(spec) {
68
51
  const { records } = readRecords(spec.telemetryPath);
69
52
  const stored = readBaseline(baselinePath);
70
53
  if (stored === null) {
71
- // Absence and corruption are the same signal (§2-e). The baseline file is deliberately
72
- // NOT on the protection list — protecting it would need a comparison of its own — so its
73
- // disappearance has to stay legible in the log instead.
54
+ // Absence and corruption are the same signal. The baseline file is NOT on the protection
55
+ // list — protecting it would need a comparison of its own — so its disappearance has to
56
+ // stay legible in the log instead.
74
57
  appendRecordFailOpen(spec.telemetryPath, {
75
58
  event: 'unattributed',
76
59
  label: BASELINE_LABEL,
@@ -96,7 +79,7 @@ function compareBaseline(spec) {
96
79
  }
97
80
  }
98
81
  /**
99
- * Re-establish the baseline at hook call END (COVENANT-14 §5).
82
+ * Re-establish the baseline at hook call END.
100
83
  *
101
84
  * At call end rather than right after the comparison: refreshing at comparison time would
102
85
  * miss whatever this call's own judged writes changed, leaving detection permanently one
@@ -115,7 +98,7 @@ function updateBaseline(spec) {
115
98
  writeBaseline(join(dotDir, 'baseline.json'), snapshotBaseline({ rootDir: spec.repoRoot, entries: spec.entries }), new Date().toISOString());
116
99
  }
117
100
  /**
118
- * Where the comparison writes and what it observes, or `undefined` (COVENANT-14 §6).
101
+ * Where the comparison writes and what it observes, or `undefined`.
119
102
  *
120
103
  * The domain is derived from config rather than enumerated here, and the telemetry path is
121
104
  * resolved by the same precedence the judgment uses so both land in one log. A config that
@@ -140,20 +123,88 @@ function comparisonSpec(spec) {
140
123
  };
141
124
  }
142
125
  /**
143
- * Judge one declared tool call before it runs (DIST-01 §3-c). Async because the dispatcher
144
- * spawns covenant bodies (CORE-01) — a synchronous runner would mean reimplementing the
145
- * judge, which the single-dispatcher principle forbids.
126
+ * The session surface's registration set. One assembly, two consumers: the runner below
127
+ * dispatches it, `explain` renders it so what a reader is shown is the table the judgment
128
+ * actually uses, never a second opinion about it.
129
+ */
130
+ export function assembleSessionRegistrations(spec) {
131
+ const { config, rootDir, covenant, transcriptPath, transcript, witness } = spec;
132
+ // The live transcript is the evidence channel the context family reads AND the one the
133
+ // witness reads, so erasing or forging it disables every context discipline while opening
134
+ // or shutting the human valve on the same file. It must NOT join this list: it lives deep
135
+ // under HOME, and a path entry makes every ancestor protected — which measured as an
136
+ // over-block refusing `cd /home/<user>`, `echo $HOME`, and every edit whose content
137
+ // carried a bare `~`. The dedicated `transcript-mod` registration below covers that one
138
+ // file instead: equality-only, never an ancestor. Ancestor destruction outside the
139
+ // repository is out of observation scope — the agent's own deny policy owns what no
140
+ // repo-scoped judge can reach.
141
+ const protectedPaths = normalizeProtectedPaths({
142
+ protectedPaths: config.protectedPaths ?? [],
143
+ });
144
+ const disciplines = config.disciplines ?? [];
145
+ const registrations = [
146
+ covenant.selfModRegistration({
147
+ protectedPaths,
148
+ mutatingToolNames: MUTATING_TOOLS,
149
+ witness,
150
+ }),
151
+ covenant.shellModRegistration({
152
+ protectedPaths,
153
+ shellTools: SHELL_TOOLS,
154
+ commandArgs: COMMAND_ARGS,
155
+ witness,
156
+ }),
157
+ // Routing is the matches predicate, never path mention, so the home directory cannot
158
+ // become a protected ancestor. No transcript in the payload means nothing to protect —
159
+ // the valve and the context family already forfeited on the same absence.
160
+ ...(transcriptPath === undefined
161
+ ? []
162
+ : [
163
+ covenant.transcriptModRegistration({
164
+ transcriptPath,
165
+ // The env value first, since that is what the judged shell expands `~` and
166
+ // `$HOME` from. `homedir()` reads the same passwd entry bash falls back to when
167
+ // HOME is unset, so a hook spawned without an environment (a service manager,
168
+ // `env -i`) keeps judging the home spellings instead of silently going
169
+ // absolute-only — an inert spelling closure looks identical to a passing call.
170
+ home: process.env.HOME ?? homedir(),
171
+ shellTools: SHELL_TOOLS,
172
+ commandArgs: COMMAND_ARGS,
173
+ mutatingTools: MUTATING_TOOLS,
174
+ witness,
175
+ }),
176
+ ]),
177
+ ...covenant.compileDisciplineRegistrations({
178
+ disciplines,
179
+ rootDir,
180
+ shellTools: SHELL_TOOLS,
181
+ commandArgs: COMMAND_ARGS,
182
+ witness,
183
+ // Context-family evidence is evaluated here, at assembly: a spawned body cannot hold
184
+ // a transcript, and passing a path would leak JSONL knowledge into covenant. The
185
+ // adapter brings the evaluator for its own `subagent`/`tool` vocabulary; core owns
186
+ // `command`, which the compiler judges directly.
187
+ transcript,
188
+ evaluatePrecedent,
189
+ }),
190
+ ];
191
+ return registrations;
192
+ }
193
+ /**
194
+ * Judge one declared tool call before it runs. Async because the dispatcher spawns covenant
195
+ * bodies — a synchronous runner would mean reimplementing the judge, which the
196
+ * single-dispatcher principle forbids.
146
197
  */
147
198
  async function judgeHookCall(spec) {
148
- // Env-first telemetry precedence (E2E contract), settled BEFORE any failure branch: a
149
- // config that never loads still has somewhere to write its one blocked row. The config
150
- // value applies after the load succeeds.
199
+ // Env-first telemetry precedence, settled BEFORE any failure branch: a config that never
200
+ // loads still has somewhere to write its one blocked row. The config value applies after
201
+ // the load succeeds.
151
202
  //
152
203
  // Computed INSIDE the try even though it must run first, because `join` throws on a
153
204
  // non-string repoRoot and this function's contract is that nothing escapes it — a rejection
154
205
  // would exit a delegator non-blocking, which is the cheapest bypass there is. A throw here
155
206
  // leaves `telemetryPath` undefined, which the catch tolerates: there is no root to write a
156
- // row under anyway (PR #46 review).
207
+ // row under anyway.
157
208
  let telemetryPath;
158
209
  try {
159
210
  const envTelemetryPath = process.env.POLYDEUKES_TELEMETRY_PATH;
@@ -172,31 +223,14 @@ async function judgeHookCall(spec) {
172
223
  // The transcript path travels in the raw payload only — up-translation drops it, so the
173
224
  // adapter reads it from the string. Every failure narrows to `undefined`, which leaves
174
225
  // the dispatcher on its `noopTranscript` default: lost evidence closes the valve rather
175
- // than opening it (ADAPTER-04 §4.4).
226
+ // than opening it.
176
227
  const transcriptPath = transcriptPathFromPayload(rawPayload);
177
228
  const transcript = transcriptPath === undefined ? undefined : transcriptFromJsonlFile(transcriptPath);
178
- // The live transcript is the evidence channel the context family reads AND the one the
179
- // witness reads, so erasing or forging it disables every context discipline while
180
- // opening or shutting the human valve on the same file. It lives outside the repository,
181
- // so no config `protectedPaths` entry can reach it — and since COVENANT-07c it does NOT
182
- // join this list either. A file deep under HOME makes HOME itself a protected ANCESTOR,
183
- // which measured as the COVENANT-13 over-block: `cd /home/<user>` refused for two weeks,
184
- // and the 07b attempt to register the home spellings alongside only widened that to
185
- // `echo $HOME` and every edit whose content carried a bare `~`. Assembly knows the path
186
- // AND the home value, so assembly registers a dedicated `matches` predicate over that
187
- // ONE file instead (transcript-mod, below): equality-only — never an ancestor — with the
188
- // `~`/`$HOME`/`${HOME}`/`~<user>` spellings closed as data, reads absolved by the
189
- // read-only allowlist, and ancestor destruction outside the repository declared out of
190
- // observation scope (07c §2: the agent's own deny policy owns what no repo-scoped judge
191
- // can). The witness valve applies to it like any other registration.
192
- const protectedPaths = normalizeProtectedPaths({
193
- protectedPaths: config.protectedPaths ?? [],
194
- });
195
229
  // One witness predicate shared by every registration: a witness is a session-wide
196
230
  // permission the human granted, not a per-covenant one. Absent `witness` config leaves
197
231
  // this undefined, and no verdict can be witnessed open at all. The predicate receives
198
- // the transcript as its second argument from the dispatcher (CORE-04 seam), which is why
199
- // the transcript is injected below rather than captured here.
232
+ // the transcript as its second argument from the dispatcher, which is why the transcript
233
+ // is injected below rather than captured here.
200
234
  const witness = config.witness === undefined
201
235
  ? undefined
202
236
  : ttlWitness({
@@ -205,117 +239,44 @@ async function judgeHookCall(spec) {
205
239
  // Core passes the value through verbatim, so the conversion belongs to assembly.
206
240
  ttlMs: config.witness.ttlMinutes * 60_000,
207
241
  });
208
- // The judge bodies are the covenant package's dist executables — resolved through the
209
- // real package (never a test alias), so the session surface spawns the same judges the
210
- // commit surface does. An injected directory overrides that resolution: `createRequire`
211
- // is real Node resolution and always lands on the real build, which no fixture tree can
212
- // take a body away from.
213
- const covenantDist = spec.covenantDist ?? dirname(createRequire(import.meta.url).resolve('@polydeukes/covenant'));
214
- // Only the two unconditional registrations compose their paths here. The transcript-mod
215
- // and discipline bodies are composed inside the conditions that decide whether their
216
- // registrations exist at all proving a body this run will never spawn would close a
217
- // call over a file it was never going to use (CONFIG-06b §4.2 corollary).
218
- const selfModBody = provenBodyPath(covenantDist, 'self-mod-body.js');
219
- const shellModBody = provenBodyPath(covenantDist, 'shell-mod-body.js');
220
- const disciplines = config.disciplines ?? [];
221
- const pathArgs = protectedPaths.flatMap((path) => ['--protected-path', path]);
222
- const registrations = [
223
- {
224
- label: 'self-mod',
225
- protectedPaths,
226
- body: {
227
- command: process.execPath,
228
- args: [
229
- selfModBody,
230
- ...pathArgs,
231
- ...MUTATING_TOOLS.flatMap((tool) => ['--mutating-tool', tool]),
232
- ],
233
- },
234
- witness,
235
- },
236
- {
237
- label: 'shell-mod',
238
- protectedPaths,
239
- body: {
240
- command: process.execPath,
241
- args: [
242
- shellModBody,
243
- ...pathArgs,
244
- ...SHELL_TOOLS.flatMap((tool) => ['--shell-tool', tool]),
245
- ...COMMAND_ARGS.flatMap((arg) => ['--command-arg', arg]),
246
- ],
247
- },
248
- witness,
249
- },
250
- // The transcript's own registration (COVENANT-07c). Routing is the matches predicate,
251
- // never path mention, so the home directory cannot become a protected ancestor. No
252
- // transcript in the payload means nothing to protect — the valve and the context
253
- // family already forfeited on the same absence.
254
- ...(transcriptPath === undefined
255
- ? []
256
- : [
257
- transcriptModRegistration({
258
- transcriptPath,
259
- // The env value first, since that is what the judged shell expands `~` and
260
- // `$HOME` from. `homedir()` reads the same passwd entry bash falls back to when
261
- // HOME is unset, so a hook spawned without an environment (a service manager,
262
- // `env -i`) keeps judging the home spellings instead of silently going
263
- // absolute-only — an inert spelling closure looks identical to a passing call.
264
- home: process.env.HOME ?? homedir(),
265
- bodyCommand: process.execPath,
266
- bodyModulePath: provenBodyPath(covenantDist, 'transcript-mod-body.js'),
267
- shellTools: SHELL_TOOLS,
268
- commandArgs: COMMAND_ARGS,
269
- mutatingTools: MUTATING_TOOLS,
270
- witness,
271
- }),
272
- ]),
273
- // The body path is passed as a thunk, so the proof fires only where the compiler
274
- // actually composes a body. Entry count cannot stand in for that: an entry may compile
275
- // to a body-less skip (a `requirePrecedent` one whenever no transcript came with the
276
- // payload), and the compiler appends the body-less `shell-unjudgeable` backstop even
277
- // for zero entries — gating the call itself would drop that record and turn an
278
- // uncomputable shell write back into a silent pass, undoing COVENANT-10b.
279
- ...compileDisciplineRegistrations({
280
- disciplines,
281
- rootDir: spec.repoRoot,
282
- bodyCommand: process.execPath,
283
- bodyModulePath: () => provenBodyPath(covenantDist, 'discipline-body.js'),
284
- shellTools: SHELL_TOOLS,
285
- commandArgs: COMMAND_ARGS,
286
- witness,
287
- // Context-family evidence is evaluated here, at assembly: a spawned body cannot hold
288
- // a transcript, and passing a path would leak JSONL knowledge into covenant
289
- // (COVENANT-13 §4.4). The adapter brings the evaluator for its own `subagent`/`tool`
290
- // vocabulary; core owns `command`, which the compiler judges directly.
291
- transcript,
292
- evaluatePrecedent,
293
- }),
294
- ];
295
- // This assembly is versioned with the umbrella; the covenant dist it composes against is
296
- // resolved from the installation graph, so a workspace nobody rebuilt pairs a new
297
- // assembly with an old compiler — and an old compiler stores the body-path thunk itself
298
- // where a string belongs. `spawn` does not reject a non-string argv entry — it
299
- // stringifies it — so the judge would be spawned on the thunk's own source text, exit 1,
300
- // and be recorded as a VERDICT under a discipline's label. Assert the shape and let the
301
- // fail-closed catch answer instead.
302
- for (const registration of registrations) {
303
- if (registration.body !== undefined && typeof registration.body.args?.[0] !== 'string') {
304
- throw new Error(`covenant dist predates the lazy body-path convention (registration '${registration.label}') — run 'pnpm build'`);
305
- }
306
- }
242
+ // The judges are the covenant package's built barrel — resolved through the real
243
+ // package (never a test alias), so the session surface runs the same judges the commit
244
+ // surface does. An injected directory overrides that resolution, which is how a fixture
245
+ // reaches a dist that real Node resolution would never land on. Awaited HERE, before
246
+ // any registration is composed: a dist the barrel cannot load throws now, into the
247
+ // fail-closed catch, instead of leaving a half-judged table behind.
248
+ const covenantDist = spec.covenantDist ?? resolveCovenantDist();
249
+ const covenant = await loadCovenantModule(covenantDist);
250
+ // Assembled HERE, outside the dispatch seam: a judge takes its call set as an argument,
251
+ // so assembly needs no payload, and an assembly throw belongs to this function's own
252
+ // fail-closed catch — `hook` label, `covenant hook failed closed:` on stderr. Composed
253
+ // inside the dispatch closure it would land in `runAdapterPath`'s catch instead, which
254
+ // records the adapter's label and says nothing about what broke.
255
+ const registrations = assembleSessionRegistrations({
256
+ config,
257
+ rootDir: spec.repoRoot,
258
+ covenant,
259
+ transcriptPath,
260
+ transcript,
261
+ witness,
262
+ });
307
263
  return await runAdapterPath({
308
264
  rawPayload,
309
265
  telemetryPath: logPath,
310
- dispatch: (stdinPayload) => dispatchCovenants({ stdinPayload, registrations, telemetryPath: logPath, transcript }),
266
+ dispatch: (stdinPayload) => covenant.dispatchCovenants({
267
+ stdinPayload,
268
+ registrations,
269
+ telemetryPath: logPath,
270
+ transcript,
271
+ }),
311
272
  });
312
273
  }
313
274
  catch (error) {
314
275
  process.stderr.write(`covenant hook failed closed: ${error instanceof Error ? error.message : String(error)}\n`);
315
276
  // Honor the one-call-one-record invariant with a blocked record under the assembly's own
316
- // label (COVENANT-07 §4.3) — never a judge's, since no judge answered. `undefined` means
317
- // the failure landed before a path could even be composed (a non-string repoRoot), where
318
- // there is nowhere to write and nothing to attribute the row to.
277
+ // label — never a judge's, since no judge answered. `undefined` means the failure landed
278
+ // before a path could even be composed (a non-string repoRoot), where there is nowhere
279
+ // to write and nothing to attribute the row to.
319
280
  if (telemetryPath !== undefined) {
320
281
  appendRecordFailOpen(telemetryPath, { event: 'blocked', label: 'hook', subject: '-' });
321
282
  }
@@ -323,8 +284,7 @@ async function judgeHookCall(spec) {
323
284
  }
324
285
  }
325
286
  /**
326
- * The session-surface entry point: the post-hoc state comparison wrapped around the judgment
327
- * (COVENANT-14 §2-f).
287
+ * The session-surface entry point: the post-hoc state comparison wrapped around the judgment.
328
288
  *
329
289
  * The comparison sits OUTSIDE {@link judgeHookCall}'s fail-closed try on both ends. Inside
330
290
  * it, a comparison failure would become a blocked call — the opposite of a mechanism whose
@@ -1,57 +1,78 @@
1
1
  /**
2
- * `pdks covenant check` — the assembled commit-surface judgment runner (ADAPTER-git §4.3).
2
+ * `pdks covenant check` — the commit surface's composition root.
3
3
  *
4
- * This is the commit-surface counterpart of the session hook's composition root: the one
5
- * umbrella-owned place where the git adapter (staged-diff vocabulary), the covenant
6
- * dispatcher, and the config loader meet. Assembly order mirrors the session hook
7
- * loadConfig normalizeProtectedPaths collect/translate dispatchCovenants and the
8
- * judge bodies it spawns are the very same covenant dist executables, so a staged change
9
- * receives the same verdict a session tool call would (AC-4 same-judge).
4
+ * Assembly mirrors the session hook loadConfig normalizeProtectedPaths collect
5
+ * dispatchCovenants and spawns the same covenant dist bodies, so a change receives the
6
+ * verdict a session tool call would. Each change is dispatched as its own input so
7
+ * telemetry stays one row per file. The witness valve is a `/dev/tty` prompt that only
8
+ * the staged domain assembles; the other domains open no commit.
10
9
  *
11
- * Each staged change is dispatched as its own single-change input: one staged file is
12
- * the commit surface's analogue of one session tool call, so telemetry stays N:N (AC-6)
13
- * and `gain` reads a per-file subject rather than one opaque batch line.
14
- *
15
- * The valve is a TTY prompt (PRD §4.4 decision A): the injected `ttyPrompt` seam returns
16
- * the line a human typed at the terminal, compared against the config witness token in
17
- * FULL (COVENANT-15 — substring acceptance is forbidden). The seam's absence models a
18
- * non-interactive environment (CI, an AI-spawned git commit): no prompt, no witness
19
- * the valve is structurally reachable only by a human at a terminal, which is the
20
- * commit-surface translation of "only a human utterance opens the session valve". The
21
- * answer is cached so one commit prompts at most once, and nothing is ever persisted —
22
- * a state file would be an agent-forgeable surface (PRD §7).
10
+ * fail-closed: a missing config, an unbuilt body, or a collector failure exits 2 with one
11
+ * blocked record. An empty domain is an explicit pass with no records.
12
+ */
13
+ import type { CovenantRegistration } from '@polydeukes/covenant';
14
+ import { type CovenantModule } from './covenant-module.js';
15
+ import { loadConfig } from './load-config.js';
16
+ /**
17
+ * Which observation of the commit surface a run judges. Only the collector differs between
18
+ * them; the IR, the assembly, and the dispatcher are one path.
23
19
  *
24
- * fail-closed: a missing/invalid config, an unbuilt judge body, or a collector failure
25
- * exits 2 with one blocked record. The telemetry path is settled before the first failure
26
- * branch can be taken (ADAPTER-git-b §4.1), so the record has somewhere to land even when
27
- * the config that names its path never loaded. An empty staging area is an explicit pass
28
- * (nothing to judge — the dispatcher precedent of zero matches, zero records).
20
+ * `range` names its two refs. `ancestry: 'merge-base'` selects the `A...B` reading, whose
21
+ * base is the two refs' common ancestor rather than `A` itself; the adapter that owns the
22
+ * range grammar resolves it.
29
23
  */
30
- /** `runCovenantCheck` input (ADAPTER-git §4.3 — the contract covenant-check tests pin). */
24
+ export type CheckDomain = {
25
+ kind: 'staged';
26
+ } | {
27
+ kind: 'worktree';
28
+ } | {
29
+ kind: 'range';
30
+ base: string;
31
+ head: string;
32
+ ancestry?: 'merge-base';
33
+ };
34
+ /** `runCovenantCheck` input. */
31
35
  export type CovenantCheckSpec = {
32
36
  /** Repository root — config discovery and staged collection both anchor here. */
33
37
  repoRoot: string;
34
38
  /**
35
39
  * Overrides where telemetry is written (tests and assembly injection) — the first term
36
40
  * of the precedence, ahead of the config's `telemetry.logPath` and of the default this
37
- * runner settles before the config loads (ADAPTER-git-b §4.1). Absent, both of those
38
- * apply in that order.
41
+ * runner settles before the config loads. Absent, both of those apply in that order.
39
42
  */
40
43
  telemetryPath?: string;
41
44
  /** Overrides the resolved covenant dist directory (tests and assembly injection). */
42
45
  covenantDist?: string;
43
46
  /**
44
47
  * TTY valve seam: writes the given prompt and returns the line a human typed, or null
45
- * for no input. ABSENT means a non-TTY environment — the valve never opens (AC-3
46
- * human-only arming).
48
+ * for no input. ABSENT means a non-TTY environment — the valve never opens, which is
49
+ * what keeps it human-only.
47
50
  */
48
51
  ttyPrompt?: (prompt: string) => string | null;
52
+ /** Which observation to judge. ABSENT means `staged`. */
53
+ domain?: CheckDomain;
49
54
  };
55
+ /** {@link assembleCommitRegistrations} input — what the commit surface's assembly needs. */
56
+ export type CommitAssemblySpec = {
57
+ config: ReturnType<typeof loadConfig>['config'];
58
+ rootDir: string;
59
+ /**
60
+ * The covenant surface the registrations are built from — the module the caller loaded
61
+ * from the resolved dist, so what judges a change is what that dist carries, and what
62
+ * `explain` renders is what would judge it.
63
+ */
64
+ covenant: CovenantModule;
65
+ witness?: CovenantRegistration['witness'];
66
+ };
67
+ /**
68
+ * The commit surface's registration set — one assembly that the runner dispatches and
69
+ * `explain` renders.
70
+ */
71
+ export declare function assembleCommitRegistrations(spec: CommitAssemblySpec): CovenantRegistration[];
50
72
  /**
51
- * Judge the staged changes of `repoRoot` exactly as the session surface would
52
- * (ADAPTER-git §4.3). Async because the dispatcher spawns covenant bodies (CORE-01) —
53
- * a synchronous runner would mean reimplementing the judge, which the single-dispatcher
54
- * principle forbids.
73
+ * Judge one observation of `repoRoot` exactly as the session surface would — the staged
74
+ * diff by default, the working tree or a ref range on request. Async because the dispatcher
75
+ * spawns covenant bodies. An empty domain is an explicit pass: nothing to judge, no records.
55
76
  */
56
77
  export declare function runCovenantCheck(spec: CovenantCheckSpec): Promise<{
57
78
  exitCode: 0 | 2;