@polydeukes/core 0.5.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/dist/index.d.ts CHANGED
@@ -6,97 +6,13 @@
6
6
  * (appendRecord / readRecords / appendRecordFailOpen).
7
7
  * See https://github.com/huskyhoochu/polydeukes
8
8
  */
9
- export { ConfigValidationError, DEFAULT_TELEMETRY_LOG_PATH, type DisciplineDraft, type DisciplineEntry, type DisciplineForbid, defineConfig, type EnforceLevel, type LanguageProfile, type PolydeukesConfig, type ResolvedConfig, type ResolvedLanguageProfile, } from './config.js';
10
- export { EXIT_BREAK_BLOCKING, EXIT_BREAK_NON_BLOCKING, EXIT_UPHOLD, } from './exit-codes.js';
11
- export { type FailMode, type FailureKind, failModeToExitCode, resolveFailMode, } from './fail-policy.js';
12
- export { isPlainObject } from './is-plain-object.js';
13
- export { normalizeProtectedPaths } from './protected-paths.js';
14
- export { aggregateGain, appendRecord, appendRecordFailOpen, formatRecordLine, type GainSummary, parseRecordLine, readRecords, runGain, type TelemetryEvent, type TelemetryRecord, } from './telemetry.js';
15
- export { type CanonicalTranscript, noopTranscript, type SubagentInvocation, type TranscriptToolCall, type TranscriptUserMessage, transcriptFromInput, } from './transcript.js';
16
- /**
17
- * `FileChange` one file's mutation evidence around the judged call.
18
- *
19
- * Agent-neutral, discriminated by `kind`: a deletion is first-class evidence rather
20
- * than an unrepresentable case, and impossible states (a deletion with resulting
21
- * content, a creation with a baseline) cannot be written down. Adapters fill this from
22
- * their own sources (virtual apply, git blobs) — the core only transports it.
23
- * `delete.pre` is the readable text baseline when one exists — absent for a binary
24
- * blob, because a deletion needs no content to be judged.
25
- */
26
- export type FileChange = {
27
- kind: 'create';
28
- path: string;
29
- post: string;
30
- } | {
31
- kind: 'modify';
32
- path: string;
33
- pre: string;
34
- post: string;
35
- } | {
36
- kind: 'delete';
37
- path: string;
38
- pre?: string;
39
- };
40
- /**
41
- * `CovenantInput` — the agent-neutral input IR a covenant judges.
42
- *
43
- * Adapters up-translate their own agent payloads into this shape and pipe it as
44
- * stdin-JSON. The vocabulary carries no agent/tool literals; concrete tool or
45
- * subagent names are *values* an adapter fills in, never part of the core's type.
46
- * Evidence has exactly one home, the call element it belongs to: `fileChange` absent
47
- * means "this call is unproven", and no sibling call's evidence can stand in for it.
48
- */
49
- export type CovenantInput = {
50
- toolCalls: {
51
- name: string;
52
- args?: Record<string, unknown>;
53
- fileChange?: FileChange;
54
- }[];
55
- subagentSpawns: {
56
- kind: string;
57
- }[];
58
- userMessages: {
59
- text: string;
60
- }[];
61
- };
62
- /**
63
- * `CovenantVerdict` — the result a covenant body produces.
64
- *
65
- * Either the promise was upheld, or it was broken with a human-readable reason.
66
- * Maps to an exit code via {@link verdictToExitCode}.
67
- */
68
- export type CovenantVerdict = {
69
- upheld: true;
70
- } | {
71
- upheld: false;
72
- reason: string;
73
- };
74
- /**
75
- * Deserialize stdin-JSON into a {@link CovenantInput} (the protocol's reverse direction).
76
- *
77
- * fail-closed: this never throws. Any failure — unparseable JSON, an empty
78
- * payload, a parsed value that is not an object, or a missing required collection —
79
- * resolves to a blocking `{ ok: false, exitCode: 2 }`. "Cannot judge" means block,
80
- * so an unjudgeable input can never be mistaken for a valid one.
81
- */
82
- export declare function parseInput(stdinJson: string): {
83
- ok: true;
84
- value: CovenantInput;
85
- } | {
86
- ok: false;
87
- exitCode: 2;
88
- };
89
- /**
90
- * Flatten every call's evidence into one array in call order.
91
- *
92
- * The one traversal for consumers that need no attribution (discipline scope, delta
93
- * judging): calls without evidence are skipped, never substituted for.
94
- */
95
- export declare function allFileChanges(input: CovenantInput): FileChange[];
96
- /**
97
- * Map a {@link CovenantVerdict} to an exit code (the protocol's forward direction).
98
- *
99
- * Responsibility boundary: the body emits `0` when upheld and `1` when
100
- * broken — never the blocking `2`. Translating `1` into `2` is the wrapper's policy.
101
- */
102
- export declare function verdictToExitCode(verdict: CovenantVerdict): 0 | 1;
9
+ export { type AlgebraDeclaration, BINARY_COMBINATOR_NAMES, type BinaryStep, type ExtractBlock, type ExtractStep, RELATION_NAMES, type RelateEntry, type RelationDecl, type RelationName, type ScopeBlock, SUPPLY_POLICIES, type SupplyBlock, type UnaryStep, validateAlgebraDeclaration, type Witness, type WitnessBlock, type Witnesses, } from './algebra.ts';
10
+ export { AXIS_NAMES, type Axis, type DerivableDeclaration, deriveShape, MECHANISM_NAMES, MECHANISM_SHAPES, type MechanismName, type MechanismShape, } from './catalogue.ts';
11
+ export { type AlgebraDeclarationBody, ConfigValidationError, DEFAULT_TELEMETRY_LOG_PATH, type DisciplineDraft, type DisciplineEntry, defineConfig, type EnforceLevel, type LanguageProfile, type PolydeukesConfig, type ResolvedConfig, type ResolvedLanguageProfile, } from './config.ts';
12
+ export { EXIT_BREAK_BLOCKING, EXIT_BREAK_NON_BLOCKING, EXIT_UPHOLD, } from './exit-codes.ts';
13
+ export { type FailMode, type FailureKind, failModeToExitCode, resolveFailMode, } from './fail-policy.ts';
14
+ export { isPlainObject } from './is-plain-object.ts';
15
+ export { normalizeProtectedPaths } from './protected-paths.ts';
16
+ export { allFileChanges, type ChannelReader, type CovenantInput, type CovenantVerdict, type DispatchOutcome, type FileChange, parseInput, type SourceReader, verdictToExitCode, } from './protocol.ts';
17
+ export { aggregateGain, appendRecord, appendRecordFailOpen, formatRecordLine, type GainSummary, parseRecordLine, readRecords, runGain, SKIP_REASONS, type SkipReason, type TelemetryEvent, type TelemetryRecord, } from './telemetry.ts';
18
+ export { type CanonicalTranscript, noopTranscript, type TranscriptToolCall, type TranscriptUserMessage, transcriptFromInput, } from './transcript.ts';
package/dist/index.js CHANGED
@@ -6,62 +6,13 @@
6
6
  * (appendRecord / readRecords / appendRecordFailOpen).
