polydeukes 0.6.1 → 0.8.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.
Files changed (108) hide show
  1. package/README.ko.md +16 -28
  2. package/README.md +16 -28
  3. package/dist/baseline.d.ts +37 -1
  4. package/dist/baseline.js +68 -1
  5. package/dist/bin.d.ts +3 -4
  6. package/dist/bin.js +73 -90
  7. package/dist/covenant/bash-line.d.ts +130 -0
  8. package/dist/covenant/bash-line.js +566 -0
  9. package/dist/covenant/declaration-engine.d.ts +108 -0
  10. package/dist/covenant/declaration-engine.js +415 -0
  11. package/dist/covenant/discipline.d.ts +109 -0
  12. package/dist/covenant/discipline.js +667 -0
  13. package/dist/covenant/dispatch.d.ts +156 -0
  14. package/dist/covenant/dispatch.js +250 -0
  15. package/dist/covenant/extract-steps.d.ts +122 -0
  16. package/dist/covenant/extract-steps.js +0 -0
  17. package/dist/covenant/mention.d.ts +117 -0
  18. package/dist/covenant/mention.js +209 -0
  19. package/dist/covenant/module.d.ts +25 -0
  20. package/dist/covenant/module.js +23 -0
  21. package/dist/covenant/mutation-rules.d.ts +40 -0
  22. package/dist/covenant/mutation-rules.js +146 -0
  23. package/dist/covenant/relations.d.ts +46 -0
  24. package/dist/covenant/relations.js +68 -0
  25. package/dist/covenant/run-covenant.d.ts +95 -0
  26. package/dist/covenant/run-covenant.js +164 -0
  27. package/dist/covenant/self-mod.d.ts +55 -0
  28. package/dist/covenant/self-mod.js +84 -0
  29. package/dist/covenant/shell-evidence.d.ts +38 -0
  30. package/dist/covenant/shell-evidence.js +247 -0
  31. package/dist/covenant/shell-mod.d.ts +84 -0
  32. package/dist/covenant/shell-mod.js +213 -0
  33. package/dist/covenant/supply.d.ts +54 -0
  34. package/dist/covenant/supply.js +50 -0
  35. package/dist/covenant/transcript-mod.d.ts +60 -0
  36. package/dist/covenant/transcript-mod.js +254 -0
  37. package/dist/covenant/ttl-witness.d.ts +47 -0
  38. package/dist/covenant/ttl-witness.js +80 -0
  39. package/dist/covenant-check.d.ts +74 -41
  40. package/dist/covenant-check.js +372 -129
  41. package/dist/diff-ir.d.ts +28 -0
  42. package/dist/diff-ir.js +310 -0
  43. package/dist/docs/README.ko.md +10 -9
  44. package/dist/docs/README.md +10 -9
  45. package/dist/docs/catalog.json +76 -28
  46. package/dist/docs/concepts/judgment.ko.md +2 -2
  47. package/dist/docs/concepts/judgment.md +2 -2
  48. package/dist/docs/how-to/configure-project.ko.md +16 -16
  49. package/dist/docs/how-to/configure-project.md +15 -12
  50. package/dist/docs/how-to/connect-surfaces.ko.md +67 -34
  51. package/dist/docs/how-to/connect-surfaces.md +67 -34
  52. package/dist/docs/how-to/write-disciplines.ko.md +64 -7
  53. package/dist/docs/how-to/write-disciplines.md +65 -7
  54. package/dist/docs/index.json +744 -362
  55. package/dist/docs/reference/cli/covenant-check.ko.md +112 -65
  56. package/dist/docs/reference/cli/covenant-check.md +114 -59
  57. package/dist/docs/reference/cli/docs.ko.md +2 -2
  58. package/dist/docs/reference/cli/docs.md +2 -2
  59. package/dist/docs/reference/cli/explain.ko.md +20 -15
  60. package/dist/docs/reference/cli/explain.md +22 -15
  61. package/dist/docs/reference/cli/init.ko.md +91 -52
  62. package/dist/docs/reference/cli/init.md +81 -55
  63. package/dist/docs/reference/configuration/index.ko.md +182 -71
  64. package/dist/docs/reference/configuration/index.md +188 -76
  65. package/dist/docs/reference/packages/adapter-claude-code.ko.md +39 -11
  66. package/dist/docs/reference/packages/adapter-claude-code.md +40 -14
  67. package/dist/docs/reference/packages/adapter-codex.ko.md +99 -0
  68. package/dist/docs/reference/packages/adapter-codex.md +100 -0
  69. package/dist/docs/reference/packages/adapter-grok.ko.md +81 -0
  70. package/dist/docs/reference/packages/adapter-grok.md +83 -0
  71. package/dist/docs/reference/packages/core.ko.md +50 -4
  72. package/dist/docs/reference/packages/core.md +55 -6
  73. package/dist/docs/reference/packages/polydeukes.ko.md +139 -77
  74. package/dist/docs/reference/packages/polydeukes.md +147 -81
  75. package/dist/docs/reference/packages/sdk-ts.ko.md +164 -0
  76. package/dist/docs/reference/packages/sdk-ts.md +167 -0
  77. package/dist/docs/troubleshooting.ko.md +51 -19
  78. package/dist/docs/troubleshooting.md +53 -17
  79. package/dist/docs/tutorials/first-judgment.ko.md +6 -4
  80. package/dist/docs/tutorials/first-judgment.md +6 -4
  81. package/dist/explain.d.ts +1 -1
  82. package/dist/explain.js +37 -22
  83. package/dist/load-config.d.ts +25 -1
  84. package/dist/load-config.js +21 -3
  85. package/dist/pre-state-reader.d.ts +1 -1
  86. package/dist/pre-state-reader.js +1 -1
  87. package/dist/scaffold-project.d.ts +2 -2
  88. package/dist/scaffold-project.js +12 -4
  89. package/dist/schema/polydeukes.schema.json +12 -0
  90. package/dist/worktree-reader.d.ts +19 -0
  91. package/dist/worktree-reader.js +30 -0
  92. package/package.json +5 -18
  93. package/dist/claude-code-hook.d.ts +0 -79
  94. package/dist/claude-code-hook.js +0 -372
  95. package/dist/claude-code.d.ts +0 -6
  96. package/dist/claude-code.js +0 -6
  97. package/dist/covenant-module.d.ts +0 -25
  98. package/dist/covenant-module.js +0 -42
  99. package/dist/docs/reference/packages/adapter-git.ko.md +0 -101
  100. package/dist/docs/reference/packages/adapter-git.md +0 -108
  101. package/dist/docs/reference/packages/covenant.ko.md +0 -115
  102. package/dist/docs/reference/packages/covenant.md +0 -123
  103. package/dist/index.d.ts +0 -22
  104. package/dist/index.js +0 -21
  105. package/dist/init-claude-code.d.ts +0 -52
  106. package/dist/init-claude-code.js +0 -586
  107. package/dist/init-grok.d.ts +0 -51
  108. package/dist/init-grok.js +0 -242
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Path-routing dispatcher — routes an input to the covenant bodies whose protected paths
3
+ * it mentions.
4
+ *
5
+ * Matching is a pure path-mention core (zero I/O); execution reuses {@link runCovenant}
6
+ * (the sole judgment wrapper) and {@link appendRecordFailOpen} (the sole log seam). The
7
+ * dispatcher parses the payload once, to decide routing and to hand the judge thunks the
8
+ * one parsed call set they judge.
9
+ */
10
+ import { type CanonicalTranscript, type CovenantInput, type DispatchOutcome, type EnforceLevel, type SkipReason } from '@polydeukes/core';
11
+ import { type JudgeOutcome } from './run-covenant.ts';
12
+ /**
13
+ * `WitnessPredicate` — the valve a registration offers, asked after a blocking judgment:
14
+ * the observation, the transcript, and the registration's own label with the matched subject.
15
+ */
16
+ export type WitnessPredicate = (input: CovenantInput, transcript: CanonicalTranscript, context: {
17
+ label: string;
18
+ subject: string;
19
+ }) => boolean;
20
+ /**
21
+ * `CovenantRegistration` — one registered covenant.
22
+ *
23
+ * `protectedPaths` are literal path strings (the output shape of normalization, not
24
+ * globs); an empty array never matches, and empty-string entries are ignored (an empty
25
+ * `''` would match every input). `body` is the in-process judge: assembly binds its
26
+ * options into it, and the dispatcher supplies the parsed call set as its argument when a
27
+ * protected path is mentioned. Assembly therefore never needs the payload, so one compiled
28
+ * registration set judges every call.
29
+ *
30
+ * `witness`, when present, is consulted only after a *matched* registration's body has run
31
+ * and broken, receiving the injected transcript seam as its second argument and a
32
+ * `{ label, subject }` context naming what broke as its third: a `true` return relaxes
33
+ * that block (measured as `witnessed`).
34
+ *
35
+ * `matches`, when present, replaces path-mention routing with a content predicate: a
36
+ * non-null return routes (the string becomes the telemetry subject), null does not, and a
37
+ * throw is a fail-closed match with subject `'-'`.
38
+ *
39
+ * A registration carries EITHER a `body` or a `skip`. `skip` means assembly could not
40
+ * produce a judgeable body — the evidence channel is absent, or the declared evidence
41
+ * vocabulary could not be resolved — so a match records one `skipped` and upholds instead
42
+ * of judging. Judging it anyway would block every matched input with no legitimate pass
43
+ * path; throwing at assembly would take down every sibling registration and the witness
44
+ * valve with it. `reason` is the sentence the author reads; `kind` is the token the row
45
+ * carries, so the measurement separates an environment fact from a config fault.
46
+ *
47
+ * `enforce` is the AUTHOR's level for this one registration, distinct from the observer's
48
+ * dispatch-wide level; absence means the registration inherits whatever the dispatch
49
+ * carries.
50
+ *
51
+ * `sources` are what outside the target this registration's declaration names, each element
52
+ * carrying its own kind — a repo-relative file, a channel the surface supplies, or the
53
+ * session's conversation history. They are the supply layer's only input; a family that
54
+ * names nothing carries no key.
55
+ */
56
+ export type CovenantRegistration = {
57
+ label: string;
58
+ protectedPaths: string[];
59
+ enforce?: EnforceLevel;
60
+ sources?: readonly ({
61
+ name: string;
62
+ file: string;
63
+ } | {
64
+ name: string;
65
+ sidecar: true;
66
+ } | {
67
+ name: string;
68
+ transcript: true;
69
+ })[];
70
+ witness?: WitnessPredicate;
71
+ matches?: (input: CovenantInput) => string | null;
72
+ } & ({
73
+ body: (input: CovenantInput) => Promise<JudgeOutcome>;
74
+ skip?: never;
75
+ } | {
76
+ body?: never;
77
+ skip: {
78
+ reason: string;
79
+ kind: SkipReason;
80
+ };
81
+ });
82
+ /**
83
+ * Match registrations against a {@link CovenantInput} by path mention (pure).
84
+ *
85
+ * A registration matches when any of its `protectedPaths` is an ancestor/descendant/equal of
86
+ * a path candidate. Candidates come from two places: every string value reachable at any
87
+ * depth inside `input.toolCalls[].args`, read as quote-aware tokenizer words so a
88
+ * quote-split write still routes, and each call's proven `fileChange` path, taken whole
89
+ * because an adapter already resolved it. A call carrying only the latter is the ordinary
90
+ * shape where a host names its target in a form the adapter must parse — the patch text of
91
+ * one edit rather than a path argument. An unread span with a non-empty `protectedPaths`
92
+ * routes fail-closed (the registration matches on its first protected path) rather than
93
+ * silently miss. `subagentSpawns` and `userMessages` never participate. `mentionedPath` is
94
+ * the first protected path (in array order) that mentions. Result preserves registration
95
+ * order, at most one entry per registration.
96
+ *
97
+ * A registration carrying a `matches` predicate routes on it exclusively, path mention
98
+ * skipped for it: non-null return → included with that string as `mentionedPath`; null →
99
+ * skipped; a throw → fail-closed inclusion with `'-'` (caught per registration, never
100
+ * bubbling to the dispatcher-level catch).
101
+ */
102
+ export declare function matchRegistrations(input: CovenantInput, registrations: CovenantRegistration[]): {
103
+ registration: CovenantRegistration;
104
+ mentionedPath: string;
105
+ routingFailed?: boolean;
106
+ }[];
107
+ /** A registration assembled by a meta-covenant: one that always carries a judgeable body. */
108
+ export type MetaCovenantRegistration = CovenantRegistration & {
109
+ body: NonNullable<CovenantRegistration['body']>;
110
+ };
111
+ /** `dispatchCovenants` input — the payload, the registration table, and the dispatch posture. */
112
+ export type DispatchCovenantsSpec = {
113
+ stdinPayload: string;
114
+ registrations: CovenantRegistration[];
115
+ telemetryPath: string;
116
+ dispatcherLabel?: string;
117
+ transcript?: CanonicalTranscript;
118
+ enforce?: EnforceLevel;
119
+ world?: CovenantInput['world'];
120
+ };
121
+ /**
122
+ * Dispatch covenants for a stdin payload.
123
+ *
124
+ * Fail-closed: an unjudgeable payload — unparseable JSON, or a parseable one whose
125
+ * structure defeats the matching traversal (a null toolCalls element, adversarially deep
126
+ * nesting) — yields exitCode 2, judges nothing, and appends exactly one `blocked` record
127
+ * for the dispatcher itself. "Cannot judge" means block; it never means throw, because an
128
+ * uncaught rejection exits the hook with a non-blocking code and becomes a bypass vector.
129
+ * On matches, every matched registration runs sequentially via {@link runCovenant}
130
+ * (run-all, no short-circuit); the verdict is `2` if any body blocks, else `0`. No matches
131
+ * passes vacuously with zero judgments and zero telemetry.
132
+ *
133
+ * World: `spec.world` is the supply layer's result, attached to the parsed input before
134
+ * routing so the body, the routing predicate, and the valve all judge one world. A world
135
+ * the payload itself carries stands only when the spec names none.
136
+ *
137
+ * Witness: the dispatcher only BINDS the witness's arguments — the parsed input, the
138
+ * injected `spec.transcript` (`noopTranscript` when omitted), and a `{ label, subject }`
139
+ * context naming the registration and its matched path — and hands the thunk to
140
+ * {@link runCovenant}, which consults it after the judgment and only when the body's
141
+ * outcome translated to `blocked`. So the body always runs: a matched registration that
142
+ * upholds is never witnessed, and a `true` return relaxes a real break into
143
+ * `0` / `witnessed`. A predicate that throws opens nothing: an uncertain witness never
144
+ * leaks toward fail-open.
145
+ *
146
+ * Enforce: the level has two owners — `spec.enforce` is the observer's posture for the
147
+ * whole dispatch, `registration.enforce` the author's for one entry — and the dispatcher
148
+ * composes them per registration with the lenient side winning, then threads the effective
149
+ * level into {@link runCovenant}, where the translation table lives. Lenient-wins keeps an
150
+ * explicit `block` entry from raising a surface the observer lowered, and lets one entry
151
+ * lower itself under a block surface. The dispatcher's own fail-closed is outside that axis
152
+ * and outside the valve too (nothing judged, so no verdict to relax). Each results entry
153
+ * surfaces the telemetry `event` the wrapper recorded, never a recomputed one — the valve
154
+ * is impure, and a recompute would consult it twice for one verdict.
155
+ */
156
+ export declare function dispatchCovenants(spec: DispatchCovenantsSpec): Promise<DispatchOutcome>;
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Path-routing dispatcher — routes an input to the covenant bodies whose protected paths
3
+ * it mentions.
4
+ *
5
+ * Matching is a pure path-mention core (zero I/O); execution reuses {@link runCovenant}
6
+ * (the sole judgment wrapper) and {@link appendRecordFailOpen} (the sole log seam). The
7
+ * dispatcher parses the payload once, to decide routing and to hand the judge thunks the
8
+ * one parsed call set they judge.
9
+ */
10
+ import { appendRecordFailOpen, EXIT_BREAK_BLOCKING, EXIT_UPHOLD, noopTranscript, parseInput, } from '@polydeukes/core';
11
+ import { tokenizeCommandLine } from './bash-line.js';
12
+ import { pathCandidates, pathMatchesProtected, provenChangePath } from './mention.js';
13
+ import { runCovenant } from './run-covenant.js';
14
+ /**
15
+ * Collect path candidates from every string value inside `value`. Each string is tokenized
16
+ * quote-aware (via the shared tokenizer) so quote/escape splits collapse to the word the
17
+ * shell would see; each resulting word text is a candidate. An unread span surfaces as
18
+ * `failed = true` so the caller can route fail-closed rather than fall back to a
19
+ * raw-substring scan — the candidates the same line's read commands contribute add routing
20
+ * precision, they never withdraw that flag.
21
+ */
22
+ function collectPathCandidates(value) {
23
+ const candidates = [];
24
+ let failed = false;
25
+ const walk = (node) => {
26
+ if (typeof node === 'string') {
27
+ const result = tokenizeCommandLine(node);
28
+ if (result.unread.length > 0)
29
+ failed = true;
30
+ for (const command of result.commands) {
31
+ // Split each tokenized word the same way mentionsPath does, so a path fused to
32
+ // another lexeme (`--dest=path`) still surfaces as its own candidate.
33
+ for (const word of command.words)
34
+ candidates.push(...pathCandidates(word.text));
35
+ for (const redirect of command.redirects) {
36
+ candidates.push(...pathCandidates(redirect.target.text));
37
+ }
38
+ }
39
+ return;
40
+ }
41
+ if (Array.isArray(node)) {
42
+ for (const item of node)
43
+ walk(item);
44
+ return;
45
+ }
46
+ if (typeof node === 'object' && node !== null) {
47
+ for (const item of Object.values(node))
48
+ walk(item);
49
+ }
50
+ };
51
+ walk(value);
52
+ return { candidates, failed };
53
+ }
54
+ /**
55
+ * Match registrations against a {@link CovenantInput} by path mention (pure).
56
+ *
57
+ * A registration matches when any of its `protectedPaths` is an ancestor/descendant/equal of
58
+ * a path candidate. Candidates come from two places: every string value reachable at any
59
+ * depth inside `input.toolCalls[].args`, read as quote-aware tokenizer words so a
60
+ * quote-split write still routes, and each call's proven `fileChange` path, taken whole
61
+ * because an adapter already resolved it. A call carrying only the latter is the ordinary
62
+ * shape where a host names its target in a form the adapter must parse — the patch text of
63
+ * one edit rather than a path argument. An unread span with a non-empty `protectedPaths`
64
+ * routes fail-closed (the registration matches on its first protected path) rather than
65
+ * silently miss. `subagentSpawns` and `userMessages` never participate. `mentionedPath` is
66
+ * the first protected path (in array order) that mentions. Result preserves registration
67
+ * order, at most one entry per registration.
68
+ *
69
+ * A registration carrying a `matches` predicate routes on it exclusively, path mention
70
+ * skipped for it: non-null return → included with that string as `mentionedPath`; null →
71
+ * skipped; a throw → fail-closed inclusion with `'-'` (caught per registration, never
72
+ * bubbling to the dispatcher-level catch).
73
+ */
74
+ export function matchRegistrations(input, registrations) {
75
+ const { candidates, failed } = collectPathCandidates(input.toolCalls.map((call) => call.args));
76
+ // A proven change path joins the candidates directly, untokenized: it is a path the
77
+ // adapter already resolved, not a line to be read for one, and tokenizing it would split
78
+ // a path containing a space into words that name nothing. Without this a call whose only
79
+ // evidence is its `fileChange` routes nowhere — the judge bodies read that evidence, so
80
+ // the input would be refused before the layer that can settle it.
81
+ for (const call of input.toolCalls) {
82
+ const proven = provenChangePath(call);
83
+ if (proven !== null)
84
+ candidates.push(proven);
85
+ }
86
+ const matches = [];
87
+ for (const registration of registrations) {
88
+ if (registration.matches !== undefined) {
89
+ let subject;
90
+ let routingFailed = false;
91
+ try {
92
+ subject = registration.matches(input);
93
+ }
94
+ catch {
95
+ // An uncertain predicate must not leak fail-open — route with subject '-'. The
96
+ // flag travels with the match because a skip registration has no body to carry
97
+ // that verdict out, and answering `skipped` there would turn the fail-closed
98
+ // routing into a pass.
99
+ subject = '-';
100
+ routingFailed = true;
101
+ }
102
+ if (subject !== null) {
103
+ matches.push({ registration, mentionedPath: subject, routingFailed });
104
+ }
105
+ continue;
106
+ }
107
+ const paths = registration.protectedPaths.filter((path) => path !== '');
108
+ const mentionedPath = paths.find((path) => candidates.some((candidate) => pathMatchesProtected(candidate, path))) ??
109
+ (failed ? paths[0] : undefined);
110
+ if (mentionedPath !== undefined) {
111
+ matches.push({ registration, mentionedPath });
112
+ }
113
+ }
114
+ return matches;
115
+ }
116
+ /**
117
+ * Dispatch covenants for a stdin payload.
118
+ *
119
+ * Fail-closed: an unjudgeable payload — unparseable JSON, or a parseable one whose
120
+ * structure defeats the matching traversal (a null toolCalls element, adversarially deep
121
+ * nesting) — yields exitCode 2, judges nothing, and appends exactly one `blocked` record
122
+ * for the dispatcher itself. "Cannot judge" means block; it never means throw, because an
123
+ * uncaught rejection exits the hook with a non-blocking code and becomes a bypass vector.
124
+ * On matches, every matched registration runs sequentially via {@link runCovenant}
125
+ * (run-all, no short-circuit); the verdict is `2` if any body blocks, else `0`. No matches
126
+ * passes vacuously with zero judgments and zero telemetry.
127
+ *
128
+ * World: `spec.world` is the supply layer's result, attached to the parsed input before
129
+ * routing so the body, the routing predicate, and the valve all judge one world. A world
130
+ * the payload itself carries stands only when the spec names none.
131
+ *
132
+ * Witness: the dispatcher only BINDS the witness's arguments — the parsed input, the
133
+ * injected `spec.transcript` (`noopTranscript` when omitted), and a `{ label, subject }`
134
+ * context naming the registration and its matched path — and hands the thunk to
135
+ * {@link runCovenant}, which consults it after the judgment and only when the body's
136
+ * outcome translated to `blocked`. So the body always runs: a matched registration that
137
+ * upholds is never witnessed, and a `true` return relaxes a real break into
138
+ * `0` / `witnessed`. A predicate that throws opens nothing: an uncertain witness never
139
+ * leaks toward fail-open.
140
+ *
141
+ * Enforce: the level has two owners — `spec.enforce` is the observer's posture for the
142
+ * whole dispatch, `registration.enforce` the author's for one entry — and the dispatcher
143
+ * composes them per registration with the lenient side winning, then threads the effective
144
+ * level into {@link runCovenant}, where the translation table lives. Lenient-wins keeps an
145
+ * explicit `block` entry from raising a surface the observer lowered, and lets one entry
146
+ * lower itself under a block surface. The dispatcher's own fail-closed is outside that axis
147
+ * and outside the valve too (nothing judged, so no verdict to relax). Each results entry
148
+ * surfaces the telemetry `event` the wrapper recorded, never a recomputed one — the valve
149
+ * is impure, and a recompute would consult it twice for one verdict.
150
+ */
151
+ export async function dispatchCovenants(spec) {
152
+ const blockedByDispatcher = () => {
153
+ appendRecordFailOpen(spec.telemetryPath, {
154
+ event: 'blocked',
155
+ label: spec.dispatcherLabel ?? 'dispatcher',
156
+ subject: '-',
157
+ });
158
+ return { exitCode: EXIT_BREAK_BLOCKING, results: [] };
159
+ };
160
+ const parsed = parseInput(spec.stdinPayload);
161
+ if (!parsed.ok) {
162
+ return blockedByDispatcher();
163
+ }
164
+ // The world the supply layer built, spliced onto the parsed input here so a composition
165
+ // root never reopens the payload string its adapter path produced. Without one the key
166
+ // stays absent rather than holding `undefined`: the judge derives the change set from
167
+ // the field's absence, and an empty object would read as "supplied nothing".
168
+ const input = spec.world === undefined ? parsed.value : { ...parsed.value, world: spec.world };
169
+ let matches;
170
+ try {
171
+ matches = matchRegistrations(input, spec.registrations);
172
+ }
173
+ catch {
174
+ // Structurally unjudgeable input (parseInput validates the collection shapes, not the
175
+ // element ones) — fail-closed, same as an unparseable payload.
176
+ return blockedByDispatcher();
177
+ }
178
+ const transcript = spec.transcript ?? noopTranscript;
179
+ const results = [];
180
+ for (const { registration, mentionedPath, routingFailed } of matches) {
181
+ if (registration.skip !== undefined) {
182
+ if (routingFailed === true) {
183
+ // The routing predicate could not answer, which matchRegistrations already
184
+ // resolved fail-closed. A body-bearing registration would carry that verdict out
185
+ // by judging and blocking; a skip has no body, so the block is recorded here
186
+ // rather than softened into a pass. Outside the enforce axis, like every
187
+ // unjudgeable outcome.
188
+ appendRecordFailOpen(spec.telemetryPath, {
189
+ event: 'blocked',
190
+ label: registration.label,
191
+ subject: mentionedPath,
192
+ });
193
+ results.push({
194
+ label: registration.label,
195
+ exitCode: EXIT_BREAK_BLOCKING,
196
+ event: 'blocked',
197
+ });
198
+ continue;
199
+ }
200
+ // Nothing to judge and nothing to witness — the valve exists for a verdict, and a
201
+ // skip has none. Recording it keeps the no-op visible in `gain`.
202
+ appendRecordFailOpen(spec.telemetryPath, {
203
+ event: 'skipped',
204
+ label: registration.label,
205
+ subject: mentionedPath,
206
+ reason: registration.skip.kind,
207
+ });
208
+ results.push({ label: registration.label, exitCode: EXIT_UPHOLD, event: 'skipped' });
209
+ continue;
210
+ }
211
+ // Bound here, consulted in the wrapper: the context is what the umbrella prompt
212
+ // needs to name what broke. The local alias exists for TypeScript narrowing — a
213
+ // property access cannot stay narrowed inside the closure below.
214
+ const witness = registration.witness;
215
+ // Absence stays absent: the block default lives in the wrapper, not restated here.
216
+ // A routing that could not answer is outside the level axis on this arm too (the skip
217
+ // arm above already is): the body judges against subject '-' and its break must land
218
+ // blocked whatever level the entry or the surface declared. Every compiled entry
219
+ // carries a level, so without this the unjudgeable call would advise.
220
+ const effectiveEnforce = routingFailed === true
221
+ ? undefined
222
+ : registration.enforce === 'advise'
223
+ ? 'advise'
224
+ : spec.enforce;
225
+ // The parsed call set is handed to the judge HERE rather than baked in at assembly:
226
+ // the dispatcher is the one place that has it, so assembly stays payload-free and one
227
+ // compiled registration set serves every payload.
228
+ const body = registration.body;
229
+ const { exitCode, event } = await runCovenant({
230
+ body: () => body(input),
231
+ label: registration.label,
232
+ subject: mentionedPath,
233
+ telemetryPath: spec.telemetryPath,
234
+ enforce: effectiveEnforce,
235
+ ...(witness !== undefined
236
+ ? {
237
+ witness: () => witness(input, transcript, {
238
+ label: registration.label,
239
+ subject: mentionedPath,
240
+ }),
241
+ }
242
+ : {}),
243
+ });
244
+ results.push({ label: registration.label, exitCode, event });
245
+ }
246
+ const exitCode = results.some((result) => result.exitCode === EXIT_BREAK_BLOCKING)
247
+ ? EXIT_BREAK_BLOCKING
248
+ : EXIT_UPHOLD;
249
+ return { exitCode, results };
250
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * `EXTRACT_STEPS` — the registry of unary extract steps, the open half of the extraction
3
+ * vocabulary. A declaration can name a step only if this table carries it; anything else is
4
+ * refused when the declaration is compiled, before any value is produced.
5
+ *
6
+ * Every entry is a triple: `args` is the closed set of argument keys it admits, `validate`
7
+ * turns a wrong key or a wrong type into a config fault, and `run` maps `Items` to `Items`
8
+ * and to nothing else — no step answers a boolean, because the last comparison of a
9
+ * judgment belongs to the relation position.
10
+ *
11
+ * The seventeen steps:
12
+ *
13
+ * - `source` — lift `World[of]` into a single item keyed `'0'`.
14
+ * - `json` — parse each item's string value; an unparseable value fails supply.
15
+ * - `select` — project a dot path, spreading an array result into position-keyed items; a
16
+ * path the object does not carry projects to nothing.
17
+ * - `items` — spread each array value one level into position-keyed items; a non-array value
18
+ * is dropped, and an element that is itself an array stays one item. The keys are
19
+ * positions within their own array, so two items each holding one collide under `'0'`,
20
+ * and a relation that compares keys reads them as positions and nothing else. Pair this
21
+ * step with `keyBy` when the elements are objects carrying the key the relation needs.
22
+ * - `keyBy` — re-index each element by one of its fields, as a string.
23
+ * - `keyByPattern` — re-index each item by capture group 1 of a constant regular expression
24
+ * over its stringified value, dropping what does not match or leaves the group unbound.
25
+ * The pair of `keyBy`: one re-keys by an object field, the other by a pattern over the
26
+ * value.
27
+ * - `field` — keep the key, replace the value with one property of it.
28
+ * - `filter` — keep the items every constant predicate admits.
29
+ * - `flattenKeys` — enumerate a nested object's leaves as dot paths, key and value alike.
30
+ * - `sort` — stable sort by value: numbers numerically, strings by code point.
31
+ * - `lines` — split a string into trimmed non-empty lines keyed by source line number.
32
+ * - `matches` — keep the items whose value matches a constant regular expression.
33
+ * - `toolUses` — one item per tool call of a session snapshot, keyed by the call's own
34
+ * observation ordinal; `names` and `subagentType` narrow it and conjoin.
35
+ * - `userTexts` — one item per user turn of a session snapshot whose text matches a
36
+ * constant expression, keyed by the turn's ordinal, the value carrying the snapshot clock.
37
+ * - `agentType` — one item per parsed sidecar record whose `agentType` equals `is`, keyed by
38
+ * its position in the record list.
39
+ * - `first` — the first item alone, under its own key; it does not sort.
40
+ * - `ageMs` — add `ageMs = observedAtMs − timestampMs` to each value, dropping an item whose
41
+ * timestamp is not a number or lies after the clock, because an undated or future-dated
42
+ * observation is not a recent one.
43
+ *
44
+ * A step that would compare two world values against each other is not a unary step: that
45
+ * comparison belongs to a combinator, and the combinators are three and closed.
46
+ */
47
+ /** One element of an extraction. `key` combines and compares, `value` is what a relation sees. */
48
+ export type Item = {
49
+ readonly key: string;
50
+ readonly value: unknown;
51
+ };
52
+ /** The input and the output of every extract step; the order is the supply's. */
53
+ export type Items = readonly Item[];
54
+ /** An extraction split by a paired source into its two states. */
55
+ export type PairedItems = {
56
+ readonly pre: Items;
57
+ readonly post: Items;
58
+ };
59
+ /**
60
+ * One session's observations as plain data — the value a host flattens its own history into
61
+ * once, so this module judges data rather than a query interface. `index` is an observation
62
+ * ordinal within its own list, and `observedAtMs` the clock the ages are measured against.
63
+ */
64
+ export type SessionSnapshot = {
65
+ readonly observedAtMs: number;
66
+ readonly userMessages: readonly {
67
+ index: number;
68
+ text: string;
69
+ timestampMs?: number;
70
+ }[];
71
+ readonly toolCalls: readonly {
72
+ index: number;
73
+ name: string;
74
+ args: Record<string, unknown>;
75
+ succeeded?: boolean;
76
+ }[];
77
+ };
78
+ /** Source name to value. `state` carries `{ pre, post }` and is read as a pair. */
79
+ export type World = Readonly<Record<string, unknown>>;
80
+ /** What compilation answers with instead of a compiled declaration; never a throw. */
81
+ export type ConfigFault = {
82
+ readonly kind: 'config-fault';
83
+ readonly location: string;
84
+ readonly reason: string;
85
+ };
86
+ /** One registry entry: the closed argument keys, their validator, and the runner. */
87
+ export type ExtractStepEntry = {
88
+ readonly args: ReadonlySet<string>;
89
+ validate(args: Readonly<Record<string, unknown>>, location: string): ConfigFault | undefined;
90
+ run(items: Items, args: Readonly<Record<string, unknown>>): Items;
91
+ };
92
+ /**
93
+ * A value `json` could not parse, thrown out of `run` and caught by the engine, which
94
+ * names the source and answers a supply failure. It never leaves the engine.
95
+ */
96
+ export declare class SupplyFailure extends Error {
97
+ }
98
+ /** Build the value compilation answers with when a declaration is wrong at `location`. */
99
+ export declare function fault(location: string, reason: string): ConfigFault;
100
+ /**
101
+ * The canonical form of a value for structural comparison — object keys sorted so that two
102
+ * objects written in different orders compare equal. `undefined` is distinct from `null`:
103
+ * an absent field and a field set to null are different answers, and a discipline that
104
+ * excludes `null` must not thereby exclude the absent one.
105
+ */
106
+ export declare function canonical(value: unknown): string;
107
+ /** Structural equality — the equality every relation and the `filter` predicates use. */
108
+ export declare function sameValue(left: unknown, right: unknown): boolean;
109
+ /**
110
+ * The order `sort` and `Ordered` share, chosen once for a whole list rather than pair by
111
+ * pair: all-numbers compare numerically, anything else by its stringified form. A per-pair
112
+ * choice would order a mixed list inconsistently — `2 < 10` numerically and `'10' < '2'` as
113
+ * text, so the answer would depend on which pairs the comparison happened to visit.
114
+ */
115
+ export declare function comparatorFor(items: Items): (left: Item, right: Item) => number;
116
+ /**
117
+ * The unary vocabulary, by step name. A declaration may name only these; the engine looks a
118
+ * step up here when it compiles and refuses any name the table does not carry.
119
+ */
120
+ export declare const EXTRACT_STEPS: Readonly<Record<string, ExtractStepEntry>>;
121
+ /** The names the registry carries — the whole of the unary vocabulary. */
122
+ export declare const UNARY_STEP_NAMES: readonly string[];
Binary file
@@ -0,0 +1,117 @@
1
+ /**
2
+ * `mentionsPath` — the single path-mention semantic shared by the dispatcher and the
3
+ * pure judges.
4
+ *
5
+ * Both the path-routing dispatcher (`matchRegistrations`) and any covenant judge that keys
6
+ * on a protected path import this one function, so the two layers can never drift apart.
7
+ * Argument names are never inspected — only string *values* are scanned, at any depth,
8
+ * keeping the traversal agent-neutral.
9
+ *
10
+ * Interior `.`/`..` resolution is a SECOND comparison, never a replacement: raw segments
11
+ * match first, dot-resolved segments only if that fails. Because the result is a union, a
12
+ * newly closed notation can only ever add matches — a replacement pass would silently
13
+ * withdraw defences that the raw comparison already had.
14
+ *
15
+ * A glob, a variable expansion, and a tilde are deliberately NOT read. None can be resolved
16
+ * without running the shell or touching the filesystem, and a judge that guesses at them
17
+ * either misses the real target or blocks an innocent one. They stay undecidable here and
18
+ * are answered where undecidability belongs: the Bash axis's opaque-token rule and the skip
19
+ * registrations. A spelling some layer genuinely *can* resolve — the home directory in front
20
+ * of the session transcript — is closed by the layer that knows the value, via a dedicated
21
+ * `matches` predicate rather than a protected path, so no home spelling is inferred here.
22
+ */
23
+ /**
24
+ * Normalize a path into segments: strip leading `./`, trailing `/`, split on `/`, drop
25
+ * empties. Exported so the self-mod judge can tell a judgeable evidence path from a
26
+ * degenerate one (`''`, `'.'`, `'/'` — zero segments) that proves nothing.
27
+ *
28
+ * A lone `.` survives as a segment, and resolving interior dots is a separate pass inside
29
+ * {@link pathMatchesProtected}. Folding that pass in here would change what the
30
+ * degenerate-evidence check built on top of this function counts as degenerate.
31
+ */
32
+ export declare function pathSegments(path: string): string[];
33
+ /**
34
+ * Return the mutation target proven by a call's `fileChange` evidence, or `null` when the
35
+ * evidence proves none.
36
+ *
37
+ * @param call - The tool call whose evidence is inspected
38
+ * @returns The change path when the evidence carries a recognized kind and a path with at
39
+ * least one non-`.` segment; otherwise `null`
40
+ */
41
+ export declare function provenChangePath(call: {
42
+ fileChange?: unknown;
43
+ }): string | null;
44
+ /**
45
+ * Resolve `.` and `..` against the preceding segment — pure string work, no filesystem and
46
+ * no working directory, so the answer is the same wherever the judge runs.
47
+ *
48
+ * A `..` with nothing left to cancel is KEPT rather than dropped. Dropping it would collapse
49
+ * `../packages` into `packages` and hand a sibling checkout the protection meant for this
50
+ * one; keeping it leaves a segment that matches nothing, which is the honest answer for a
51
+ * path that points outside the tree.
52
+ *
53
+ * Exported so a judge whose own equality needs the same second pass (the transcript
54
+ * predicate) shares this one implementation instead of forking it per site.
55
+ */
56
+ export declare function resolveDotSegments(segments: string[]): string[];
57
+ /**
58
+ * True iff `candidate` names the protected path, a descendant of it, or a (relative) ancestor
59
+ * of it — compared on path segments, not raw substrings, by {@link segmentsMatch}.
60
+ *
61
+ * Two passes, unioned. The raw pass is the shipped semantic and runs first: a command that
62
+ * spells the protected path out loud is caught by it no matter what the path resolves to
63
+ * afterwards, which is why `rm -rf .claude/hooks/../..` breaks here rather than needing a
64
+ * rule of its own. The dot-resolved pass runs only when the raw one finds nothing, and is
65
+ * what `packages/core/./dist/index.js` and `packages/core/src/../dist/index.js` need. Because
66
+ * it is a union it can only ever add matches, never withdraw one.
67
+ */
68
+ export declare function pathMatchesProtected(candidate: string, protectedPath: string): boolean;
69
+ /**
70
+ * Extract path candidates from one string token. The token is split on shell separators
71
+ * that join a path to other lexemes — whitespace, `=`, `,`, parentheses, backtick — so a path
72
+ * embedded in a compound token (a `--flag=path`, an opaque command substitution, an eval's
73
+ * quoted argument) surfaces as its own candidate while a standalone token stays intact (so the
74
+ * segment-boundary trap still rejects a sibling like `core/src-generated`). `/` is never a
75
+ * separator (it is the path's own segment boundary); `:` is deliberately NOT a separator
76
+ * either — splitting on it shatters URLs (`https://…`) into fragments that the offset-free
77
+ * descendant match then over-blocks, and a colon-joined path list is already reached by the
78
+ * contiguous-run match without the split.
79
+ */
80
+ export declare function pathCandidates(token: string): string[];
81
+ /**
82
+ * Extract path candidates from a whole command line the tokenizer REFUSED — the fallback-only
83
+ * counterpart of {@link pathCandidates}.
84
+ *
85
+ * The precondition is the opposite one. On the tokenized path an operator between two words
86
+ * has already become a word boundary, so `pathCandidates`' separator set never needed the
87
+ * operators themselves. A fallback branch has no tokenizer left and gets the raw line, where
88
+ * nothing consumed them and a path glued to one (`packages/core/dist;echo x`) stayed a single
89
+ * unmatchable segment — so the set here is wider by exactly what the tokenizer would have
90
+ * eaten: `;` `&` `|` `<` `>`.
91
+ *
92
+ * The line itself stays a candidate alongside the fragments, so a protected path whose own
93
+ * segment carries an operator (`pkg/a&b/dist`) is still matchable; an added form can only add
94
+ * a match, never withdraw one. `:` stays out for the reason {@link pathCandidates} records.
95
+ *
96
+ * Widening a fragment boundary widens the ancestor direction with it: `…?x=1&packages=1`
97
+ * splits to a bare `packages`, which `segmentsMatch` accepts as a root-anchored ancestor of a
98
+ * protected `packages/core/dist`. That over-block is accepted rather than narrowed — the
99
+ * narrowing that would spare it also drops a glued ancestor destroy
100
+ * (`rm -rf packages/core;echo x`), which is the defence this fallback exists to provide.
101
+ */
102
+ export declare function untokenizableLineCandidates(line: string): string[];
103
+ /**
104
+ * True when `predicate` holds for any string value inside `value`, at any depth.
105
+ *
106
+ * Arrays and plain objects are walked by value; keys are never scanned, and non-string
107
+ * primitives never match. Short-circuits on the first hit — the walk answers an existence
108
+ * question, so a caller that needs every match wants its own traversal.
109
+ */
110
+ export declare function someStringValue(value: unknown, predicate: (text: string) => boolean): boolean;
111
+ /**
112
+ * Recursively test whether any string value inside `value` matches `path` by path-segment
113
+ * containment (ancestor / descendant / equal). Each string is split into path candidates,
114
+ * each tested via {@link pathMatchesProtected}. Only string values are scanned; keys,
115
+ * numbers, and other primitives never match.
116
+ */
117
+ export declare function mentionsPath(value: unknown, path: string): boolean;