@polydeukes/core 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +2 -1
- package/README.md +2 -1
- package/dist/algebra.d.ts +174 -0
- package/dist/algebra.js +438 -0
- package/dist/catalogue.d.ts +69 -0
- package/dist/catalogue.js +191 -0
- package/dist/config.d.ts +67 -67
- package/dist/config.js +85 -174
- package/dist/exit-codes.d.ts +10 -10
- package/dist/exit-codes.js +10 -10
- package/dist/fail-policy.d.ts +7 -7
- package/dist/fail-policy.js +7 -7
- package/dist/index.d.ts +13 -99
- package/dist/index.js +7 -57
- package/dist/is-plain-object.d.ts +4 -1
- package/dist/is-plain-object.js +4 -1
- package/dist/protected-paths.d.ts +3 -3
- package/dist/protected-paths.js +3 -3
- package/dist/protocol.d.ts +152 -0
- package/dist/protocol.js +121 -0
- package/dist/source-names.d.ts +10 -0
- package/dist/source-names.js +18 -0
- package/dist/telemetry.d.ts +46 -21
- package/dist/telemetry.js +79 -30
- package/dist/transcript.d.ts +18 -28
- package/dist/transcript.js +12 -20
- package/dist/validation.d.ts +24 -0
- package/dist/validation.js +44 -0
- package/package.json +3 -2
- package/schema/algebra-declaration.schema.json +402 -0
- package/schema/polydeukes.schema.json +54 -81
package/dist/index.js
CHANGED
|
@@ -1,68 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @polydeukes/core — the thin, domain- and agent-agnostic core.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* appendRecordFailOpen — the fail-open wrapper promoted by CORE-05).
|
|
4
|
+
* Alpha. Carries the covenant protocol, the ROI telemetry collector, and the config
|
|
5
|
+
* schema. Pure types and functions, except telemetry's confined I/O functions
|
|
6
|
+
* (appendRecord / readRecords / appendRecordFailOpen).
|
|
8
7
|
* See https://github.com/huskyhoochu/polydeukes
|
|
9
8
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
export { BINARY_COMBINATOR_NAMES, RELATION_NAMES, SUPPLY_POLICIES, validateAlgebraDeclaration, } from './algebra.js';
|
|
10
|
+
export { AXIS_NAMES, deriveShape, MECHANISM_NAMES, MECHANISM_SHAPES, } from './catalogue.js';
|
|
12
11
|
export { ConfigValidationError, DEFAULT_TELEMETRY_LOG_PATH, defineConfig, } from './config.js';
|
|
13
12
|
export { EXIT_BREAK_BLOCKING, EXIT_BREAK_NON_BLOCKING, EXIT_UPHOLD, } from './exit-codes.js';
|
|
14
13
|
export { failModeToExitCode, resolveFailMode, } from './fail-policy.js';
|
|
15
14
|
export { isPlainObject } from './is-plain-object.js';
|
|
16
15
|
export { normalizeProtectedPaths } from './protected-paths.js';
|
|
17
|
-
export {
|
|
16
|
+
export { allFileChanges, parseInput, verdictToExitCode, } from './protocol.js';
|
|
17
|
+
export { aggregateGain, appendRecord, appendRecordFailOpen, formatRecordLine, parseRecordLine, readRecords, runGain, SKIP_REASONS, } from './telemetry.js';
|
|
18
18
|
export { noopTranscript, transcriptFromInput, } from './transcript.js';
|
|
19
|
-
/**
|
|
20
|
-
* Deserialize stdin-JSON into a {@link CovenantInput} (the protocol's reverse direction).
|
|
21
|
-
*
|
|
22
|
-
* fail-closed (PRD §5.2): this never throws. Any failure — unparseable JSON, an empty
|
|
23
|
-
* payload, a parsed value that is not an object, or a missing required collection —
|
|
24
|
-
* resolves to a blocking `{ ok: false, exitCode: 2 }`. "Cannot judge" means block,
|
|
25
|
-
* so an unjudgeable input can never be mistaken for a valid one.
|
|
26
|
-
*/
|
|
27
|
-
export function parseInput(stdinJson) {
|
|
28
|
-
let parsed;
|
|
29
|
-
try {
|
|
30
|
-
parsed = JSON.parse(stdinJson);
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return { ok: false, exitCode: EXIT_BREAK_BLOCKING };
|
|
34
|
-
}
|
|
35
|
-
if (!isPlainObject(parsed)) {
|
|
36
|
-
return { ok: false, exitCode: EXIT_BREAK_BLOCKING };
|
|
37
|
-
}
|
|
38
|
-
const candidate = parsed;
|
|
39
|
-
if (!Array.isArray(candidate.toolCalls) ||
|
|
40
|
-
!Array.isArray(candidate.subagentSpawns) ||
|
|
41
|
-
!Array.isArray(candidate.userMessages)) {
|
|
42
|
-
return { ok: false, exitCode: EXIT_BREAK_BLOCKING };
|
|
43
|
-
}
|
|
44
|
-
return { ok: true, value: candidate };
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Flatten every call's evidence into one array in call order (CORE-06 §4.1).
|
|
48
|
-
*
|
|
49
|
-
* The one traversal for consumers that need no attribution (discipline scope, delta
|
|
50
|
-
* judging): calls without evidence are skipped, never substituted for.
|
|
51
|
-
*/
|
|
52
|
-
export function allFileChanges(input) {
|
|
53
|
-
const changes = [];
|
|
54
|
-
for (const call of input.toolCalls) {
|
|
55
|
-
if (call.fileChange !== undefined)
|
|
56
|
-
changes.push(call.fileChange);
|
|
57
|
-
}
|
|
58
|
-
return changes;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Map a {@link CovenantVerdict} to an exit code (the protocol's forward direction).
|
|
62
|
-
*
|
|
63
|
-
* Responsibility boundary (PRD §4.1): the body emits `0` when upheld and `1` when
|
|
64
|
-
* broken — never the blocking `2`. Translating `1` into `2` is the wrapper's policy.
|
|
65
|
-
*/
|
|
66
|
-
export function verdictToExitCode(verdict) {
|
|
67
|
-
return verdict.upheld ? EXIT_UPHOLD : EXIT_BREAK_NON_BLOCKING;
|
|
68
|
-
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `isPlainObject` — the workspace's single canonical plain-object predicate.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* typeof `object`, non-null, not an array.
|
|
5
|
+
*
|
|
6
|
+
* Its own file because it is a public export the adapters and the umbrella import; the
|
|
7
|
+
* core-internal validation helpers live in `validation.ts` instead.
|
|
5
8
|
*/
|
|
6
9
|
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
package/dist/is-plain-object.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `isPlainObject` — the workspace's single canonical plain-object predicate.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* typeof `object`, non-null, not an array.
|
|
5
|
+
*
|
|
6
|
+
* Its own file because it is a public export the adapters and the umbrella import; the
|
|
7
|
+
* core-internal validation helpers live in `validation.ts` instead.
|
|
5
8
|
*/
|
|
6
9
|
export function isPlainObject(value) {
|
|
7
10
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Protected-path normalization — the `protectedPaths` list normalized into the literal
|
|
3
|
-
* path strings the dispatcher contract expects
|
|
4
|
-
*
|
|
3
|
+
* path strings the dispatcher contract expects. Pure string transformation — zero file
|
|
4
|
+
* I/O, no glob expansion, no path resolution.
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
|
-
* Normalize the protection surface from a config-shaped spec
|
|
7
|
+
* Normalize the protection surface from a config-shaped spec.
|
|
8
8
|
*
|
|
9
9
|
* Processing order: trim each entry → strip a leading `./` → strip a trailing `/` → drop
|
|
10
10
|
* empty-equivalent entries → dedupe on the normalized value, keeping the first occurrence.
|
package/dist/protected-paths.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Protected-path normalization — the `protectedPaths` list normalized into the literal
|
|
3
|
-
* path strings the dispatcher contract expects
|
|
4
|
-
*
|
|
3
|
+
* path strings the dispatcher contract expects. Pure string transformation — zero file
|
|
4
|
+
* I/O, no glob expansion, no path resolution.
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
|
-
* Normalize the protection surface from a config-shaped spec
|
|
7
|
+
* Normalize the protection surface from a config-shaped spec.
|
|
8
8
|
*
|
|
9
9
|
* Processing order: trim each entry → strip a leading `./` → strip a trailing `/` → drop
|
|
10
10
|
* empty-equivalent entries → dedupe on the normalized value, keeping the first occurrence.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* protocol — the covenant protocol: the input IR, the verdict, and the two directions
|
|
3
|
+
* (parse stdin-JSON in, map a verdict to an exit code out).
|
|
4
|
+
*/
|
|
5
|
+
import type { TelemetryEvent } from './telemetry.ts';
|
|
6
|
+
/**
|
|
7
|
+
* `FileChange` — one file's mutation evidence around the judged call.
|
|
8
|
+
*
|
|
9
|
+
* Agent-neutral, discriminated by `kind`: a deletion is first-class evidence rather
|
|
10
|
+
* than an unrepresentable case, and impossible states (a deletion with resulting
|
|
11
|
+
* content, a creation with a baseline) cannot be written down. Adapters fill this from
|
|
12
|
+
* their own sources (virtual apply, git blobs) — the core only transports it.
|
|
13
|
+
* `delete.pre` is the readable text baseline when one exists — absent for a binary
|
|
14
|
+
* blob, because a deletion needs no content to be judged.
|
|
15
|
+
*/
|
|
16
|
+
export type FileChange = {
|
|
17
|
+
kind: 'create';
|
|
18
|
+
path: string;
|
|
19
|
+
post: string;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'modify';
|
|
22
|
+
path: string;
|
|
23
|
+
pre: string;
|
|
24
|
+
post: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'delete';
|
|
27
|
+
path: string;
|
|
28
|
+
pre?: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* `SourceReader` — a repo-relative path in, the text a surface observes there or absence out.
|
|
32
|
+
*
|
|
33
|
+
* The one signature the supply layer injects and every surface's reader implements. Absence
|
|
34
|
+
* is `undefined` and every other failure throws, so a permission refusal reaches the root's
|
|
35
|
+
* fail-closed path instead of passing for a file that is not there.
|
|
36
|
+
*/
|
|
37
|
+
export type SourceReader = (path: string) => string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* `ChannelReader` — a channel kind in, the text the surface observes on that channel or
|
|
40
|
+
* absence out. Absence is `undefined`, same contract as {@link SourceReader}.
|
|
41
|
+
*/
|
|
42
|
+
export type ChannelReader = (kind: string) => string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* `Actor` — who made the observation the judgment is about.
|
|
45
|
+
*
|
|
46
|
+
* `agentType` is the subagent kind the host names when the call comes from one. The empty
|
|
47
|
+
* object is a positive value: the host observed the actor and it is not a subagent.
|
|
48
|
+
*/
|
|
49
|
+
export type Actor = {
|
|
50
|
+
agentType?: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* `CovenantInput` — the agent-neutral input IR a covenant judges.
|
|
54
|
+
*
|
|
55
|
+
* Adapters up-translate their own agent payloads into this shape and pipe it as
|
|
56
|
+
* stdin-JSON. The vocabulary carries no agent/tool literals; concrete tool or
|
|
57
|
+
* subagent names are *values* an adapter fills in, never part of the core's type.
|
|
58
|
+
* Evidence has exactly one home, the call element it belongs to: `fileChange` absent
|
|
59
|
+
* means "this call is unproven", and no sibling call's evidence can stand in for it.
|
|
60
|
+
*/
|
|
61
|
+
export type CovenantInput = {
|
|
62
|
+
toolCalls: {
|
|
63
|
+
name: string;
|
|
64
|
+
args?: Record<string, unknown>;
|
|
65
|
+
fileChange?: FileChange;
|
|
66
|
+
}[];
|
|
67
|
+
subagentSpawns: {
|
|
68
|
+
kind: string;
|
|
69
|
+
}[];
|
|
70
|
+
userMessages: {
|
|
71
|
+
text: string;
|
|
72
|
+
}[];
|
|
73
|
+
/**
|
|
74
|
+
* The world axis: files a supply layer read for the judgment (key = repo-relative path,
|
|
75
|
+
* an absent key = an absent file, never `null` — that is `FileChange.pre`'s creation
|
|
76
|
+
* marker), the observation unit's change set when the host sees wider than the changes
|
|
77
|
+
* this input carries, and the channels a surface supplied — `sidecar` is the spawn-record
|
|
78
|
+
* list as JSON text, where `'[]'` says the channel observed no spawn and an absent key
|
|
79
|
+
* says there is no channel at all.
|
|
80
|
+
*/
|
|
81
|
+
world?: {
|
|
82
|
+
files?: Record<string, string>;
|
|
83
|
+
changes?: string[];
|
|
84
|
+
channels?: {
|
|
85
|
+
sidecar?: string;
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* The observation's actor, when the host proves one. Absence means the host could not
|
|
90
|
+
* prove one and is never turned into `{}` — what an absent actor means is a
|
|
91
|
+
* declaration's own supply policy.
|
|
92
|
+
*/
|
|
93
|
+
actor?: Actor;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* `CovenantVerdict` — the result a covenant body produces.
|
|
97
|
+
*
|
|
98
|
+
* Either the promise was upheld, or it was broken with a human-readable reason.
|
|
99
|
+
* Maps to an exit code via {@link verdictToExitCode}.
|
|
100
|
+
*/
|
|
101
|
+
export type CovenantVerdict = {
|
|
102
|
+
upheld: true;
|
|
103
|
+
} | {
|
|
104
|
+
upheld: false;
|
|
105
|
+
reason: string;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* `DispatchOutcome` — the protocol-level result of one dispatch over many covenants.
|
|
109
|
+
*
|
|
110
|
+
* The blocking exit code plus one entry per judged covenant: `label` names which covenant
|
|
111
|
+
* produced the entry, and `event` is the telemetry word the wrapper already recorded for
|
|
112
|
+
* it. The event rides on the entry rather than being recomputed by a reader — the witness
|
|
113
|
+
* valve is impure, so recomputing would consult it a second time for one verdict.
|
|
114
|
+
*/
|
|
115
|
+
export type DispatchOutcome = {
|
|
116
|
+
exitCode: 0 | 2;
|
|
117
|
+
results: {
|
|
118
|
+
label: string;
|
|
119
|
+
exitCode: 0 | 2;
|
|
120
|
+
event: TelemetryEvent;
|
|
121
|
+
}[];
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Deserialize stdin-JSON into a {@link CovenantInput} (the protocol's reverse direction).
|
|
125
|
+
*
|
|
126
|
+
* fail-closed: this never throws. Any failure — unparseable JSON, an empty
|
|
127
|
+
* payload, a parsed value that is not an object, a missing required collection, a
|
|
128
|
+
* malformed world axis, or a malformed actor — resolves to a blocking
|
|
129
|
+
* `{ ok: false, exitCode: 2 }`. "Cannot judge" means block, so an unjudgeable input can
|
|
130
|
+
* never be mistaken for a valid one.
|
|
131
|
+
*/
|
|
132
|
+
export declare function parseInput(stdinJson: string): {
|
|
133
|
+
ok: true;
|
|
134
|
+
value: CovenantInput;
|
|
135
|
+
} | {
|
|
136
|
+
ok: false;
|
|
137
|
+
exitCode: 2;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Flatten every call's evidence into one array in call order.
|
|
141
|
+
*
|
|
142
|
+
* The one traversal for consumers that need no attribution (discipline scope, delta
|
|
143
|
+
* judging): calls without evidence are skipped, never substituted for.
|
|
144
|
+
*/
|
|
145
|
+
export declare function allFileChanges(input: CovenantInput): FileChange[];
|
|
146
|
+
/**
|
|
147
|
+
* Map a {@link CovenantVerdict} to an exit code (the protocol's forward direction).
|
|
148
|
+
*
|
|
149
|
+
* Responsibility boundary: the body emits `0` when upheld and `1` when
|
|
150
|
+
* broken — never the blocking `2`. Translating `1` into `2` is the wrapper's policy.
|
|
151
|
+
*/
|
|
152
|
+
export declare function verdictToExitCode(verdict: CovenantVerdict): 0 | 1;
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* protocol — the covenant protocol: the input IR, the verdict, and the two directions
|
|
3
|
+
* (parse stdin-JSON in, map a verdict to an exit code out).
|
|
4
|
+
*/
|
|
5
|
+
import { EXIT_BREAK_BLOCKING, EXIT_BREAK_NON_BLOCKING, EXIT_UPHOLD } from './exit-codes.js';
|
|
6
|
+
import { isPlainObject } from './is-plain-object.js';
|
|
7
|
+
/** The channel kinds a world may carry, closed. A name outside it supplies nothing. */
|
|
8
|
+
const CHANNEL_KINDS = new Set(['sidecar']);
|
|
9
|
+
/**
|
|
10
|
+
* Whether a value is the world axis: a plain object carrying nothing but a `files` record
|
|
11
|
+
* of strings, a `changes` list of strings, and a `channels` record of channel texts.
|
|
12
|
+
*
|
|
13
|
+
* Closed at three fields, and all shape-checked: a supplier writing under a misspelt key
|
|
14
|
+
* supplies nothing while looking like a supply, and a `null` under a path or a channel kind
|
|
15
|
+
* would pass the engine's key-presence test as a supplied value whose text is missing.
|
|
16
|
+
*/
|
|
17
|
+
function isWorld(value) {
|
|
18
|
+
if (!isPlainObject(value))
|
|
19
|
+
return false;
|
|
20
|
+
for (const key of Object.keys(value)) {
|
|
21
|
+
if (key !== 'files' && key !== 'changes' && key !== 'channels')
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const { files, changes, channels } = value;
|
|
25
|
+
if (channels !== undefined) {
|
|
26
|
+
if (!isPlainObject(channels))
|
|
27
|
+
return false;
|
|
28
|
+
for (const [kind, text] of Object.entries(channels)) {
|
|
29
|
+
if (!CHANNEL_KINDS.has(kind))
|
|
30
|
+
return false;
|
|
31
|
+
if (typeof text !== 'string')
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (files !== undefined) {
|
|
36
|
+
if (!isPlainObject(files))
|
|
37
|
+
return false;
|
|
38
|
+
if (!Object.values(files).every((text) => typeof text === 'string'))
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (changes !== undefined) {
|
|
42
|
+
if (!Array.isArray(changes))
|
|
43
|
+
return false;
|
|
44
|
+
if (!changes.every((path) => typeof path === 'string'))
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Whether a value is the actor: a plain object carrying nothing but a string `agentType`.
|
|
51
|
+
*
|
|
52
|
+
* Closed at one key, same reason as {@link isWorld}: a host writing its own payload
|
|
53
|
+
* spelling of the actor key under `actor` would supply nothing while looking like a supply,
|
|
54
|
+
* and a non-string `agentType` reaches a pattern step as a value no regex matches.
|
|
55
|
+
*/
|
|
56
|
+
function isActor(value) {
|
|
57
|
+
if (!isPlainObject(value))
|
|
58
|
+
return false;
|
|
59
|
+
for (const key of Object.keys(value)) {
|
|
60
|
+
if (key !== 'agentType')
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return value.agentType === undefined || typeof value.agentType === 'string';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Deserialize stdin-JSON into a {@link CovenantInput} (the protocol's reverse direction).
|
|
67
|
+
*
|
|
68
|
+
* fail-closed: this never throws. Any failure — unparseable JSON, an empty
|
|
69
|
+
* payload, a parsed value that is not an object, a missing required collection, a
|
|
70
|
+
* malformed world axis, or a malformed actor — resolves to a blocking
|
|
71
|
+
* `{ ok: false, exitCode: 2 }`. "Cannot judge" means block, so an unjudgeable input can
|
|
72
|
+
* never be mistaken for a valid one.
|
|
73
|
+
*/
|
|
74
|
+
export function parseInput(stdinJson) {
|
|
75
|
+
let parsed;
|
|
76
|
+
try {
|
|
77
|
+
parsed = JSON.parse(stdinJson);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return { ok: false, exitCode: EXIT_BREAK_BLOCKING };
|
|
81
|
+
}
|
|
82
|
+
if (!isPlainObject(parsed)) {
|
|
83
|
+
return { ok: false, exitCode: EXIT_BREAK_BLOCKING };
|
|
84
|
+
}
|
|
85
|
+
const candidate = parsed;
|
|
86
|
+
if (!Array.isArray(candidate.toolCalls) ||
|
|
87
|
+
!Array.isArray(candidate.subagentSpawns) ||
|
|
88
|
+
!Array.isArray(candidate.userMessages)) {
|
|
89
|
+
return { ok: false, exitCode: EXIT_BREAK_BLOCKING };
|
|
90
|
+
}
|
|
91
|
+
if (candidate.world !== undefined && !isWorld(candidate.world)) {
|
|
92
|
+
return { ok: false, exitCode: EXIT_BREAK_BLOCKING };
|
|
93
|
+
}
|
|
94
|
+
if (candidate.actor !== undefined && !isActor(candidate.actor)) {
|
|
95
|
+
return { ok: false, exitCode: EXIT_BREAK_BLOCKING };
|
|
96
|
+
}
|
|
97
|
+
return { ok: true, value: candidate };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Flatten every call's evidence into one array in call order.
|
|
101
|
+
*
|
|
102
|
+
* The one traversal for consumers that need no attribution (discipline scope, delta
|
|
103
|
+
* judging): calls without evidence are skipped, never substituted for.
|
|
104
|
+
*/
|
|
105
|
+
export function allFileChanges(input) {
|
|
106
|
+
const changes = [];
|
|
107
|
+
for (const call of input.toolCalls) {
|
|
108
|
+
if (call.fileChange !== undefined)
|
|
109
|
+
changes.push(call.fileChange);
|
|
110
|
+
}
|
|
111
|
+
return changes;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Map a {@link CovenantVerdict} to an exit code (the protocol's forward direction).
|
|
115
|
+
*
|
|
116
|
+
* Responsibility boundary: the body emits `0` when upheld and `1` when
|
|
117
|
+
* broken — never the blocking `2`. Translating `1` into `2` is the wrapper's policy.
|
|
118
|
+
*/
|
|
119
|
+
export function verdictToExitCode(verdict) {
|
|
120
|
+
return verdict.upheld ? EXIT_UPHOLD : EXIT_BREAK_NON_BLOCKING;
|
|
121
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `source-names.ts` — the source names every world carries on its own.
|
|
3
|
+
*
|
|
4
|
+
* Both the declaration grammar and the mechanism catalogue read this list, so it lives
|
|
5
|
+
* apart from either: the grammar refuses a `sources` binding that shadows one of them, and
|
|
6
|
+
* the catalogue derives an axis from each — the change axis from the six that read the
|
|
7
|
+
* change, the actor axis from `actor`.
|
|
8
|
+
*/
|
|
9
|
+
/** The source names the world supplies on its own; a `sources` entry may not shadow one. */
|
|
10
|
+
export declare const FIXED_SOURCE_NAMES: readonly ['target.path', 'pre', 'post', 'state', 'changes', 'command', 'actor'];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `source-names.ts` — the source names every world carries on its own.
|
|
3
|
+
*
|
|
4
|
+
* Both the declaration grammar and the mechanism catalogue read this list, so it lives
|
|
5
|
+
* apart from either: the grammar refuses a `sources` binding that shadows one of them, and
|
|
6
|
+
* the catalogue derives an axis from each — the change axis from the six that read the
|
|
7
|
+
* change, the actor axis from `actor`.
|
|
8
|
+
*/
|
|
9
|
+
/** The source names the world supplies on its own; a `sources` entry may not shadow one. */
|
|
10
|
+
export const FIXED_SOURCE_NAMES = [
|
|
11
|
+
'target.path',
|
|
12
|
+
'pre',
|
|
13
|
+
'post',
|
|
14
|
+
'state',
|
|
15
|
+
'changes',
|
|
16
|
+
'command',
|
|
17
|
+
'actor',
|
|
18
|
+
];
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ROI telemetry — the single shared collector and its `gain` aggregation
|
|
2
|
+
* ROI telemetry — the single shared collector and its `gain` aggregation.
|
|
3
3
|
*
|
|
4
|
-
* One record is one line of
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* than building its own logger.
|
|
4
|
+
* One record is one line of TSV — four fields, or five when a judgment names witnesses;
|
|
5
|
+
* one append is one write call. I/O is confined to
|
|
6
|
+
* exactly two functions — {@link appendRecord} (the only write) and {@link readRecords}
|
|
7
|
+
* (the only read). Formatting, parsing, and aggregation are pure.
|
|
9
8
|
*/
|
|
10
9
|
/**
|
|
11
10
|
* The six telemetry events. `witnessed` is a first-class event, not a flag on `passed`:
|
|
@@ -21,16 +20,33 @@
|
|
|
21
20
|
*/
|
|
22
21
|
export type TelemetryEvent = 'passed' | 'blocked' | 'witnessed' | 'advised' | 'skipped' | 'unattributed';
|
|
23
22
|
/**
|
|
24
|
-
* `
|
|
23
|
+
* Why a `skipped` row records no judgment, closed: the surface has no observation channel
|
|
24
|
+
* for what the entry reads, assembly could not compile the entry, or the declaration's own
|
|
25
|
+
* `supply: pass` let an absent source through.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SKIP_REASONS: readonly ['no-observation', 'config-fault', 'supply-pass'];
|
|
28
|
+
/** One of the three skip reasons — the closed vocabulary of the fifth field on a skip row. */
|
|
29
|
+
export type SkipReason = (typeof SKIP_REASONS)[number];
|
|
30
|
+
/**
|
|
31
|
+
* `TelemetryRecord` — one measured covenant outcome.
|
|
25
32
|
*
|
|
26
33
|
* `subject` is the judged target (a file path, etc.); `-` is the documented sentinel
|
|
27
34
|
* for "no subject", carried round-trip like any other value.
|
|
35
|
+
*
|
|
36
|
+
* `witnesses` is the optional fifth field: an already-serialized JSON string naming what
|
|
37
|
+
* a judgment found broken. Only a judgment that produced witness elements carries it, so
|
|
38
|
+
* every other producer's row keeps its four fields.
|
|
39
|
+
*
|
|
40
|
+
* `reason` is the other reading of that fifth field, and only a `skipped` row takes it: a
|
|
41
|
+
* skip has no witness to name, so the two never contend for the slot.
|
|
28
42
|
*/
|
|
29
43
|
export type TelemetryRecord = {
|
|
30
44
|
timestamp: string;
|
|
31
45
|
event: TelemetryEvent;
|
|
32
46
|
label: string;
|
|
33
47
|
subject: string;
|
|
48
|
+
witnesses?: string;
|
|
49
|
+
reason?: SkipReason;
|
|
34
50
|
};
|
|
35
51
|
/** Per-label event counts, keyed by label then event. */
|
|
36
52
|
export type GainSummary = {
|
|
@@ -41,26 +57,35 @@ export type GainSummary = {
|
|
|
41
57
|
* Serialize a {@link TelemetryRecord} into one newline-terminated TSV line (pure).
|
|
42
58
|
*
|
|
43
59
|
* The returned string already includes the trailing `\n`, so {@link appendRecord}
|
|
44
|
-
* writes it verbatim in a single call.
|
|
60
|
+
* writes it verbatim in a single call. The fifth field appears only when the record
|
|
61
|
+
* carries `witnesses` or a skip `reason`; a record without either writes the four-field
|
|
62
|
+
* line unchanged.
|
|
63
|
+
*
|
|
64
|
+
* Throws when a record claims the fifth field twice, or claims it as a reason on an event
|
|
65
|
+
* that is not `skipped`: both are assembly errors, and writing either one would produce a
|
|
66
|
+
* row no reader can take apart.
|
|
45
67
|
*/
|
|
46
68
|
export declare function formatRecordLine(record: TelemetryRecord): string;
|
|
47
69
|
/**
|
|
48
70
|
* Parse one TSV line back into a {@link TelemetryRecord}, or `null` if malformed (pure).
|
|
49
71
|
*
|
|
50
72
|
* Tolerates a trailing newline (so it round-trips {@link formatRecordLine}). Returns
|
|
51
|
-
* `null` for
|
|
52
|
-
* empty line — a malformed line is rejected, never
|
|
53
|
-
*
|
|
73
|
+
* `null` for a field count outside four (no fifth field) and five (with one), an event
|
|
74
|
+
* outside the six valid events, or an empty line — a malformed line is rejected, never
|
|
75
|
+
* coerced into a bogus record. The fifth field is read per event: on `skipped` it is a
|
|
76
|
+
* token of {@link SKIP_REASONS} and nothing else, on every other event a JSON array of
|
|
77
|
+
* witnesses. The one exception is {@link LEGACY_WITNESSED_EVENT}, which reads back as
|
|
78
|
+
* `witnessed`.
|
|
54
79
|
*/
|
|
55
80
|
export declare function parseRecordLine(line: string): TelemetryRecord | null;
|
|
56
81
|
/**
|
|
57
|
-
* Append one record to the log at `path` — the only write I/O
|
|
82
|
+
* Append one record to the log at `path` — the only write I/O.
|
|
58
83
|
*
|
|
59
84
|
* Exactly one {@link appendFileSync} call per record, writing {@link formatRecordLine}
|
|
60
85
|
* verbatim. Relying on POSIX `O_APPEND` single-write semantics, concurrent appends do
|
|
61
86
|
* not interleave lines.
|
|
62
87
|
*
|
|
63
|
-
* fail-open
|
|
88
|
+
* fail-open: any fs failure — bad path, permissions, disk — returns
|
|
64
89
|
* `{ ok: false }` and never throws. This is deliberately the opposite direction of the
|
|
65
90
|
* covenant path's fail-closed: the worst outcome of telemetry is a missing datum, never
|
|
66
91
|
* a blocked workflow.
|
|
@@ -69,16 +94,16 @@ export declare function appendRecord(path: string, record: TelemetryRecord): {
|
|
|
69
94
|
ok: boolean;
|
|
70
95
|
};
|
|
71
96
|
/**
|
|
72
|
-
* Append one telemetry record fail-open, timestamping it here
|
|
97
|
+
* Append one telemetry record fail-open, timestamping it here.
|
|
73
98
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
99
|
+
* {@link appendRecord} is deliberately mkdir-free — for it an absent directory is just a
|
|
100
|
+
* fail-open `{ ok: false }` — so this wrapper carries the parent-directory guarantee. The
|
|
101
|
+
* mkdir and the append share one try block, and a failure of either never alters the
|
|
102
|
+
* caller's verdict and never propagates.
|
|
78
103
|
*/
|
|
79
104
|
export declare function appendRecordFailOpen(telemetryPath: string, record: Omit<TelemetryRecord, 'timestamp'>): void;
|
|
80
105
|
/**
|
|
81
|
-
* Read every record from the log at `path` — the only read I/O
|
|
106
|
+
* Read every record from the log at `path` — the only read I/O.
|
|
82
107
|
*
|
|
83
108
|
* fail-open: an absent file or any read error returns `{ records: [], skipped: 0 }`
|
|
84
109
|
* (an absent log means "nothing collected yet"), never throwing. Corrupt lines
|
|
@@ -90,14 +115,14 @@ export declare function readRecords(path: string): {
|
|
|
90
115
|
skipped: number;
|
|
91
116
|
};
|
|
92
117
|
/**
|
|
93
|
-
* Aggregate records into per-label event counts (
|
|
118
|
+
* Aggregate records into per-label event counts (pure).
|
|
94
119
|
*
|
|
95
120
|
* Each label gets its own counter across all six events, so a corrupt or missing
|
|
96
121
|
* event never bleeds counts between labels.
|
|
97
122
|
*/
|
|
98
123
|
export declare function aggregateGain(records: TelemetryRecord[]): GainSummary;
|
|
99
124
|
/**
|
|
100
|
-
* `gain` entry point — read the log at `path`, aggregate, and render
|
|
125
|
+
* `gain` entry point — read the log at `path`, aggregate, and render.
|
|
101
126
|
*
|
|
102
127
|
* Composes {@link readRecords} + {@link aggregateGain} + a pure renderer. An absent or
|
|
103
128
|
* empty log yields `no telemetry collected`; a corrupt line is skipped upstream, reported
|