7
7
  * See https://github.com/huskyhoochu/polydeukes
8
8
  */
9
- import { EXIT_BREAK_BLOCKING, EXIT_BREAK_NON_BLOCKING, EXIT_UPHOLD } from './exit-codes.js';
10
- import { isPlainObject } from './is-plain-object.js';
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';
11
11
  export { ConfigValidationError, DEFAULT_TELEMETRY_LOG_PATH, defineConfig, } from './config.js';
12
12
  export { EXIT_BREAK_BLOCKING, EXIT_BREAK_NON_BLOCKING, EXIT_UPHOLD, } from './exit-codes.js';
13
13
  export { failModeToExitCode, resolveFailMode, } from './fail-policy.js';
14
14
  export { isPlainObject } from './is-plain-object.js';
15
15
  export { normalizeProtectedPaths } from './protected-paths.js';
16
- export { aggregateGain, appendRecord, appendRecordFailOpen, formatRecordLine, parseRecordLine, readRecords, runGain, } from './telemetry.js';
16
+ export { allFileChanges, parseInput, verdictToExitCode, } from './protocol.js';
17
+ export { aggregateGain, appendRecord, appendRecordFailOpen, formatRecordLine, parseRecordLine, readRecords, runGain, SKIP_REASONS, } from './telemetry.js';
17
18
  export { noopTranscript, transcriptFromInput, } from './transcript.js';
