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.
- package/README.ko.md +16 -28
- package/README.md +16 -28
- package/dist/baseline.d.ts +37 -1
- package/dist/baseline.js +68 -1
- package/dist/bin.d.ts +3 -4
- package/dist/bin.js +73 -90
- package/dist/covenant/bash-line.d.ts +130 -0
- package/dist/covenant/bash-line.js +566 -0
- package/dist/covenant/declaration-engine.d.ts +108 -0
- package/dist/covenant/declaration-engine.js +415 -0
- package/dist/covenant/discipline.d.ts +109 -0
- package/dist/covenant/discipline.js +667 -0
- package/dist/covenant/dispatch.d.ts +156 -0
- package/dist/covenant/dispatch.js +250 -0
- package/dist/covenant/extract-steps.d.ts +122 -0
- package/dist/covenant/extract-steps.js +0 -0
- package/dist/covenant/mention.d.ts +117 -0
- package/dist/covenant/mention.js +209 -0
- package/dist/covenant/module.d.ts +25 -0
- package/dist/covenant/module.js +23 -0
- package/dist/covenant/mutation-rules.d.ts +40 -0
- package/dist/covenant/mutation-rules.js +146 -0
- package/dist/covenant/relations.d.ts +46 -0
- package/dist/covenant/relations.js +68 -0
- package/dist/covenant/run-covenant.d.ts +95 -0
- package/dist/covenant/run-covenant.js +164 -0
- package/dist/covenant/self-mod.d.ts +55 -0
- package/dist/covenant/self-mod.js +84 -0
- package/dist/covenant/shell-evidence.d.ts +38 -0
- package/dist/covenant/shell-evidence.js +247 -0
- package/dist/covenant/shell-mod.d.ts +84 -0
- package/dist/covenant/shell-mod.js +213 -0
- package/dist/covenant/supply.d.ts +54 -0
- package/dist/covenant/supply.js +50 -0
- package/dist/covenant/transcript-mod.d.ts +60 -0
- package/dist/covenant/transcript-mod.js +254 -0
- package/dist/covenant/ttl-witness.d.ts +47 -0
- package/dist/covenant/ttl-witness.js +80 -0
- package/dist/covenant-check.d.ts +74 -41
- package/dist/covenant-check.js +372 -129
- package/dist/diff-ir.d.ts +28 -0
- package/dist/diff-ir.js +310 -0
- package/dist/docs/README.ko.md +10 -9
- package/dist/docs/README.md +10 -9
- package/dist/docs/catalog.json +76 -28
- package/dist/docs/concepts/judgment.ko.md +2 -2
- package/dist/docs/concepts/judgment.md +2 -2
- package/dist/docs/how-to/configure-project.ko.md +16 -16
- package/dist/docs/how-to/configure-project.md +15 -12
- package/dist/docs/how-to/connect-surfaces.ko.md +67 -34
- package/dist/docs/how-to/connect-surfaces.md +67 -34
- package/dist/docs/how-to/write-disciplines.ko.md +64 -7
- package/dist/docs/how-to/write-disciplines.md +65 -7
- package/dist/docs/index.json +744 -362
- package/dist/docs/reference/cli/covenant-check.ko.md +112 -65
- package/dist/docs/reference/cli/covenant-check.md +114 -59
- package/dist/docs/reference/cli/docs.ko.md +2 -2
- package/dist/docs/reference/cli/docs.md +2 -2
- package/dist/docs/reference/cli/explain.ko.md +20 -15
- package/dist/docs/reference/cli/explain.md +22 -15
- package/dist/docs/reference/cli/init.ko.md +91 -52
- package/dist/docs/reference/cli/init.md +81 -55
- package/dist/docs/reference/configuration/index.ko.md +182 -71
- package/dist/docs/reference/configuration/index.md +188 -76
- package/dist/docs/reference/packages/adapter-claude-code.ko.md +39 -11
- package/dist/docs/reference/packages/adapter-claude-code.md +40 -14
- package/dist/docs/reference/packages/adapter-codex.ko.md +99 -0
- package/dist/docs/reference/packages/adapter-codex.md +100 -0
- package/dist/docs/reference/packages/adapter-grok.ko.md +81 -0
- package/dist/docs/reference/packages/adapter-grok.md +83 -0
- package/dist/docs/reference/packages/core.ko.md +50 -4
- package/dist/docs/reference/packages/core.md +55 -6
- package/dist/docs/reference/packages/polydeukes.ko.md +139 -77
- package/dist/docs/reference/packages/polydeukes.md +147 -81
- package/dist/docs/reference/packages/sdk-ts.ko.md +164 -0
- package/dist/docs/reference/packages/sdk-ts.md +167 -0
- package/dist/docs/troubleshooting.ko.md +51 -19
- package/dist/docs/troubleshooting.md +53 -17
- package/dist/docs/tutorials/first-judgment.ko.md +6 -4
- package/dist/docs/tutorials/first-judgment.md +6 -4
- package/dist/explain.d.ts +1 -1
- package/dist/explain.js +37 -22
- package/dist/load-config.d.ts +25 -1
- package/dist/load-config.js +21 -3
- package/dist/pre-state-reader.d.ts +1 -1
- package/dist/pre-state-reader.js +1 -1
- package/dist/scaffold-project.d.ts +2 -2
- package/dist/scaffold-project.js +12 -4
- package/dist/schema/polydeukes.schema.json +12 -0
- package/dist/worktree-reader.d.ts +19 -0
- package/dist/worktree-reader.js +30 -0
- package/package.json +5 -18
- package/dist/claude-code-hook.d.ts +0 -79
- package/dist/claude-code-hook.js +0 -372
- package/dist/claude-code.d.ts +0 -6
- package/dist/claude-code.js +0 -6
- package/dist/covenant-module.d.ts +0 -25
- package/dist/covenant-module.js +0 -42
- package/dist/docs/reference/packages/adapter-git.ko.md +0 -101
- package/dist/docs/reference/packages/adapter-git.md +0 -108
- package/dist/docs/reference/packages/covenant.ko.md +0 -115
- package/dist/docs/reference/packages/covenant.md +0 -123
- package/dist/index.d.ts +0 -22
- package/dist/index.js +0 -21
- package/dist/init-claude-code.d.ts +0 -52
- package/dist/init-claude-code.js +0 -586
- package/dist/init-grok.d.ts +0 -51
- package/dist/init-grok.js +0 -242
|
@@ -0,0 +1,209 @@
|
|
|
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 function pathSegments(path) {
|
|
33
|
+
return path
|
|
34
|
+
.replace(/^(\.\/)+/, '')
|
|
35
|
+
.replace(/\/+$/, '')
|
|
36
|
+
.split('/')
|
|
37
|
+
.filter((segment) => segment !== '');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Return the mutation target proven by a call's `fileChange` evidence, or `null` when the
|
|
41
|
+
* evidence proves none.
|
|
42
|
+
*
|
|
43
|
+
* @param call - The tool call whose evidence is inspected
|
|
44
|
+
* @returns The change path when the evidence carries a recognized kind and a path with at
|
|
45
|
+
* least one non-`.` segment; otherwise `null`
|
|
46
|
+
*/
|
|
47
|
+
export function provenChangePath(call) {
|
|
48
|
+
const evidence = call.fileChange;
|
|
49
|
+
if (typeof evidence !== 'object' || evidence === null)
|
|
50
|
+
return null;
|
|
51
|
+
const { kind, path } = evidence;
|
|
52
|
+
if (typeof path !== 'string')
|
|
53
|
+
return null;
|
|
54
|
+
// Core `parseInput` validates the collection shapes, not the element ones, so evidence
|
|
55
|
+
// is usable only when it could prove a target: a recognized discriminant and a path that
|
|
56
|
+
// carries segments to judge. A one-field stub, a bogus kind, or a degenerate path (`''`,
|
|
57
|
+
// `'.'`, `'/'` — zero segments) proves nothing and must fall through rather than be
|
|
58
|
+
// dereferenced or, worse, suppress the fallback — the evidence branch upholding on proof
|
|
59
|
+
// it never had is a fail-open, and an exported pure judge that throws is a bypass vector.
|
|
60
|
+
// `pathSegments` keeps a lone `.` as a segment, so require one that names a file.
|
|
61
|
+
if (!pathSegments(path).some((segment) => segment !== '.'))
|
|
62
|
+
return null;
|
|
63
|
+
return kind === 'create' || kind === 'modify' || kind === 'delete' ? path : null;
|
|
64
|
+
}
|
|
65
|
+
/** True iff `needle` occurs as a contiguous segment run inside `haystack` (any offset). */
|
|
66
|
+
function containsSegmentRun(haystack, needle) {
|
|
67
|
+
if (needle.length === 0 || needle.length > haystack.length)
|
|
68
|
+
return false;
|
|
69
|
+
for (let start = 0; start + needle.length <= haystack.length; start++) {
|
|
70
|
+
if (needle.every((segment, i) => segment === haystack[start + i]))
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* One comparison, shared by both passes. The two directions are deliberately asymmetric:
|
|
77
|
+
* - descendant / equal: the protected segments appear as a contiguous run at ANY offset in
|
|
78
|
+
* the candidate, so an ABSOLUTE `file_path` (`/home/u/proj/core/src/x` — the real Edit
|
|
79
|
+
* payload shape) matches the relative protected `core/src`;
|
|
80
|
+
* - ancestor: the WHOLE candidate is a root-anchored prefix of the protected path, so the
|
|
81
|
+
* relative parent op `rm -rf packages/core` matches but an unrelated `vendor/packages`
|
|
82
|
+
* whose tail merely coincides with the protected head does NOT.
|
|
83
|
+
* The asymmetry is load-bearing: allowing any candidate *suffix* to head the protected path
|
|
84
|
+
* would block legitimate unrelated dirs (`x/packages/core`). The cost is that an ABSOLUTE
|
|
85
|
+
* ancestor path (`rm -rf /abs/.../packages/core`) is not caught — an accepted non-goal
|
|
86
|
+
* (complete Bash lockdown was never the goal; the relative form is still caught, and the
|
|
87
|
+
* over-block alternative is worse). The segment boundary is exact, so `core/src-generated`
|
|
88
|
+
* never matches `core/src`.
|
|
89
|
+
*/
|
|
90
|
+
function segmentsMatch(a, b) {
|
|
91
|
+
if (a.length === 0)
|
|
92
|
+
return false;
|
|
93
|
+
if (containsSegmentRun(a, b))
|
|
94
|
+
return true;
|
|
95
|
+
// Ancestor: the candidate is a proper root-anchored prefix of the protected path.
|
|
96
|
+
return a.length < b.length && a.every((segment, i) => segment === b[i]);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolve `.` and `..` against the preceding segment — pure string work, no filesystem and
|
|
100
|
+
* no working directory, so the answer is the same wherever the judge runs.
|
|
101
|
+
*
|
|
102
|
+
* A `..` with nothing left to cancel is KEPT rather than dropped. Dropping it would collapse
|
|
103
|
+
* `../packages` into `packages` and hand a sibling checkout the protection meant for this
|
|
104
|
+
* one; keeping it leaves a segment that matches nothing, which is the honest answer for a
|
|
105
|
+
* path that points outside the tree.
|
|
106
|
+
*
|
|
107
|
+
* Exported so a judge whose own equality needs the same second pass (the transcript
|
|
108
|
+
* predicate) shares this one implementation instead of forking it per site.
|
|
109
|
+
*/
|
|
110
|
+
export function resolveDotSegments(segments) {
|
|
111
|
+
const resolved = [];
|
|
112
|
+
for (const segment of segments) {
|
|
113
|
+
if (segment === '.')
|
|
114
|
+
continue;
|
|
115
|
+
if (segment === '..' && resolved.length > 0 && resolved[resolved.length - 1] !== '..') {
|
|
116
|
+
resolved.pop();
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
resolved.push(segment);
|
|
120
|
+
}
|
|
121
|
+
return resolved;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* True iff `candidate` names the protected path, a descendant of it, or a (relative) ancestor
|
|
125
|
+
* of it — compared on path segments, not raw substrings, by {@link segmentsMatch}.
|
|
126
|
+
*
|
|
127
|
+
* Two passes, unioned. The raw pass is the shipped semantic and runs first: a command that
|
|
128
|
+
* spells the protected path out loud is caught by it no matter what the path resolves to
|
|
129
|
+
* afterwards, which is why `rm -rf .claude/hooks/../..` breaks here rather than needing a
|
|
130
|
+
* rule of its own. The dot-resolved pass runs only when the raw one finds nothing, and is
|
|
131
|
+
* what `packages/core/./dist/index.js` and `packages/core/src/../dist/index.js` need. Because
|
|
132
|
+
* it is a union it can only ever add matches, never withdraw one.
|
|
133
|
+
*/
|
|
134
|
+
export function pathMatchesProtected(candidate, protectedPath) {
|
|
135
|
+
const a = pathSegments(candidate);
|
|
136
|
+
const b = pathSegments(protectedPath);
|
|
137
|
+
if (b.length === 0)
|
|
138
|
+
return false;
|
|
139
|
+
if (segmentsMatch(a, b))
|
|
140
|
+
return true;
|
|
141
|
+
return segmentsMatch(resolveDotSegments(a), b);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Extract path candidates from one string token. The token is split on shell separators
|
|
145
|
+
* that join a path to other lexemes — whitespace, `=`, `,`, parentheses, backtick — so a path
|
|
146
|
+
* embedded in a compound token (a `--flag=path`, an opaque command substitution, an eval's
|
|
147
|
+
* quoted argument) surfaces as its own candidate while a standalone token stays intact (so the
|
|
148
|
+
* segment-boundary trap still rejects a sibling like `core/src-generated`). `/` is never a
|
|
149
|
+
* separator (it is the path's own segment boundary); `:` is deliberately NOT a separator
|
|
150
|
+
* either — splitting on it shatters URLs (`https://…`) into fragments that the offset-free
|
|
151
|
+
* descendant match then over-blocks, and a colon-joined path list is already reached by the
|
|
152
|
+
* contiguous-run match without the split.
|
|
153
|
+
*/
|
|
154
|
+
export function pathCandidates(token) {
|
|
155
|
+
return token.split(/[\s=,()`]+/).filter((fragment) => fragment !== '');
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Extract path candidates from a whole command line the tokenizer REFUSED — the fallback-only
|
|
159
|
+
* counterpart of {@link pathCandidates}.
|
|
160
|
+
*
|
|
161
|
+
* The precondition is the opposite one. On the tokenized path an operator between two words
|
|
162
|
+
* has already become a word boundary, so `pathCandidates`' separator set never needed the
|
|
163
|
+
* operators themselves. A fallback branch has no tokenizer left and gets the raw line, where
|
|
164
|
+
* nothing consumed them and a path glued to one (`packages/core/dist;echo x`) stayed a single
|
|
165
|
+
* unmatchable segment — so the set here is wider by exactly what the tokenizer would have
|
|
166
|
+
* eaten: `;` `&` `|` `<` `>`.
|
|
167
|
+
*
|
|
168
|
+
* The line itself stays a candidate alongside the fragments, so a protected path whose own
|
|
169
|
+
* segment carries an operator (`pkg/a&b/dist`) is still matchable; an added form can only add
|
|
170
|
+
* a match, never withdraw one. `:` stays out for the reason {@link pathCandidates} records.
|
|
171
|
+
*
|
|
172
|
+
* Widening a fragment boundary widens the ancestor direction with it: `…?x=1&packages=1`
|
|
173
|
+
* splits to a bare `packages`, which `segmentsMatch` accepts as a root-anchored ancestor of a
|
|
174
|
+
* protected `packages/core/dist`. That over-block is accepted rather than narrowed — the
|
|
175
|
+
* narrowing that would spare it also drops a glued ancestor destroy
|
|
176
|
+
* (`rm -rf packages/core;echo x`), which is the defence this fallback exists to provide.
|
|
177
|
+
*/
|
|
178
|
+
export function untokenizableLineCandidates(line) {
|
|
179
|
+
const fragments = line.split(/[;&|<>]+/).filter((f) => f !== '' && f !== line);
|
|
180
|
+
return [line, ...fragments];
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* True when `predicate` holds for any string value inside `value`, at any depth.
|
|
184
|
+
*
|
|
185
|
+
* Arrays and plain objects are walked by value; keys are never scanned, and non-string
|
|
186
|
+
* primitives never match. Short-circuits on the first hit — the walk answers an existence
|
|
187
|
+
* question, so a caller that needs every match wants its own traversal.
|
|
188
|
+
*/
|
|
189
|
+
export function someStringValue(value, predicate) {
|
|
190
|
+
if (typeof value === 'string') {
|
|
191
|
+
return predicate(value);
|
|
192
|
+
}
|
|
193
|
+
if (Array.isArray(value)) {
|
|
194
|
+
return value.some((item) => someStringValue(item, predicate));
|
|
195
|
+
}
|
|
196
|
+
if (typeof value === 'object' && value !== null) {
|
|
197
|
+
return Object.values(value).some((item) => someStringValue(item, predicate));
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Recursively test whether any string value inside `value` matches `path` by path-segment
|
|
203
|
+
* containment (ancestor / descendant / equal). Each string is split into path candidates,
|
|
204
|
+
* each tested via {@link pathMatchesProtected}. Only string values are scanned; keys,
|
|
205
|
+
* numbers, and other primitives never match.
|
|
206
|
+
*/
|
|
207
|
+
export function mentionsPath(value, path) {
|
|
208
|
+
return someStringValue(value, (text) => pathCandidates(text).some((candidate) => pathMatchesProtected(candidate, path)));
|
|
209
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The judge as the composition roots see it: the seven verbs they call, gathered in one
|
|
3
|
+
* object. A test replaces one member of it; the roots default to the real seven.
|
|
4
|
+
*
|
|
5
|
+
* A leaf module on purpose — it imports the judge and nothing else, so the session subpath
|
|
6
|
+
* that loads it does not also load the change-set surface's translator and reader.
|
|
7
|
+
*/
|
|
8
|
+
import { compileDisciplineRegistrations } from './discipline.ts';
|
|
9
|
+
import { dispatchCovenants } from './dispatch.ts';
|
|
10
|
+
import { selfModRegistration } from './self-mod.ts';
|
|
11
|
+
import { shellModRegistration } from './shell-mod.ts';
|
|
12
|
+
import { planSources, supplySources } from './supply.ts';
|
|
13
|
+
import { transcriptModRegistration } from './transcript-mod.ts';
|
|
14
|
+
/** The judge verbs the composition roots call — the seam a test replaces one member of. */
|
|
15
|
+
export type CovenantModule = {
|
|
16
|
+
dispatchCovenants: typeof dispatchCovenants;
|
|
17
|
+
compileDisciplineRegistrations: typeof compileDisciplineRegistrations;
|
|
18
|
+
selfModRegistration: typeof selfModRegistration;
|
|
19
|
+
shellModRegistration: typeof shellModRegistration;
|
|
20
|
+
transcriptModRegistration: typeof transcriptModRegistration;
|
|
21
|
+
planSources: typeof planSources;
|
|
22
|
+
supplySources: typeof supplySources;
|
|
23
|
+
};
|
|
24
|
+
/** The real seven — what judges a call unless a test injects a replacement. */
|
|
25
|
+
export declare const covenantModule: CovenantModule;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The judge as the composition roots see it: the seven verbs they call, gathered in one
|
|
3
|
+
* object. A test replaces one member of it; the roots default to the real seven.
|
|
4
|
+
*
|
|
5
|
+
* A leaf module on purpose — it imports the judge and nothing else, so the session subpath
|
|
6
|
+
* that loads it does not also load the change-set surface's translator and reader.
|
|
7
|
+
*/
|
|
8
|
+
import { compileDisciplineRegistrations } from './discipline.js';
|
|
9
|
+
import { dispatchCovenants } from './dispatch.js';
|
|
10
|
+
import { selfModRegistration } from './self-mod.js';
|
|
11
|
+
import { shellModRegistration } from './shell-mod.js';
|
|
12
|
+
import { planSources, supplySources } from './supply.js';
|
|
13
|
+
import { transcriptModRegistration } from './transcript-mod.js';
|
|
14
|
+
/** The real seven — what judges a call unless a test injects a replacement. */
|
|
15
|
+
export const covenantModule = {
|
|
16
|
+
dispatchCovenants,
|
|
17
|
+
compileDisciplineRegistrations,
|
|
18
|
+
selfModRegistration,
|
|
19
|
+
shellModRegistration,
|
|
20
|
+
transcriptModRegistration,
|
|
21
|
+
planSources,
|
|
22
|
+
supplySources,
|
|
23
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection rules for the mutation-rule seam.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions only — each rule is `SimpleCommand → MutationTarget[]`, nothing more.
|
|
5
|
+
* Every rule stays silent on opaque tokens: an unknowable value is never reported as a
|
|
6
|
+
* confident path. The tokenizer already marks such commands indeterminate, so the
|
|
7
|
+
* fail-closed signal survives the silence. Protected-path matching, blocking, allowlists,
|
|
8
|
+
* and telemetry belong to the judges that consume these rules.
|
|
9
|
+
*/
|
|
10
|
+
import type { MutationRule, WordToken } from './bash-line.ts';
|
|
11
|
+
/** The final path segment of a command word (`/usr/bin/tee` → `tee`). */
|
|
12
|
+
export declare function commandBasename(word: WordToken): string;
|
|
13
|
+
/**
|
|
14
|
+
* True when a `>&`-family target is an fd reference: all digits, `-` (close), or the
|
|
15
|
+
* digits+`-` move-fd form (`2>&1-` moves fd 1, touching no file). Exported as the SSOT
|
|
16
|
+
* of that boundary — the shell-evidence derivation consults the same judgment.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isFdReference(text: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Reports the target path of every write-direction redirect (any operator containing `>`).
|
|
21
|
+
* Read redirects (`<`) and fd duplication (`2>&1`, `>&-`) are excluded; a csh-style
|
|
22
|
+
* `>& file` whose target is not an fd reference is still a write.
|
|
23
|
+
*/
|
|
24
|
+
export declare const redirectWriteRule: MutationRule;
|
|
25
|
+
/**
|
|
26
|
+
* Reports every non-flag argument of a `tee` command (first-word basename match, so
|
|
27
|
+
* `/usr/bin/tee` fires too). Flags are skipped until the `--` end-of-options marker;
|
|
28
|
+
* after it, `-`-prefixed words are paths. Wrapper commands (`sudo tee`) never fire —
|
|
29
|
+
* the judges' path-mention backstop covers them.
|
|
30
|
+
*/
|
|
31
|
+
export declare const teeRule: MutationRule;
|
|
32
|
+
/**
|
|
33
|
+
* Reports the file operands of an in-place `sed` (first-word basename match, so
|
|
34
|
+
* `/usr/bin/sed` fires too). Without an in-place flag sed writes to stdout — silence.
|
|
35
|
+
* The `-e`/`-f` family is skipped with its value (the `-f` script file is read, not
|
|
36
|
+
* written); when no such flag is present the first positional operand is the sed script
|
|
37
|
+
* — skipped even after `--`. Wrapper commands (`sudo sed`) never fire — the judges'
|
|
38
|
+
* path-mention backstop covers them.
|
|
39
|
+
*/
|
|
40
|
+
export declare const sedInPlaceRule: MutationRule;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection rules for the mutation-rule seam.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions only — each rule is `SimpleCommand → MutationTarget[]`, nothing more.
|
|
5
|
+
* Every rule stays silent on opaque tokens: an unknowable value is never reported as a
|
|
6
|
+
* confident path. The tokenizer already marks such commands indeterminate, so the
|
|
7
|
+
* fail-closed signal survives the silence. Protected-path matching, blocking, allowlists,
|
|
8
|
+
* and telemetry belong to the judges that consume these rules.
|
|
9
|
+
*/
|
|
10
|
+
const REDIRECT_WRITE_RULE_NAME = 'redirect-write';
|
|
11
|
+
const TEE_RULE_NAME = 'tee';
|
|
12
|
+
const SED_IN_PLACE_RULE_NAME = 'sed-in-place';
|
|
13
|
+
/** The final path segment of a command word (`/usr/bin/tee` → `tee`). */
|
|
14
|
+
export function commandBasename(word) {
|
|
15
|
+
return word.text.slice(word.text.lastIndexOf('/') + 1);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* True when a `>&`-family target is an fd reference: all digits, `-` (close), or the
|
|
19
|
+
* digits+`-` move-fd form (`2>&1-` moves fd 1, touching no file). Exported as the SSOT
|
|
20
|
+
* of that boundary — the shell-evidence derivation consults the same judgment.
|
|
21
|
+
*/
|
|
22
|
+
export function isFdReference(text) {
|
|
23
|
+
return text === '-' || /^[0-9]+-?$/.test(text);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Reports the target path of every write-direction redirect (any operator containing `>`).
|
|
27
|
+
* Read redirects (`<`) and fd duplication (`2>&1`, `>&-`) are excluded; a csh-style
|
|
28
|
+
* `>& file` whose target is not an fd reference is still a write.
|
|
29
|
+
*/
|
|
30
|
+
export const redirectWriteRule = {
|
|
31
|
+
name: REDIRECT_WRITE_RULE_NAME,
|
|
32
|
+
detect(command) {
|
|
33
|
+
const targets = [];
|
|
34
|
+
for (const redirect of command.redirects) {
|
|
35
|
+
if (!redirect.operator.includes('>'))
|
|
36
|
+
continue;
|
|
37
|
+
if (redirect.operator.endsWith('>&') && isFdReference(redirect.target.text))
|
|
38
|
+
continue;
|
|
39
|
+
if (redirect.target.opaque)
|
|
40
|
+
continue;
|
|
41
|
+
targets.push({ path: redirect.target.text, rule: REDIRECT_WRITE_RULE_NAME });
|
|
42
|
+
}
|
|
43
|
+
return targets;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Reports every non-flag argument of a `tee` command (first-word basename match, so
|
|
48
|
+
* `/usr/bin/tee` fires too). Flags are skipped until the `--` end-of-options marker;
|
|
49
|
+
* after it, `-`-prefixed words are paths. Wrapper commands (`sudo tee`) never fire —
|
|
50
|
+
* the judges' path-mention backstop covers them.
|
|
51
|
+
*/
|
|
52
|
+
export const teeRule = {
|
|
53
|
+
name: TEE_RULE_NAME,
|
|
54
|
+
detect(command) {
|
|
55
|
+
const first = command.words[0];
|
|
56
|
+
if (first === undefined || first.opaque)
|
|
57
|
+
return [];
|
|
58
|
+
if (commandBasename(first) !== 'tee')
|
|
59
|
+
return [];
|
|
60
|
+
const targets = [];
|
|
61
|
+
let optionsEnded = false;
|
|
62
|
+
for (const word of command.words.slice(1)) {
|
|
63
|
+
if (!optionsEnded) {
|
|
64
|
+
if (word.text === '--') {
|
|
65
|
+
optionsEnded = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// A lone `-` is a file operand, not a flag — GNU tee writes a literal `-` file.
|
|
69
|
+
if (word.text.startsWith('-') && word.text !== '-')
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (word.opaque)
|
|
73
|
+
continue;
|
|
74
|
+
targets.push({ path: word.text, rule: TEE_RULE_NAME });
|
|
75
|
+
}
|
|
76
|
+
return targets;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
/** True for any in-place flag form: `-i`, `-i<suffix>`, `--in-place`, `--in-place=<suffix>`. */
|
|
80
|
+
function isInPlaceFlag(text) {
|
|
81
|
+
return text.startsWith('-i') || text === '--in-place' || text.startsWith('--in-place=');
|
|
82
|
+
}
|
|
83
|
+
// The script-flag forms whose value arrives in the NEXT word. isScriptFlag must cover
|
|
84
|
+
// every flag listed here, or `scriptSkipped` mis-seeds and the wrong operand is dropped.
|
|
85
|
+
const SEPARATED_SCRIPT_FLAGS = ['-e', '-f', '--expression', '--file'];
|
|
86
|
+
/** True for any script-supplying flag form (`-e`/`-f` family) — its value is never a file. */
|
|
87
|
+
function isScriptFlag(text) {
|
|
88
|
+
return (SEPARATED_SCRIPT_FLAGS.includes(text) ||
|
|
89
|
+
text.startsWith('-e') ||
|
|
90
|
+
text.startsWith('-f') ||
|
|
91
|
+
text.startsWith('--expression=') ||
|
|
92
|
+
text.startsWith('--file='));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Reports the file operands of an in-place `sed` (first-word basename match, so
|
|
96
|
+
* `/usr/bin/sed` fires too). Without an in-place flag sed writes to stdout — silence.
|
|
97
|
+
* The `-e`/`-f` family is skipped with its value (the `-f` script file is read, not
|
|
98
|
+
* written); when no such flag is present the first positional operand is the sed script
|
|
99
|
+
* — skipped even after `--`. Wrapper commands (`sudo sed`) never fire — the judges'
|
|
100
|
+
* path-mention backstop covers them.
|
|
101
|
+
*/
|
|
102
|
+
export const sedInPlaceRule = {
|
|
103
|
+
name: SED_IN_PLACE_RULE_NAME,
|
|
104
|
+
detect(command) {
|
|
105
|
+
const first = command.words[0];
|
|
106
|
+
if (first === undefined || first.opaque)
|
|
107
|
+
return [];
|
|
108
|
+
if (commandBasename(first) !== 'sed')
|
|
109
|
+
return [];
|
|
110
|
+
const args = command.words.slice(1);
|
|
111
|
+
if (!args.some((word) => isInPlaceFlag(word.text)))
|
|
112
|
+
return [];
|
|
113
|
+
const targets = [];
|
|
114
|
+
let optionsEnded = false;
|
|
115
|
+
let skipNext = false;
|
|
116
|
+
// With no script flag anywhere, the first positional operand is the script itself.
|
|
117
|
+
let scriptSkipped = args.some((word) => isScriptFlag(word.text));
|
|
118
|
+
for (const word of args) {
|
|
119
|
+
if (skipNext) {
|
|
120
|
+
skipNext = false;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!optionsEnded) {
|
|
124
|
+
if (word.text === '--') {
|
|
125
|
+
optionsEnded = true;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
// A lone `-` is an operand, not a flag — same boundary as the tee rule.
|
|
129
|
+
if (word.text.startsWith('-') && word.text !== '-') {
|
|
130
|
+
// The separated flag forms carry their value in the NEXT word — skip it too.
|
|
131
|
+
if (SEPARATED_SCRIPT_FLAGS.includes(word.text))
|
|
132
|
+
skipNext = true;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (!scriptSkipped) {
|
|
137
|
+
scriptSkipped = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (word.opaque)
|
|
141
|
+
continue;
|
|
142
|
+
targets.push({ path: word.text, rule: SED_IN_PLACE_RULE_NAME });
|
|
143
|
+
}
|
|
144
|
+
return targets;
|
|
145
|
+
},
|
|
146
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seven relations, each answering a witness list rather than a boolean: an empty list
|
|
3
|
+
* means the relation holds, and every other list names the elements that broke it, in the
|
|
4
|
+
* order the extraction supplied them. No relation sorts or deduplicates, because the two
|
|
5
|
+
* surfaces that read a judgment agree on the witness order only if it is the input's.
|
|
6
|
+
*
|
|
7
|
+
* `empty`, `subset` and `ordered` are written directly; the other four are expansions over
|
|
8
|
+
* them — `nonEmpty` is the negation of `empty`, `equal` is `subset` in both directions,
|
|
9
|
+
* `implies` is `subset` of the two key projections, and `unchanged` is `equal` over the keys
|
|
10
|
+
* the two states share.
|
|
11
|
+
*/
|
|
12
|
+
import { type Items } from './extract-steps.ts';
|
|
13
|
+
/** One element for which a relation does not hold; `side` is `equal`'s two directions. */
|
|
14
|
+
export type Witness = {
|
|
15
|
+
readonly key: string;
|
|
16
|
+
readonly value: unknown;
|
|
17
|
+
readonly side?: 'left' | 'right';
|
|
18
|
+
};
|
|
19
|
+
/** The pre value behind an `unchanged` witness, for the message that reports the change. */
|
|
20
|
+
export type WitnessWithBefore = Witness & {
|
|
21
|
+
readonly before?: unknown;
|
|
22
|
+
};
|
|
23
|
+
/** Every item is a witness: the relation asked for none. */
|
|
24
|
+
export declare function relateEmpty(of: Items): readonly Witness[];
|
|
25
|
+
/**
|
|
26
|
+
* The negation of `empty`. Emptiness itself is the witness, so the one witness names the
|
|
27
|
+
* extraction rather than an element — there is no element to name.
|
|
28
|
+
*/
|
|
29
|
+
export declare function relateNonEmpty(of: Items, extractName: string): readonly Witness[];
|
|
30
|
+
/** The `of` items whose value occurs nowhere in `in`, in `of` order, duplicates kept. */
|
|
31
|
+
export declare function relateSubset(of: Items, inItems: Items): readonly Witness[];
|
|
32
|
+
/** `subset` both ways: the left-only items first, then the right-only ones. */
|
|
33
|
+
export declare function relateEqual(left: Items, right: Items): readonly Witness[];
|
|
34
|
+
/** `subset` of the key projections: the `of` items whose key `requires` does not carry. */
|
|
35
|
+
export declare function relateImplies(of: Items, requires: Items): readonly Witness[];
|
|
36
|
+
/**
|
|
37
|
+
* Adjacent pairs rise; `strict` forbids equal neighbours. The witness is the later item of
|
|
38
|
+
* each pair that breaks it, so a caller reading the first witness sees where the sequence
|
|
39
|
+
* turned — an ordering reduced to a sort would answer only whether it held.
|
|
40
|
+
*/
|
|
41
|
+
export declare function relateOrdered(of: Items, strict: boolean): readonly Witness[];
|
|
42
|
+
/**
|
|
43
|
+
* `equal` over the shared keys: the post item of every key both states carry whose value
|
|
44
|
+
* changed. A key only one side has is an addition or a removal, not a change.
|
|
45
|
+
*/
|
|
46
|
+
export declare function relateUnchanged(pre: Items, post: Items): readonly WitnessWithBefore[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seven relations, each answering a witness list rather than a boolean: an empty list
|
|
3
|
+
* means the relation holds, and every other list names the elements that broke it, in the
|
|
4
|
+
* order the extraction supplied them. No relation sorts or deduplicates, because the two
|
|
5
|
+
* surfaces that read a judgment agree on the witness order only if it is the input's.
|
|
6
|
+
*
|
|
7
|
+
* `empty`, `subset` and `ordered` are written directly; the other four are expansions over
|
|
8
|
+
* them — `nonEmpty` is the negation of `empty`, `equal` is `subset` in both directions,
|
|
9
|
+
* `implies` is `subset` of the two key projections, and `unchanged` is `equal` over the keys
|
|
10
|
+
* the two states share.
|
|
11
|
+
*/
|
|
12
|
+
import { canonical, comparatorFor, sameValue } from './extract-steps.js';
|
|
13
|
+
/** Every item is a witness: the relation asked for none. */
|
|
14
|
+
export function relateEmpty(of) {
|
|
15
|
+
return of.map(({ key, value }) => ({ key, value }));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The negation of `empty`. Emptiness itself is the witness, so the one witness names the
|
|
19
|
+
* extraction rather than an element — there is no element to name.
|
|
20
|
+
*/
|
|
21
|
+
export function relateNonEmpty(of, extractName) {
|
|
22
|
+
return of.length === 0 ? [{ key: extractName, value: null }] : [];
|
|
23
|
+
}
|
|
24
|
+
/** The `of` items whose value occurs nowhere in `in`, in `of` order, duplicates kept. */
|
|
25
|
+
export function relateSubset(of, inItems) {
|
|
26
|
+
const present = new Set(inItems.map((item) => canonical(item.value)));
|
|
27
|
+
return of
|
|
28
|
+
.filter((item) => !present.has(canonical(item.value)))
|
|
29
|
+
.map(({ key, value }) => ({ key, value }));
|
|
30
|
+
}
|
|
31
|
+
/** `subset` both ways: the left-only items first, then the right-only ones. */
|
|
32
|
+
export function relateEqual(left, right) {
|
|
33
|
+
return [
|
|
34
|
+
...relateSubset(left, right).map((witness) => ({ ...witness, side: 'left' })),
|
|
35
|
+
...relateSubset(right, left).map((witness) => ({ ...witness, side: 'right' })),
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
/** `subset` of the key projections: the `of` items whose key `requires` does not carry. */
|
|
39
|
+
export function relateImplies(of, requires) {
|
|
40
|
+
const keys = new Set(requires.map((item) => item.key));
|
|
41
|
+
return of.filter((item) => !keys.has(item.key)).map(({ key, value }) => ({ key, value }));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Adjacent pairs rise; `strict` forbids equal neighbours. The witness is the later item of
|
|
45
|
+
* each pair that breaks it, so a caller reading the first witness sees where the sequence
|
|
46
|
+
* turned — an ordering reduced to a sort would answer only whether it held.
|
|
47
|
+
*/
|
|
48
|
+
export function relateOrdered(of, strict) {
|
|
49
|
+
const witnesses = [];
|
|
50
|
+
const compare = comparatorFor(of);
|
|
51
|
+
for (let index = 1; index < of.length; index += 1) {
|
|
52
|
+
const order = compare(of[index - 1], of[index]);
|
|
53
|
+
if (order > 0 || (strict && order === 0)) {
|
|
54
|
+
witnesses.push({ key: of[index].key, value: of[index].value });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return witnesses;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `equal` over the shared keys: the post item of every key both states carry whose value
|
|
61
|
+
* changed. A key only one side has is an addition or a removal, not a change.
|
|
62
|
+
*/
|
|
63
|
+
export function relateUnchanged(pre, post) {
|
|
64
|
+
const before = new Map(pre.map((item) => [item.key, item.value]));
|
|
65
|
+
return post
|
|
66
|
+
.filter((item) => before.has(item.key) && !sameValue(before.get(item.key), item.value))
|
|
67
|
+
.map((item) => ({ key: item.key, value: item.value, before: before.get(item.key) }));
|
|
68
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `runCovenant` — the covenant execution wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Calls an in-process judge thunk the assembly has already bound its payload and options
|
|
5
|
+
* into, translates the thunk's exit-code equivalent by policy (1 → blocking 2), writes the
|
|
6
|
+
* break reason to stderr, and appends exactly one telemetry record per call via
|
|
7
|
+
* {@link appendRecordFailOpen} (the core's fail-open wrapper around its sole collector —
|
|
8
|
+
* no local logger). {@link translateExitCode} is pure.
|
|
9
|
+
*/
|
|
10
|
+
import { type CovenantVerdict, type EnforceLevel, EXIT_BREAK_BLOCKING, EXIT_UPHOLD, type SkipReason, type TelemetryEvent } from '@polydeukes/core';
|
|
11
|
+
import type { Break } from './declaration-engine.ts';
|
|
12
|
+
/** The wrapper's final verdict — `1` never escapes: a break becomes the blocking `2`. */
|
|
13
|
+
type WrapperExitCode = typeof EXIT_UPHOLD | typeof EXIT_BREAK_BLOCKING;
|
|
14
|
+
/**
|
|
15
|
+
* What a judge thunk answers: `0` uphold, `1` break, `2` unjudgeable, and `reason` naming
|
|
16
|
+
* the break for the agent that has to read it. `witnesses` carries the elements a
|
|
17
|
+
* declaration's break was found on; the body answers with values and the wrapper turns
|
|
18
|
+
* them into the row's fifth field.
|
|
19
|
+
*
|
|
20
|
+
* `skipped` is the body's own report that it evaluated no relation — exit 0 without a
|
|
21
|
+
* judgment. It is recorded as a `skipped` row carrying that token rather than as `passed`,
|
|
22
|
+
* which would read as a covenant upheld.
|
|
23
|
+
*/
|
|
24
|
+
export type JudgeOutcome = {
|
|
25
|
+
exitCode: number;
|
|
26
|
+
reason?: string;
|
|
27
|
+
witnesses?: readonly Break[];
|
|
28
|
+
skipped?: SkipReason;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* `runCovenant` specification.
|
|
32
|
+
*
|
|
33
|
+
* `body` is an in-process judge thunk with the payload and its options already bound by
|
|
34
|
+
* the assembly; it is the only judgment this wrapper performs. `subject` defaults to the
|
|
35
|
+
* `-` sentinel in telemetry when absent. `telemetryPath` is always an explicit argument.
|
|
36
|
+
* `enforce` selects the translation column: absent defaults to `block`. `witness` is the
|
|
37
|
+
* valve axis — a zero-arg thunk whose arguments the caller has already bound, consulted
|
|
38
|
+
* only once the body has run and its outcome translated to `blocked`.
|
|
39
|
+
*/
|
|
40
|
+
export type RunCovenantSpec = {
|
|
41
|
+
body: () => Promise<JudgeOutcome>;
|
|
42
|
+
label: string;
|
|
43
|
+
subject?: string;
|
|
44
|
+
telemetryPath: string;
|
|
45
|
+
enforce?: EnforceLevel;
|
|
46
|
+
witness?: () => boolean;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Translate a body outcome into the wrapper verdict and telemetry event (pure).
|
|
50
|
+
*
|
|
51
|
+
* `bodyExitCode === 0` (uphold) passes; every other outcome — a break report (`1`), the
|
|
52
|
+
* body's own fail-closed (`2`), any uninterpretable code (`3+`), or a body that answered
|
|
53
|
+
* nothing interpretable (`null`) — is fail-closed to the blocking `2` / `blocked`. The
|
|
54
|
+
* unconditional 1→2 translation lives here, isolated, so it has one place to evolve in.
|
|
55
|
+
*
|
|
56
|
+
* `enforce` relaxes ONLY the verdict cell: under `advise` a break report (`1`) becomes
|
|
57
|
+
* `0` / `advised` — recorded, not blocking. Every unjudgeable outcome (`2`, `3+`, `null`)
|
|
58
|
+
* stays `2` / `blocked` regardless of level.
|
|
59
|
+
*/
|
|
60
|
+
export declare function translateExitCode(bodyExitCode: number | null, enforce?: EnforceLevel): {
|
|
61
|
+
exitCode: WrapperExitCode;
|
|
62
|
+
event: TelemetryEvent;
|
|
63
|
+
};
|
|
64
|
+
/** Turn a pure judge's verdict into the outcome a thunk answers. */
|
|
65
|
+
export declare function outcomeFromVerdict(verdict: CovenantVerdict): JudgeOutcome;
|
|
66
|
+
/** The unjudgeable outcome: a misassembly or an input no judge could read (`2`, no reason). */
|
|
67
|
+
export declare const UNJUDGEABLE_OUTCOME: JudgeOutcome;
|
|
68
|
+
/** What one wrapped judgment answers — the final exit code and the telemetry event recorded. */
|
|
69
|
+
export type RunCovenantVerdict = {
|
|
70
|
+
exitCode: typeof EXIT_UPHOLD | typeof EXIT_BREAK_BLOCKING;
|
|
71
|
+
event: TelemetryEvent;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Run a covenant body through the wrapper.
|
|
75
|
+
*
|
|
76
|
+
* The order is judge → translate → valve: the body always runs, and only a `blocked`
|
|
77
|
+
* translation has anything for the valve to relax into `0` / `witnessed`. Whatever that
|
|
78
|
+
* leaves is recorded ONCE — one call, one row — so a witnessed break never leaves a
|
|
79
|
+
* `blocked` row beside its `witnessed` one.
|
|
80
|
+
*
|
|
81
|
+
* The break reason goes to stderr whenever the thunk carried one, whatever the level and
|
|
82
|
+
* whatever the final event: gating it on the verdict would leave `advised` mute and the
|
|
83
|
+
* valve silent about what it opened.
|
|
84
|
+
*
|
|
85
|
+
* A body that answers `skipped` evaluated no relation, so it never reaches the translation
|
|
86
|
+
* table or the valve: the row says so and the call upholds.
|
|
87
|
+
*
|
|
88
|
+
* Resolves with the wrapper's final `exitCode` (`0` or `2`) and the telemetry `event` that
|
|
89
|
+
* was recorded. The event is surfaced rather than left to callers: the valve is impure, so
|
|
90
|
+
* recomputing the event would consult it a second time. Logging is fail-open
|
|
91
|
+
* via {@link appendRecordFailOpen}: a telemetry failure never alters the verdict and never
|
|
92
|
+
* throws. The gate closes; the measurement stays open.
|
|
93
|
+
*/
|
|
94
|
+
export declare function runCovenant(spec: RunCovenantSpec): Promise<RunCovenantVerdict>;
|
|
95
|
+
export {};
|