polydeukes 0.3.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.
- package/README.ko.md +3 -2
- package/README.md +10 -2
- package/dist/bin.d.ts +5 -6
- package/dist/bin.js +75 -38
- package/dist/claude-code-hook.d.ts +45 -15
- package/dist/claude-code-hook.js +245 -158
- package/dist/covenant-check.d.ts +58 -31
- package/dist/covenant-check.js +145 -179
- package/dist/covenant-module.d.ts +25 -0
- package/dist/covenant-module.js +34 -0
- package/dist/docs/configuration.md +44 -302
- package/dist/docs/installation.md +7 -3
- package/dist/docs/reference/adapter-git.md +17 -6
- package/dist/docs/reference/configuration.md +338 -0
- package/dist/docs/reference/core.md +8 -6
- package/dist/docs/reference/covenant.md +13 -5
- package/dist/docs/reference/polydeukes.md +103 -26
- package/dist/docs/troubleshooting.md +12 -2
- package/dist/docs-query.d.ts +10 -10
- package/dist/docs-query.js +20 -20
- package/dist/explain.d.ts +27 -0
- package/dist/explain.js +152 -0
- package/dist/index.d.ts +10 -14
- package/dist/index.js +9 -13
- package/dist/init-claude-code.d.ts +28 -17
- package/dist/init-claude-code.js +208 -40
- package/dist/load-config.d.ts +15 -16
- package/dist/load-config.js +21 -14
- package/dist/scaffold-project.d.ts +20 -20
- package/dist/scaffold-project.js +69 -30
- package/dist/schema/polydeukes.schema.json +229 -0
- package/package.json +8 -7
package/dist/covenant-check.js
CHANGED
|
@@ -1,57 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `pdks covenant check` — the
|
|
2
|
+
* `pdks covenant check` — the commit surface's composition root.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
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).
|
|
23
|
-
*
|
|
24
|
-
* fail-closed: a missing/invalid config, an unbuilt judge body, or a collector failure
|
|
25
|
-
* exits 2 with one blocked record when a telemetry path is known. An empty staging area
|
|
26
|
-
* is an explicit pass (nothing to judge — the dispatcher precedent of zero matches, zero
|
|
27
|
-
* records).
|
|
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.
|
|
28
12
|
*/
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import { appendRecord, normalizeProtectedPaths } from '@polydeukes/core';
|
|
34
|
-
import { compileDisciplineRegistrations, dispatchCovenants, } from '@polydeukes/covenant';
|
|
13
|
+
import { resolve } from 'node:path';
|
|
14
|
+
import { collectRangeChanges, collectStagedChanges, collectWorktreeChanges, covenantInputFromStagedChanges, resolveGitAdapterSettings, STAGED_DELETE, STAGED_WRITE, } from '@polydeukes/adapter-git';
|
|
15
|
+
import { appendRecordFailOpen, DEFAULT_TELEMETRY_LOG_PATH, normalizeProtectedPaths, } from '@polydeukes/core';
|
|
16
|
+
import { loadCovenantModule, resolveCovenantDist } from './covenant-module.js';
|
|
35
17
|
import { loadConfig } from './load-config.js';
|
|
36
18
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* telemetry row carries, so screen and log never disagree. The human reads what broke,
|
|
44
|
-
* on what, and how far one answer reaches. The verdict is cached: one commit, at most
|
|
45
|
-
* one prompt, full-token equality only — and the token itself is never printed, or
|
|
46
|
-
* typing it from memory would become copying it off the screen.
|
|
47
|
-
*
|
|
48
|
-
* Both comparison sides are trimmed, mirroring the session valve: `ttlWitness` trims the
|
|
49
|
-
* config token at assembly precisely because config validation accepts a padded value,
|
|
50
|
-
* and it compares the utterance's first line trimmed — without the same normalisation
|
|
51
|
-
* here, one padded token would open the session surface and permanently shut this one
|
|
52
|
-
* (PR #41 review). The cache latches CLOSED before the seam is consulted: a throwing
|
|
53
|
-
* seam must not retry on the next broken registration, or the prompt's own commit-wide
|
|
54
|
-
* promise becomes a lie (AC §5.3 one commit, at most one prompt).
|
|
19
|
+
* The TTY witness predicate, or undefined when no valve can exist (no witness configured
|
|
20
|
+
* or no TTY seam). It fires on the first registration that broke, names it from the
|
|
21
|
+
* dispatcher's context, and caches the answer: one commit, at most one prompt, full-token
|
|
22
|
+
* equality. Both sides are trimmed like the session valve, since config validation accepts
|
|
23
|
+
* a padded token. The cache latches closed before the seam is consulted so a throwing seam
|
|
24
|
+
* never re-prompts.
|
|
55
25
|
*/
|
|
56
26
|
function ttyWitnessValve(witness, ttyPrompt) {
|
|
57
27
|
if (witness === undefined || ttyPrompt === undefined)
|
|
@@ -71,150 +41,123 @@ function ttyWitnessValve(witness, ttyPrompt) {
|
|
|
71
41
|
};
|
|
72
42
|
}
|
|
73
43
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* that number alone), so the proof happens here, before the spawn. Producing the path
|
|
79
|
-
* and proving it are one step on purpose: a path that skipped the proof cannot be
|
|
80
|
-
* constructed, and only the bodies this surface actually composes are proven.
|
|
44
|
+
* One blocked record for a run that failed closed before any dispatch could judge.
|
|
45
|
+
* `appendRecordFailOpen` creates the missing `.polydeukes/` of a never-judged repository,
|
|
46
|
+
* and a telemetry failure never softens the exit. An undefined path (non-string
|
|
47
|
+
* `repoRoot`) leaves no root to write under.
|
|
81
48
|
*/
|
|
82
|
-
function provenBodyPath(distDir, fileName) {
|
|
83
|
-
const modulePath = join(distDir, fileName);
|
|
84
|
-
if (!existsSync(modulePath)) {
|
|
85
|
-
throw new Error(`judge body ${modulePath} is missing — run 'pnpm build' to rebuild it`);
|
|
86
|
-
}
|
|
87
|
-
return modulePath;
|
|
88
|
-
}
|
|
89
|
-
/** One blocked record for a run that failed closed before any dispatch could judge. */
|
|
90
49
|
function recordFailClosed(telemetryPath) {
|
|
91
50
|
if (telemetryPath === undefined)
|
|
92
51
|
return;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
subject: '-',
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
// A telemetry failure must never soften the blocking exit (session-hook precedent).
|
|
103
|
-
}
|
|
52
|
+
appendRecordFailOpen(telemetryPath, {
|
|
53
|
+
event: 'blocked',
|
|
54
|
+
label: 'covenant-check',
|
|
55
|
+
subject: '-',
|
|
56
|
+
});
|
|
104
57
|
}
|
|
105
58
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* a synchronous runner would mean reimplementing the judge, which the single-dispatcher
|
|
109
|
-
* principle forbids.
|
|
59
|
+
* The commit surface's registration set — one assembly that the runner dispatches and
|
|
60
|
+
* `explain` renders.
|
|
110
61
|
*/
|
|
111
|
-
export
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
62
|
+
export function assembleCommitRegistrations(spec) {
|
|
63
|
+
const { config, rootDir, covenant, witness } = spec;
|
|
64
|
+
const { protectedPaths: gitAdditivePaths } = resolveGitAdapterSettings(config.adapters?.git);
|
|
65
|
+
// Union of the common list and the git-additive one, common first so first-occurrence
|
|
66
|
+
// dedupe is deterministic. The session hook reads the common list alone.
|
|
67
|
+
const protectedPaths = normalizeProtectedPaths({
|
|
68
|
+
protectedPaths: [...(config.protectedPaths ?? []), ...gitAdditivePaths],
|
|
69
|
+
});
|
|
70
|
+
const disciplines = config.disciplines ?? [];
|
|
71
|
+
const registrations = [
|
|
72
|
+
covenant.selfModRegistration({
|
|
73
|
+
protectedPaths,
|
|
74
|
+
mutatingToolNames: [STAGED_WRITE, STAGED_DELETE],
|
|
75
|
+
witness,
|
|
76
|
+
}),
|
|
77
|
+
// No shell axis here, so command-family entries are left out. Context-family entries
|
|
78
|
+
// stay in: with no transcript the compiler gives them skip registrations, which record
|
|
79
|
+
// `skipped` on a match.
|
|
80
|
+
...covenant.compileDisciplineRegistrations({
|
|
81
|
+
disciplines: disciplines.filter((entry) => entry.forbidCommand === undefined),
|
|
82
|
+
rootDir,
|
|
83
|
+
shellTools: [],
|
|
84
|
+
commandArgs: [],
|
|
85
|
+
witness,
|
|
86
|
+
}),
|
|
87
|
+
];
|
|
88
|
+
return registrations;
|
|
89
|
+
}
|
|
90
|
+
/** One stage's failure disposition: the stderr line, the recorded row, and exit 2. */
|
|
91
|
+
function failClosed(telemetryPath, error) {
|
|
92
|
+
process.stderr.write(`covenant check failed closed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
93
|
+
recordFailClosed(telemetryPath);
|
|
94
|
+
return { exitCode: 2 };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Settle the telemetry path and load the config once, or fail closed. The provisional
|
|
98
|
+
* path is settled before the load so a config that never loads still has somewhere to
|
|
99
|
+
* write its blocked row; both terms use `resolve` so a relative `repoRoot` cannot send
|
|
100
|
+
* them to different files. The provisional term sits inside the try because `resolve`
|
|
101
|
+
* throws on a non-string `repoRoot`.
|
|
102
|
+
*/
|
|
103
|
+
function settleConfig(spec) {
|
|
104
|
+
let telemetryPath;
|
|
123
105
|
try {
|
|
124
|
-
|
|
106
|
+
telemetryPath = spec.telemetryPath ?? resolve(spec.repoRoot, DEFAULT_TELEMETRY_LOG_PATH);
|
|
107
|
+
const { config } = loadConfig(spec.repoRoot);
|
|
108
|
+
telemetryPath = spec.telemetryPath ?? resolve(spec.repoRoot, config.telemetry.logPath);
|
|
109
|
+
return { settled: true, telemetryPath, config };
|
|
125
110
|
}
|
|
126
111
|
catch (error) {
|
|
127
|
-
|
|
128
|
-
recordFailClosed(telemetryPath);
|
|
129
|
-
return { exitCode: 2 };
|
|
112
|
+
return { settled: false, ...failClosed(telemetryPath, error) };
|
|
130
113
|
}
|
|
131
|
-
|
|
132
|
-
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Collect the changes of one domain. The three collectors return the same shape, so
|
|
117
|
+
* everything downstream of this dispatch is one path.
|
|
118
|
+
*/
|
|
119
|
+
function collectDomain(repoRoot, domain) {
|
|
120
|
+
if (domain.kind === 'worktree')
|
|
121
|
+
return collectWorktreeChanges(repoRoot);
|
|
122
|
+
if (domain.kind === 'range') {
|
|
123
|
+
const separator = domain.ancestry === 'merge-base' ? '...' : '..';
|
|
124
|
+
return collectRangeChanges(repoRoot, `${domain.base}${separator}${domain.head}`);
|
|
133
125
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
126
|
+
return collectStagedChanges(repoRoot);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Assemble the registrations and dispatch every collected change. Any throw here (an
|
|
130
|
+
* unbuilt dist, a registration-build failure) is unjudgeable: block and leave one record.
|
|
131
|
+
*/
|
|
132
|
+
async function judgeChanges(spec, domain, telemetryPath, config, changes) {
|
|
139
133
|
try {
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// judges the session hook does. An injected directory overrides that resolution:
|
|
153
|
-
// `createRequire` is real Node resolution and always lands on the real build, which
|
|
154
|
-
// no fixture can take a body away from.
|
|
155
|
-
const covenantDist = spec.covenantDist ?? dirname(createRequire(import.meta.url).resolve('@polydeukes/covenant'));
|
|
156
|
-
// Under advise the TTY valve is structurally absent (CONFIG-06 §4.6): a verdict
|
|
157
|
-
// already passes, so there is nothing to witness and the prompt must never fire.
|
|
158
|
-
const witness = enforce === 'advise' ? undefined : ttyWitnessValve(config.witness, spec.ttyPrompt);
|
|
159
|
-
const disciplines = config.disciplines ?? [];
|
|
160
|
-
const registrations = [
|
|
161
|
-
{
|
|
162
|
-
label: 'self-mod',
|
|
163
|
-
protectedPaths,
|
|
164
|
-
body: {
|
|
165
|
-
command: process.execPath,
|
|
166
|
-
args: [
|
|
167
|
-
provenBodyPath(covenantDist, 'self-mod-body.js'),
|
|
168
|
-
...protectedPaths.flatMap((path) => ['--protected-path', path]),
|
|
169
|
-
...[STAGED_WRITE, STAGED_DELETE].flatMap((tool) => ['--mutating-tool', tool]),
|
|
170
|
-
],
|
|
171
|
-
},
|
|
172
|
-
witness,
|
|
173
|
-
},
|
|
174
|
-
// Command-family entries are excluded: the commit surface has no shell axis (a
|
|
175
|
-
// staged diff carries no commands), so registering them would be spawn waste by
|
|
176
|
-
// design (PRD §2) — a vacuous exclusion, hence recorded nowhere. Path and delta
|
|
177
|
-
// families judge the staged fileChanges as-is.
|
|
178
|
-
//
|
|
179
|
-
// Context-family entries are NOT filtered out any more. No transcript is injected
|
|
180
|
-
// here, so the compiler gives them skip registrations, and a skip records one
|
|
181
|
-
// `skipped` exactly when its trigger matches a staged change (COVENANT-13 §4.5).
|
|
182
|
-
// The commit surface stopped being a special case: an absent evidence channel gets
|
|
183
|
-
// the same disposition on both surfaces, and the scope gate comes free with the
|
|
184
|
-
// routing every registration already carries.
|
|
185
|
-
//
|
|
186
|
-
// The body path is passed as a thunk, so the proof fires only where the compiler
|
|
187
|
-
// actually composes a body (CONFIG-06b §4.2 corollary). Entry count cannot stand in
|
|
188
|
-
// for that: an entry may compile to a body-less skip — every `requirePrecedent` one
|
|
189
|
-
// does here, since this surface injects neither transcript nor evaluator — and the
|
|
190
|
-
// compiler appends the body-less `shell-unjudgeable` backstop even for zero entries,
|
|
191
|
-
// so gating the call itself would drop that record.
|
|
192
|
-
...compileDisciplineRegistrations({
|
|
193
|
-
disciplines: disciplines.filter((entry) => entry.forbidCommand === undefined),
|
|
194
|
-
rootDir: spec.repoRoot,
|
|
195
|
-
bodyCommand: process.execPath,
|
|
196
|
-
bodyModulePath: () => provenBodyPath(covenantDist, 'discipline-body.js'),
|
|
197
|
-
shellTools: [],
|
|
198
|
-
commandArgs: [],
|
|
199
|
-
witness,
|
|
200
|
-
}),
|
|
201
|
-
];
|
|
202
|
-
// The commit surface resolves the compiler through the installed package, so a
|
|
203
|
-
// workspace whose dist predates the lazy body-path convention hands back the thunk
|
|
204
|
-
// itself where a string belongs. `spawn` stringifies rather than rejects it, which
|
|
205
|
-
// would spawn the judge on the thunk's own source text and record the exit 1 as a
|
|
206
|
-
// verdict under a discipline's label — the confusion this ticket removes, arriving
|
|
207
|
-
// through the build-skew door. Assert the shape and let the fail-closed catch answer.
|
|
208
|
-
for (const registration of registrations) {
|
|
209
|
-
if (registration.body !== undefined && typeof registration.body.args?.[0] !== 'string') {
|
|
210
|
-
throw new Error(`covenant dist predates the lazy body-path convention (registration '${registration.label}') — run 'pnpm build'`);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
134
|
+
// Inside the try so an invalid adapter namespace fails closed.
|
|
135
|
+
const { enforce } = resolveGitAdapterSettings(config.adapters?.git);
|
|
136
|
+
// Real Node resolution of the covenant package, so the commit surface runs the same
|
|
137
|
+
// judges the session hook does; tests inject a directory instead. Awaited before any
|
|
138
|
+
// registration is composed, so a dist the barrel cannot load fails the run closed here
|
|
139
|
+
// rather than leaving a half-judged table behind.
|
|
140
|
+
const covenantDist = spec.covenantDist ?? resolveCovenantDist();
|
|
141
|
+
const covenant = await loadCovenantModule(covenantDist);
|
|
142
|
+
// No valve under advise (nothing to witness) and none outside `staged`.
|
|
143
|
+
const witness = enforce === 'advise' || domain.kind !== 'staged'
|
|
144
|
+
? undefined
|
|
145
|
+
: ttyWitnessValve(config.witness, spec.ttyPrompt);
|
|
213
146
|
let blocked = false;
|
|
214
147
|
let advisedCount = 0;
|
|
148
|
+
// Assembled ONCE for the run, not per change: a judge takes its call set as an argument,
|
|
149
|
+
// so the table is payload-free. Recompiling per file would repeat every compile-time
|
|
150
|
+
// side effect — the stderr line a config-faulted discipline names itself with would
|
|
151
|
+
// print once per staged file rather than once.
|
|
152
|
+
const registrations = assembleCommitRegistrations({
|
|
153
|
+
config,
|
|
154
|
+
rootDir: spec.repoRoot,
|
|
155
|
+
covenant,
|
|
156
|
+
witness,
|
|
157
|
+
});
|
|
215
158
|
for (const change of changes) {
|
|
216
159
|
const input = covenantInputFromStagedChanges([change]);
|
|
217
|
-
const { exitCode, results } = await dispatchCovenants({
|
|
160
|
+
const { exitCode, results } = await covenant.dispatchCovenants({
|
|
218
161
|
stdinPayload: JSON.stringify(input),
|
|
219
162
|
registrations,
|
|
220
163
|
telemetryPath,
|
|
@@ -225,14 +168,37 @@ export async function runCovenantCheck(spec) {
|
|
|
225
168
|
blocked = true;
|
|
226
169
|
advisedCount += results.filter((result) => result.event === 'advised').length;
|
|
227
170
|
}
|
|
171
|
+
// Names no level: surface-level and entry-level advice mix in one run, so the commit's
|
|
172
|
+
// fate is read from the run.
|
|
228
173
|
if (advisedCount > 0) {
|
|
229
|
-
|
|
174
|
+
const outcome = blocked ? 'commit blocked by another verdict' : 'commit allowed';
|
|
175
|
+
process.stderr.write(`covenant advisory: ${advisedCount} verdict(s) recorded as advised, ${outcome}\n`);
|
|
230
176
|
}
|
|
231
177
|
return { exitCode: blocked ? 2 : 0 };
|
|
232
178
|
}
|
|
233
179
|
catch (error) {
|
|
234
|
-
|
|
235
|
-
recordFailClosed(telemetryPath);
|
|
236
|
-
return { exitCode: 2 };
|
|
180
|
+
return failClosed(telemetryPath, error);
|
|
237
181
|
}
|
|
238
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Judge one observation of `repoRoot` exactly as the session surface would — the staged
|
|
185
|
+
* diff by default, the working tree or a ref range on request. Async because the dispatcher
|
|
186
|
+
* spawns covenant bodies. An empty domain is an explicit pass: nothing to judge, no records.
|
|
187
|
+
*/
|
|
188
|
+
export async function runCovenantCheck(spec) {
|
|
189
|
+
const settlement = settleConfig(spec);
|
|
190
|
+
if (!settlement.settled)
|
|
191
|
+
return { exitCode: settlement.exitCode };
|
|
192
|
+
const { telemetryPath, config } = settlement;
|
|
193
|
+
const domain = spec.domain ?? { kind: 'staged' };
|
|
194
|
+
let changes;
|
|
195
|
+
try {
|
|
196
|
+
changes = collectDomain(spec.repoRoot, domain);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
return failClosed(telemetryPath, error);
|
|
200
|
+
}
|
|
201
|
+
if (changes.length === 0)
|
|
202
|
+
return { exitCode: 0 };
|
|
203
|
+
return judgeChanges(spec, domain, telemetryPath, config, changes);
|
|
204
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The covenant package as a resolved artifact — the existence proof both composition roots
|
|
3
|
+
* share.
|
|
4
|
+
*
|
|
5
|
+
* What the roots prove is the package IMPORT itself. The barrel is eager: a dist missing one
|
|
6
|
+
* of the modules it references throws on import, before any assembly can compose a
|
|
7
|
+
* registration, and the surface's own fail-closed catch records that as one `blocked` row. A
|
|
8
|
+
* partially loaded judge set has no representation here — an ESM import either fully succeeds
|
|
9
|
+
* or throws.
|
|
10
|
+
*
|
|
11
|
+
* The `covenantDist` seam selects WHICH dist is imported, so a fixture can inject a gutted
|
|
12
|
+
* mirror where real Node resolution would always land on the healthy build.
|
|
13
|
+
*/
|
|
14
|
+
import type * as covenant from '@polydeukes/covenant';
|
|
15
|
+
/** The covenant surface both roots assemble against. */
|
|
16
|
+
export type CovenantModule = typeof covenant;
|
|
17
|
+
/** Where real Node resolution puts the covenant package's built barrel. */
|
|
18
|
+
export declare function resolveCovenantDist(): string;
|
|
19
|
+
/**
|
|
20
|
+
* Import the covenant barrel from `distDir`, naming the recovery command when it will not
|
|
21
|
+
* load. The message carries the loader's own text, which names the module that is missing;
|
|
22
|
+
* a reader locked out by an unbuilt or half-built dist needs both that name and the one
|
|
23
|
+
* command that fixes it.
|
|
24
|
+
*/
|
|
25
|
+
export declare function loadCovenantModule(distDir: string): Promise<CovenantModule>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The covenant package as a resolved artifact — the existence proof both composition roots
|
|
3
|
+
* share.
|
|
4
|
+
*
|
|
5
|
+
* What the roots prove is the package IMPORT itself. The barrel is eager: a dist missing one
|
|
6
|
+
* of the modules it references throws on import, before any assembly can compose a
|
|
7
|
+
* registration, and the surface's own fail-closed catch records that as one `blocked` row. A
|
|
8
|
+
* partially loaded judge set has no representation here — an ESM import either fully succeeds
|
|
9
|
+
* or throws.
|
|
10
|
+
*
|
|
11
|
+
* The `covenantDist` seam selects WHICH dist is imported, so a fixture can inject a gutted
|
|
12
|
+
* mirror where real Node resolution would always land on the healthy build.
|
|
13
|
+
*/
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { pathToFileURL } from 'node:url';
|
|
17
|
+
/** Where real Node resolution puts the covenant package's built barrel. */
|
|
18
|
+
export function resolveCovenantDist() {
|
|
19
|
+
return join(createRequire(import.meta.url).resolve('@polydeukes/covenant'), '..');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Import the covenant barrel from `distDir`, naming the recovery command when it will not
|
|
23
|
+
* load. The message carries the loader's own text, which names the module that is missing;
|
|
24
|
+
* a reader locked out by an unbuilt or half-built dist needs both that name and the one
|
|
25
|
+
* command that fixes it.
|
|
26
|
+
*/
|
|
27
|
+
export async function loadCovenantModule(distDir) {
|
|
28
|
+
try {
|
|
29
|
+
return (await import(pathToFileURL(join(distDir, 'index.js')).href));
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw new Error(`the covenant judges could not be loaded from ${distDir} — run 'pnpm build' to rebuild them: ${error instanceof Error ? error.message : String(error)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|