18
- /**
19
- * Deserialize stdin-JSON into a {@link CovenantInput} (the protocol's reverse direction).
20
- *
21
- * fail-closed: this never throws. Any failure — unparseable JSON, an empty
22
- * payload, a parsed value that is not an object, or a missing required collection —
23
- * resolves to a blocking `{ ok: false, exitCode: 2 }`. "Cannot judge" means block,
24
- * so an unjudgeable input can never be mistaken for a valid one.
25
- */
26
- export function parseInput(stdinJson) {
27
- let parsed;
28
- try {
29
- parsed = JSON.parse(stdinJson);
30
- }
31
- catch {
32
- return { ok: false, exitCode: EXIT_BREAK_BLOCKING };
33
- }
34
- if (!isPlainObject(parsed)) {
35
- return { ok: false, exitCode: EXIT_BREAK_BLOCKING };
36
- }
37
- const candidate = parsed;
38
- if (!Array.isArray(candidate.toolCalls) ||
39
- !Array.isArray(candidate.subagentSpawns) ||
40
- !Array.isArray(candidate.userMessages)) {
41
- return { ok: false, exitCode: EXIT_BREAK_BLOCKING };
42
- }
43
- return { ok: true, value: candidate };
44
- }
45
- /**
46
- * Flatten every call's evidence into one array in call order.
47
- *
48
- * The one traversal for consumers that need no attribution (discipline scope, delta
49
- * judging): calls without evidence are skipped, never substituted for.
50
- */
51
- export function allFileChanges(input) {
52
- const changes = [];
53
- for (const call of input.toolCalls) {
54
- if (call.fileChange !== undefined)
55
- changes.push(call.fileChange);
56
- }
57
- return changes;
58
- }
59
- /**
60
- * Map a {@link CovenantVerdict} to an exit code (the protocol's forward direction).
61
- *
62
- * Responsibility boundary: the body emits `0` when upheld and `1` when
63
- * broken — never the blocking `2`. Translating `1` into `2` is the wrapper's policy.
64
- */
65
- export function verdictToExitCode(verdict) {
66
- return verdict.upheld ? EXIT_UPHOLD : EXIT_BREAK_NON_BLOCKING;
67
- }
@@ -2,5 +2,8 @@
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>;
@@ -2,6 +2,9 @@
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);
@@ -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;
@@ -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
+ ];
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * ROI telemetry — the single shared collector and its `gain` aggregation.
3
3
  *
4
- * One record is one line of 4-field TSV; one append is one write call. I/O is confined to
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
5
6
  * exactly two functions — {@link appendRecord} (the only write) and {@link readRecords}
6
7
  * (the only read). Formatting, parsing, and aggregation are pure.
7
8
  */
@@ -18,17 +19,34 @@
18
19
  * failure found after the fact — and it never blocks or passes a call.
19
20
  */
20
21
  export type TelemetryEvent = 'passed' | 'blocked' | 'witnessed' | 'advised' | 'skipped' | 'unattributed';
22
+ /**
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];
21
30
  /**
22
31
  * `TelemetryRecord` — one measured covenant outcome.
23
32
  *
24
33
  * `subject` is the judged target (a file path, etc.); `-` is the documented sentinel
25
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.
26
42
  */
27
43
  export type TelemetryRecord = {
28
44
  timestamp: string;
29
45
  event: TelemetryEvent;
30
46
  label: string;
31
47
  subject: string;
48
+ witnesses?: string;
49
+ reason?: SkipReason;
32
50
  };
33
51
  /** Per-label event counts, keyed by label then event. */
34
52
  export type GainSummary = {
@@ -39,16 +57,25 @@ export type GainSummary = {
39
57
  * Serialize a {@link TelemetryRecord} into one newline-terminated TSV line (pure).
40
58
  *
41
59
  * The returned string already includes the trailing `\n`, so {@link appendRecord}
42
- * 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.
43
67
  */
44
68
  export declare function formatRecordLine(record: TelemetryRecord): string;
45
69
  /**
46
70
  * Parse one TSV line back into a {@link TelemetryRecord}, or `null` if malformed (pure).
47
71
  *
48
72
  * Tolerates a trailing newline (so it round-trips {@link formatRecordLine}). Returns
49
- * `null` for the wrong field count, an event outside the six valid events, or an
50
- * empty line — a malformed line is rejected, never coerced into a bogus record. The one
51
- * exception is {@link LEGACY_WITNESSED_EVENT}, which reads back as `witnessed`.
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`.
52
79
  */
53
80
  export declare function parseRecordLine(line: string): TelemetryRecord | null;
54
81
  /**