@polydeukes/core 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +64 -36
- package/dist/config.js +238 -149
- package/dist/exit-codes.d.ts +20 -0
- package/dist/exit-codes.js +20 -0
- package/dist/fail-policy.d.ts +7 -7
- package/dist/fail-policy.js +8 -8
- package/dist/index.d.ts +13 -28
- package/dist/index.js +8 -21
- package/dist/is-plain-object.d.ts +1 -1
- package/dist/is-plain-object.js +1 -1
- package/dist/protected-paths.d.ts +3 -3
- package/dist/protected-paths.js +3 -3
- package/dist/telemetry.d.ts +24 -21
- package/dist/telemetry.js +36 -29
- package/dist/transcript.d.ts +16 -19
- package/dist/transcript.js +11 -14
- package/package.json +1 -1
- package/schema/polydeukes.schema.json +38 -9
package/dist/index.d.ts
CHANGED
|
@@ -1,34 +1,20 @@
|
|
|
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
|
-
export { ConfigValidationError, DEFAULT_TELEMETRY_LOG_PATH, type DisciplineEntry, type DisciplineForbid, defineConfig, type LanguageProfile, type PolydeukesConfig, type ResolvedConfig, type ResolvedLanguageProfile, } from './config.js';
|
|
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
11
|
export { type FailMode, type FailureKind, failModeToExitCode, resolveFailMode, } from './fail-policy.js';
|
|
12
12
|
export { isPlainObject } from './is-plain-object.js';
|
|
13
13
|
export { normalizeProtectedPaths } from './protected-paths.js';
|
|
14
14
|
export { aggregateGain, appendRecord, appendRecordFailOpen, formatRecordLine, type GainSummary, parseRecordLine, readRecords, runGain, type TelemetryEvent, type TelemetryRecord, } from './telemetry.js';
|
|
15
15
|
export { type CanonicalTranscript, noopTranscript, type SubagentInvocation, type TranscriptToolCall, type TranscriptUserMessage, transcriptFromInput, } from './transcript.js';
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* The three codes are distinct and ordered by severity. The covenant *body* only
|
|
20
|
-
* ever emits `0` (uphold) or `1` (break, non-blocking); translating a break into the
|
|
21
|
-
* blocking `2` is the wrapper's job (COVENANT-01), never the core's. The sole place
|
|
22
|
-
* the core itself reaches for `2` is the fail-closed parse path below.
|
|
23
|
-
*/
|
|
24
|
-
/** Promise upheld — no violation, the edit/push passes. */
|
|
25
|
-
export declare const EXIT_UPHOLD = 0;
|
|
26
|
-
/** Violation reported as a non-blocking signal. The covenant body's break code. */
|
|
27
|
-
export declare const EXIT_BREAK_NON_BLOCKING = 1;
|
|
28
|
-
/** Violation blocked — the edit/push is refused. Reserved for the wrapper / fail-closed. */
|
|
29
|
-
export declare const EXIT_BREAK_BLOCKING = 2;
|
|
30
|
-
/**
|
|
31
|
-
* `FileChange` — one file's mutation evidence around the judged call (CORE-06 §4.1).
|
|
17
|
+
* `FileChange` — one file's mutation evidence around the judged call.
|
|
32
18
|
*
|
|
33
19
|
* Agent-neutral, discriminated by `kind`: a deletion is first-class evidence rather
|
|
34
20
|
* than an unrepresentable case, and impossible states (a deletion with resulting
|
|
@@ -52,14 +38,13 @@ export type FileChange = {
|
|
|
52
38
|
pre?: string;
|
|
53
39
|
};
|
|
54
40
|
/**
|
|
55
|
-
* `CovenantInput` — the agent-neutral input IR a covenant judges
|
|
41
|
+
* `CovenantInput` — the agent-neutral input IR a covenant judges.
|
|
56
42
|
*
|
|
57
43
|
* Adapters up-translate their own agent payloads into this shape and pipe it as
|
|
58
44
|
* stdin-JSON. The vocabulary carries no agent/tool literals; concrete tool or
|
|
59
45
|
* subagent names are *values* an adapter fills in, never part of the core's type.
|
|
60
|
-
* Evidence has exactly one home
|
|
61
|
-
*
|
|
62
|
-
* can stand in for it.
|
|
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.
|
|
63
48
|
*/
|
|
64
49
|
export type CovenantInput = {
|
|
65
50
|
toolCalls: {
|
|
@@ -75,7 +60,7 @@ export type CovenantInput = {
|
|
|
75
60
|
}[];
|
|
76
61
|
};
|
|
77
62
|
/**
|
|
78
|
-
* `CovenantVerdict` — the result a covenant body produces
|
|
63
|
+
* `CovenantVerdict` — the result a covenant body produces.
|
|
79
64
|
*
|
|
80
65
|
* Either the promise was upheld, or it was broken with a human-readable reason.
|
|
81
66
|
* Maps to an exit code via {@link verdictToExitCode}.
|
|
@@ -89,7 +74,7 @@ export type CovenantVerdict = {
|
|
|
89
74
|
/**
|
|
90
75
|
* Deserialize stdin-JSON into a {@link CovenantInput} (the protocol's reverse direction).
|
|
91
76
|
*
|
|
92
|
-
* fail-closed
|
|
77
|
+
* fail-closed: this never throws. Any failure — unparseable JSON, an empty
|
|
93
78
|
* payload, a parsed value that is not an object, or a missing required collection —
|
|
94
79
|
* resolves to a blocking `{ ok: false, exitCode: 2 }`. "Cannot judge" means block,
|
|
95
80
|
* so an unjudgeable input can never be mistaken for a valid one.
|
|
@@ -102,7 +87,7 @@ export declare function parseInput(stdinJson: string): {
|
|
|
102
87
|
exitCode: 2;
|
|
103
88
|
};
|
|
104
89
|
/**
|
|
105
|
-
* Flatten every call's evidence into one array in call order
|
|
90
|
+
* Flatten every call's evidence into one array in call order.
|
|
106
91
|
*
|
|
107
92
|
* The one traversal for consumers that need no attribution (discipline scope, delta
|
|
108
93
|
* judging): calls without evidence are skipped, never substituted for.
|
|
@@ -111,7 +96,7 @@ export declare function allFileChanges(input: CovenantInput): FileChange[];
|
|
|
111
96
|
/**
|
|
112
97
|
* Map a {@link CovenantVerdict} to an exit code (the protocol's forward direction).
|
|
113
98
|
*
|
|
114
|
-
* Responsibility boundary
|
|
99
|
+
* Responsibility boundary: the body emits `0` when upheld and `1` when
|
|
115
100
|
* broken — never the blocking `2`. Translating `1` into `2` is the wrapper's policy.
|
|
116
101
|
*/
|
|
117
102
|
export declare function verdictToExitCode(verdict: CovenantVerdict): 0 | 1;
|
package/dist/index.js
CHANGED
|
@@ -1,37 +1,24 @@
|
|
|
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
|
*/
|
|
9
|
+
import { EXIT_BREAK_BLOCKING, EXIT_BREAK_NON_BLOCKING, EXIT_UPHOLD } from './exit-codes.js';
|
|
10
10
|
import { isPlainObject } from './is-plain-object.js';
|
|
11
11
|
export { ConfigValidationError, DEFAULT_TELEMETRY_LOG_PATH, defineConfig, } from './config.js';
|
|
12
|
+
export { EXIT_BREAK_BLOCKING, EXIT_BREAK_NON_BLOCKING, EXIT_UPHOLD, } from './exit-codes.js';
|
|
12
13
|
export { failModeToExitCode, resolveFailMode, } from './fail-policy.js';
|
|
13
14
|
export { isPlainObject } from './is-plain-object.js';
|
|
14
15
|
export { normalizeProtectedPaths } from './protected-paths.js';
|
|
15
16
|
export { aggregateGain, appendRecord, appendRecordFailOpen, formatRecordLine, parseRecordLine, readRecords, runGain, } from './telemetry.js';
|
|
16
17
|
export { noopTranscript, transcriptFromInput, } from './transcript.js';
|
|
17
|
-
/**
|
|
18
|
-
* exit-code semantics of the covenant protocol (PRD §4.1).
|
|
19
|
-
*
|
|
20
|
-
* The three codes are distinct and ordered by severity. The covenant *body* only
|
|
21
|
-
* ever emits `0` (uphold) or `1` (break, non-blocking); translating a break into the
|
|
22
|
-
* blocking `2` is the wrapper's job (COVENANT-01), never the core's. The sole place
|
|
23
|
-
* the core itself reaches for `2` is the fail-closed parse path below.
|
|
24
|
-
*/
|
|
25
|
-
/** Promise upheld — no violation, the edit/push passes. */
|
|
26
|
-
export const EXIT_UPHOLD = 0;
|
|
27
|
-
/** Violation reported as a non-blocking signal. The covenant body's break code. */
|
|
28
|
-
export const EXIT_BREAK_NON_BLOCKING = 1;
|
|
29
|
-
/** Violation blocked — the edit/push is refused. Reserved for the wrapper / fail-closed. */
|
|
30
|
-
export const EXIT_BREAK_BLOCKING = 2;
|
|
31
18
|
/**
|
|
32
19
|
* Deserialize stdin-JSON into a {@link CovenantInput} (the protocol's reverse direction).
|
|
33
20
|
*
|
|
34
|
-
* fail-closed
|
|
21
|
+
* fail-closed: this never throws. Any failure — unparseable JSON, an empty
|
|
35
22
|
* payload, a parsed value that is not an object, or a missing required collection —
|
|
36
23
|
* resolves to a blocking `{ ok: false, exitCode: 2 }`. "Cannot judge" means block,
|
|
37
24
|
* so an unjudgeable input can never be mistaken for a valid one.
|
|
@@ -56,7 +43,7 @@ export function parseInput(stdinJson) {
|
|
|
56
43
|
return { ok: true, value: candidate };
|
|
57
44
|
}
|
|
58
45
|
/**
|
|
59
|
-
* Flatten every call's evidence into one array in call order
|
|
46
|
+
* Flatten every call's evidence into one array in call order.
|
|
60
47
|
*
|
|
61
48
|
* The one traversal for consumers that need no attribution (discipline scope, delta
|
|
62
49
|
* judging): calls without evidence are skipped, never substituted for.
|
|
@@ -72,7 +59,7 @@ export function allFileChanges(input) {
|
|
|
72
59
|
/**
|
|
73
60
|
* Map a {@link CovenantVerdict} to an exit code (the protocol's forward direction).
|
|
74
61
|
*
|
|
75
|
-
* Responsibility boundary
|
|
62
|
+
* Responsibility boundary: the body emits `0` when upheld and `1` when
|
|
76
63
|
* broken — never the blocking `2`. Translating `1` into `2` is the wrapper's policy.
|
|
77
64
|
*/
|
|
78
65
|
export function verdictToExitCode(verdict) {
|
|
@@ -1,6 +1,6 @@
|
|
|
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
5
|
*/
|
|
6
6
|
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
package/dist/is-plain-object.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
5
|
*/
|
|
6
6
|
export function isPlainObject(value) {
|
|
7
7
|
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.
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
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 4-field TSV
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* aggregation are pure. This is the sole collector: later work calls this API rather
|
|
8
|
-
* than building its own logger.
|
|
4
|
+
* One record is one line of 4-field TSV; one append is one write call. I/O is confined to
|
|
5
|
+
* exactly two functions — {@link appendRecord} (the only write) and {@link readRecords}
|
|
6
|
+
* (the only read). Formatting, parsing, and aggregation are pure.
|
|
9
7
|
*/
|
|
10
8
|
/**
|
|
11
|
-
* The
|
|
9
|
+
* The six telemetry events. `witnessed` is a first-class event, not a flag on `passed`:
|
|
12
10
|
* a break a human stood behind by supplying the pass condition themselves. `advised` is a
|
|
13
11
|
* violation verdict an advise-level observer recorded but let through; `skipped` is a
|
|
14
12
|
* discipline a surface could not judge at all (no evidence channel) — a no-op that shows
|
|
15
13
|
* up in the data instead of vanishing.
|
|
14
|
+
*
|
|
15
|
+
* `unattributed` is the one observation event among them: a protected entry whose state
|
|
16
|
+
* changed with no judgment row explaining it. It sits on a different axis from the five
|
|
17
|
+
* verdicts — `skipped` is an inability known up front, `unattributed` an attribution
|
|
18
|
+
* failure found after the fact — and it never blocks or passes a call.
|
|
16
19
|
*/
|
|
17
|
-
export type TelemetryEvent = 'passed' | 'blocked' | 'witnessed' | 'advised' | 'skipped';
|
|
20
|
+
export type TelemetryEvent = 'passed' | 'blocked' | 'witnessed' | 'advised' | 'skipped' | 'unattributed';
|
|
18
21
|
/**
|
|
19
|
-
* `TelemetryRecord` — one measured covenant outcome
|
|
22
|
+
* `TelemetryRecord` — one measured covenant outcome.
|
|
20
23
|
*
|
|
21
24
|
* `subject` is the judged target (a file path, etc.); `-` is the documented sentinel
|
|
22
25
|
* for "no subject", carried round-trip like any other value.
|
|
@@ -43,19 +46,19 @@ export declare function formatRecordLine(record: TelemetryRecord): string;
|
|
|
43
46
|
* Parse one TSV line back into a {@link TelemetryRecord}, or `null` if malformed (pure).
|
|
44
47
|
*
|
|
45
48
|
* Tolerates a trailing newline (so it round-trips {@link formatRecordLine}). Returns
|
|
46
|
-
* `null` for the wrong field count, an event outside the
|
|
49
|
+
* `null` for the wrong field count, an event outside the six valid events, or an
|
|
47
50
|
* empty line — a malformed line is rejected, never coerced into a bogus record. The one
|
|
48
51
|
* exception is {@link LEGACY_WITNESSED_EVENT}, which reads back as `witnessed`.
|
|
49
52
|
*/
|
|
50
53
|
export declare function parseRecordLine(line: string): TelemetryRecord | null;
|
|
51
54
|
/**
|
|
52
|
-
* Append one record to the log at `path` — the only write I/O
|
|
55
|
+
* Append one record to the log at `path` — the only write I/O.
|
|
53
56
|
*
|
|
54
57
|
* Exactly one {@link appendFileSync} call per record, writing {@link formatRecordLine}
|
|
55
58
|
* verbatim. Relying on POSIX `O_APPEND` single-write semantics, concurrent appends do
|
|
56
59
|
* not interleave lines.
|
|
57
60
|
*
|
|
58
|
-
* fail-open
|
|
61
|
+
* fail-open: any fs failure — bad path, permissions, disk — returns
|
|
59
62
|
* `{ ok: false }` and never throws. This is deliberately the opposite direction of the
|
|
60
63
|
* covenant path's fail-closed: the worst outcome of telemetry is a missing datum, never
|
|
61
64
|
* a blocked workflow.
|
|
@@ -64,16 +67,16 @@ export declare function appendRecord(path: string, record: TelemetryRecord): {
|
|
|
64
67
|
ok: boolean;
|
|
65
68
|
};
|
|
66
69
|
/**
|
|
67
|
-
* Append one telemetry record fail-open, timestamping it here
|
|
70
|
+
* Append one telemetry record fail-open, timestamping it here.
|
|
68
71
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
72
|
+
* {@link appendRecord} is deliberately mkdir-free — for it an absent directory is just a
|
|
73
|
+
* fail-open `{ ok: false }` — so this wrapper carries the parent-directory guarantee. The
|
|
74
|
+
* mkdir and the append share one try block, and a failure of either never alters the
|
|
75
|
+
* caller's verdict and never propagates.
|
|
73
76
|
*/
|
|
74
77
|
export declare function appendRecordFailOpen(telemetryPath: string, record: Omit<TelemetryRecord, 'timestamp'>): void;
|
|
75
78
|
/**
|
|
76
|
-
* Read every record from the log at `path` — the only read I/O
|
|
79
|
+
* Read every record from the log at `path` — the only read I/O.
|
|
77
80
|
*
|
|
78
81
|
* fail-open: an absent file or any read error returns `{ records: [], skipped: 0 }`
|
|
79
82
|
* (an absent log means "nothing collected yet"), never throwing. Corrupt lines
|
|
@@ -85,14 +88,14 @@ export declare function readRecords(path: string): {
|
|
|
85
88
|
skipped: number;
|
|
86
89
|
};
|
|
87
90
|
/**
|
|
88
|
-
* Aggregate records into per-label event counts (
|
|
91
|
+
* Aggregate records into per-label event counts (pure).
|
|
89
92
|
*
|
|
90
|
-
* Each label gets its own counter across all
|
|
93
|
+
* Each label gets its own counter across all six events, so a corrupt or missing
|
|
91
94
|
* event never bleeds counts between labels.
|
|
92
95
|
*/
|
|
93
96
|
export declare function aggregateGain(records: TelemetryRecord[]): GainSummary;
|
|
94
97
|
/**
|
|
95
|
-
* `gain` entry point — read the log at `path`, aggregate, and render
|
|
98
|
+
* `gain` entry point — read the log at `path`, aggregate, and render.
|
|
96
99
|
*
|
|
97
100
|
* Composes {@link readRecords} + {@link aggregateGain} + a pure renderer. An absent or
|
|
98
101
|
* empty log yields `no telemetry collected`; a corrupt line is skipped upstream, reported
|
package/dist/telemetry.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
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 4-field TSV
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* aggregation are pure. This is the sole collector: later work calls this API rather
|
|
8
|
-
* than building its own logger.
|
|
4
|
+
* One record is one line of 4-field TSV; one append is one write call. I/O is confined to
|
|
5
|
+
* exactly two functions — {@link appendRecord} (the only write) and {@link readRecords}
|
|
6
|
+
* (the only read). Formatting, parsing, and aggregation are pure.
|
|
9
7
|
*/
|
|
10
8
|
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
11
9
|
import { dirname } from 'node:path';
|
|
@@ -16,20 +14,21 @@ const VALID_EVENTS = [
|
|
|
16
14
|
'witnessed',
|
|
17
15
|
'advised',
|
|
18
16
|
'skipped',
|
|
17
|
+
'unattributed',
|
|
19
18
|
];
|
|
20
19
|
/**
|
|
21
20
|
* The event name `witnessed` was written under before the rename — a read-only migration
|
|
22
21
|
* seam, never a value this module emits.
|
|
23
22
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
23
|
+
* A log written before the rename still carries the old name, and rejecting those rows as
|
|
24
|
+
* corrupt would discard the measurement rather than migrate it. Compatibility runs one way —
|
|
25
|
+
* {@link formatRecordLine} has no path back to this name — and the match is the exact
|
|
26
|
+
* literal, so a genuinely corrupt field is still rejected rather than coerced into a
|
|
27
|
+
* fabricated record.
|
|
29
28
|
*/
|
|
30
29
|
const LEGACY_WITNESSED_EVENT = 'bypassed';
|
|
31
30
|
/**
|
|
32
|
-
* Replace tab/newline/carriage-return with single spaces
|
|
31
|
+
* Replace tab/newline/carriage-return with single spaces.
|
|
33
32
|
*
|
|
34
33
|
* Without this, a tab or newline inside a field would fabricate extra TSV fields or
|
|
35
34
|
* extra lines — a record is always exactly one line.
|
|
@@ -51,7 +50,7 @@ export function formatRecordLine(record) {
|
|
|
51
50
|
* Parse one TSV line back into a {@link TelemetryRecord}, or `null` if malformed (pure).
|
|
52
51
|
*
|
|
53
52
|
* Tolerates a trailing newline (so it round-trips {@link formatRecordLine}). Returns
|
|
54
|
-
* `null` for the wrong field count, an event outside the
|
|
53
|
+
* `null` for the wrong field count, an event outside the six valid events, or an
|
|
55
54
|
* empty line — a malformed line is rejected, never coerced into a bogus record. The one
|
|
56
55
|
* exception is {@link LEGACY_WITNESSED_EVENT}, which reads back as `witnessed`.
|
|
57
56
|
*/
|
|
@@ -72,13 +71,13 @@ export function parseRecordLine(line) {
|
|
|
72
71
|
return { timestamp, event: resolved, label, subject };
|
|
73
72
|
}
|
|
74
73
|
/**
|
|
75
|
-
* Append one record to the log at `path` — the only write I/O
|
|
74
|
+
* Append one record to the log at `path` — the only write I/O.
|
|
76
75
|
*
|
|
77
76
|
* Exactly one {@link appendFileSync} call per record, writing {@link formatRecordLine}
|
|
78
77
|
* verbatim. Relying on POSIX `O_APPEND` single-write semantics, concurrent appends do
|
|
79
78
|
* not interleave lines.
|
|
80
79
|
*
|
|
81
|
-
* fail-open
|
|
80
|
+
* fail-open: any fs failure — bad path, permissions, disk — returns
|
|
82
81
|
* `{ ok: false }` and never throws. This is deliberately the opposite direction of the
|
|
83
82
|
* covenant path's fail-closed: the worst outcome of telemetry is a missing datum, never
|
|
84
83
|
* a blocked workflow.
|
|
@@ -93,12 +92,12 @@ export function appendRecord(path, record) {
|
|
|
93
92
|
}
|
|
94
93
|
}
|
|
95
94
|
/**
|
|
96
|
-
* Append one telemetry record fail-open, timestamping it here
|
|
95
|
+
* Append one telemetry record fail-open, timestamping it here.
|
|
97
96
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
97
|
+
* {@link appendRecord} is deliberately mkdir-free — for it an absent directory is just a
|
|
98
|
+
* fail-open `{ ok: false }` — so this wrapper carries the parent-directory guarantee. The
|
|
99
|
+
* mkdir and the append share one try block, and a failure of either never alters the
|
|
100
|
+
* caller's verdict and never propagates.
|
|
102
101
|
*/
|
|
103
102
|
export function appendRecordFailOpen(telemetryPath, record) {
|
|
104
103
|
try {
|
|
@@ -110,7 +109,7 @@ export function appendRecordFailOpen(telemetryPath, record) {
|
|
|
110
109
|
}
|
|
111
110
|
}
|
|
112
111
|
/**
|
|
113
|
-
* Read every record from the log at `path` — the only read I/O
|
|
112
|
+
* Read every record from the log at `path` — the only read I/O.
|
|
114
113
|
*
|
|
115
114
|
* fail-open: an absent file or any read error returns `{ records: [], skipped: 0 }`
|
|
116
115
|
* (an absent log means "nothing collected yet"), never throwing. Corrupt lines
|
|
@@ -142,16 +141,23 @@ export function readRecords(path) {
|
|
|
142
141
|
return { records, skipped };
|
|
143
142
|
}
|
|
144
143
|
/**
|
|
145
|
-
* Aggregate records into per-label event counts (
|
|
144
|
+
* Aggregate records into per-label event counts (pure).
|
|
146
145
|
*
|
|
147
|
-
* Each label gets its own counter across all
|
|
146
|
+
* Each label gets its own counter across all six events, so a corrupt or missing
|
|
148
147
|
* event never bleeds counts between labels.
|
|
149
148
|
*/
|
|
150
149
|
export function aggregateGain(records) {
|
|
151
150
|
const counts = {};
|
|
152
151
|
for (const record of records) {
|
|
153
152
|
if (!(record.label in counts)) {
|
|
154
|
-
counts[record.label] = {
|
|
153
|
+
counts[record.label] = {
|
|
154
|
+
passed: 0,
|
|
155
|
+
blocked: 0,
|
|
156
|
+
witnessed: 0,
|
|
157
|
+
advised: 0,
|
|
158
|
+
skipped: 0,
|
|
159
|
+
unattributed: 0,
|
|
160
|
+
};
|
|
155
161
|
}
|
|
156
162
|
counts[record.label][record.event] += 1;
|
|
157
163
|
}
|
|
@@ -160,9 +166,10 @@ export function aggregateGain(records) {
|
|
|
160
166
|
/**
|
|
161
167
|
* Render a {@link GainSummary} into human-readable lines (pure).
|
|
162
168
|
*
|
|
163
|
-
* Each label is mentioned with its passed/blocked/witnessed/advised/skipped
|
|
164
|
-
* is a distinct column, never folded into another
|
|
165
|
-
* count is reported rather than hidden — silent skipping would mask log
|
|
169
|
+
* Each label is mentioned with its passed/blocked/witnessed/advised/skipped/unattributed
|
|
170
|
+
* counts; each is a distinct column, never folded into another. A non-zero
|
|
171
|
+
* corrupt-line count is reported rather than hidden — silent skipping would mask log
|
|
172
|
+
* corruption.
|
|
166
173
|
*
|
|
167
174
|
* Two different meanings share the word `skipped`: the per-label EVENT column above,
|
|
168
175
|
* and the unparseable-line count below. They are rendered on separate lines and never
|
|
@@ -174,7 +181,7 @@ function renderGain(summary, skipped) {
|
|
|
174
181
|
}
|
|
175
182
|
const lines = [`total ${summary.total}`];
|
|
176
183
|
for (const [label, counts] of Object.entries(summary.counts)) {
|
|
177
|
-
lines.push(`${label}: passed=${counts.passed} blocked=${counts.blocked} witnessed=${counts.witnessed} advised=${counts.advised} skipped=${counts.skipped}`);
|
|
184
|
+
lines.push(`${label}: passed=${counts.passed} blocked=${counts.blocked} witnessed=${counts.witnessed} advised=${counts.advised} skipped=${counts.skipped} unattributed=${counts.unattributed}`);
|
|
178
185
|
}
|
|
179
186
|
if (skipped > 0) {
|
|
180
187
|
lines.push(`corrupt lines skipped=${skipped}`);
|
|
@@ -182,7 +189,7 @@ function renderGain(summary, skipped) {
|
|
|
182
189
|
return lines.join('\n');
|
|
183
190
|
}
|
|
184
191
|
/**
|
|
185
|
-
* `gain` entry point — read the log at `path`, aggregate, and render
|
|
192
|
+
* `gain` entry point — read the log at `path`, aggregate, and render.
|
|
186
193
|
*
|
|
187
194
|
* Composes {@link readRecords} + {@link aggregateGain} + a pure renderer. An absent or
|
|
188
195
|
* empty log yields `no telemetry collected`; a corrupt line is skipped upstream, reported
|
package/dist/transcript.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `CanonicalTranscript` — the agent-neutral session-query seam
|
|
2
|
+
* `CanonicalTranscript` — the agent-neutral session-query seam.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* query vocabulary. Pure types and functions, zero I/O.
|
|
4
|
+
* This seam does not replace `CovenantInput`. The IR is the *data* a covenant judges;
|
|
5
|
+
* `CanonicalTranscript` is the *behavioral seam* that queries session data, and the IR is
|
|
6
|
+
* one source it can wrap. Concrete transcript formats stay in adapters; the core knows only
|
|
7
|
+
* the query vocabulary. Pure types and functions, zero I/O.
|
|
9
8
|
*/
|
|
10
9
|
import type { CovenantInput } from './index.js';
|
|
11
10
|
/** One subagent invocation observed in the session. `kind` is an adapter-supplied value. */
|
|
@@ -13,7 +12,7 @@ export type SubagentInvocation = {
|
|
|
13
12
|
kind: string;
|
|
14
13
|
};
|
|
15
14
|
/**
|
|
16
|
-
* One user message observed in the session
|
|
15
|
+
* One user message observed in the session.
|
|
17
16
|
*
|
|
18
17
|
* `timestampMs` is epoch milliseconds. Its absence means the source cannot prove
|
|
19
18
|
* freshness — the fail-closed signal a witness consumer must treat as "not fresh".
|
|
@@ -23,10 +22,10 @@ export type TranscriptUserMessage = {
|
|
|
23
22
|
timestampMs?: number;
|
|
24
23
|
};
|
|
25
24
|
/**
|
|
26
|
-
* One tool call observed in the session
|
|
27
|
-
*
|
|
25
|
+
* One tool call observed in the session. `name` and `args` are adapter-supplied values —
|
|
26
|
+
* the core knows the query vocabulary, never a tool's name.
|
|
28
27
|
*
|
|
29
|
-
* `succeeded` is three-valued
|
|
28
|
+
* `succeeded` is three-valued: `true` = it ran and reported success,
|
|
30
29
|
* `false` = it ran and reported an error, was blocked, or was refused, and absent = the
|
|
31
30
|
* provider cannot observe results at all. A consumer that treats the call as evidence
|
|
32
31
|
* accepts only `true`, so the latter two share a disposition while staying diagnosable.
|
|
@@ -37,7 +36,7 @@ export type TranscriptToolCall = {
|
|
|
37
36
|
succeeded?: boolean;
|
|
38
37
|
};
|
|
39
38
|
/**
|
|
40
|
-
* `CanonicalTranscript` — what a covenant may ask about the session
|
|
39
|
+
* `CanonicalTranscript` — what a covenant may ask about the session.
|
|
41
40
|
*
|
|
42
41
|
* Synchronous by design (covenant bodies are short-lived CLI processes) and
|
|
43
42
|
* verdict-free: the seam carries facts only; TTL filtering and token matching belong
|
|
@@ -52,13 +51,13 @@ export type CanonicalTranscript = {
|
|
|
52
51
|
findToolCalls(name?: string): TranscriptToolCall[];
|
|
53
52
|
};
|
|
54
53
|
/**
|
|
55
|
-
* The injection-absent default
|
|
54
|
+
* The injection-absent default: every query answers "nothing happened".
|
|
56
55
|
* A witness consumer naturally converges to fail-closed — no evidence, no skip — and
|
|
57
56
|
* so does a precedent consumer (no evidence, gate stays shut).
|
|
58
57
|
*/
|
|
59
58
|
export declare const noopTranscript: CanonicalTranscript;
|
|
60
59
|
/**
|
|
61
|
-
* Wrap a {@link CovenantInput} as a {@link CanonicalTranscript}
|
|
60
|
+
* Wrap a {@link CovenantInput} as a {@link CanonicalTranscript}.
|
|
62
61
|
*
|
|
63
62
|
* Exposes `subagentSpawns` as invocations (filtered when a kind is given) and
|
|
64
63
|
* `userMessages` with `timestampMs` omitted — the bare IR cannot prove freshness,
|
|
@@ -66,11 +65,9 @@ export declare const noopTranscript: CanonicalTranscript;
|
|
|
66
65
|
* Order preserved; the input is never mutated, and every query returns fresh
|
|
67
66
|
* objects so consumers never hold live aliases into the shared IR.
|
|
68
67
|
*
|
|
69
|
-
* `findToolCalls` projects each call down to `{ name, args }` only:
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* being judged right now, so they have not run, and a call can never be its own
|
|
74
|
-
* precedent (COVENANT-13b §4.1).
|
|
68
|
+
* `findToolCalls` projects each call down to `{ name, args }` only: a call element also
|
|
69
|
+
* carries `fileChange` evidence, and evidence is judgment input, not session history — the
|
|
70
|
+
* two vocabularies stay separate. `succeeded` stays absent because these calls are the ones
|
|
71
|
+
* being judged right now: they have not run, and a call can never be its own precedent.
|
|
75
72
|
*/
|
|
76
73
|
export declare function transcriptFromInput(input: CovenantInput): CanonicalTranscript;
|
package/dist/transcript.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `CanonicalTranscript` — the agent-neutral session-query seam
|
|
2
|
+
* `CanonicalTranscript` — the agent-neutral session-query seam.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* query vocabulary. Pure types and functions, zero I/O.
|
|
4
|
+
* This seam does not replace `CovenantInput`. The IR is the *data* a covenant judges;
|
|
5
|
+
* `CanonicalTranscript` is the *behavioral seam* that queries session data, and the IR is
|
|
6
|
+
* one source it can wrap. Concrete transcript formats stay in adapters; the core knows only
|
|
7
|
+
* the query vocabulary. Pure types and functions, zero I/O.
|
|
9
8
|
*/
|
|
10
9
|
/**
|
|
11
|
-
* The injection-absent default
|
|
10
|
+
* The injection-absent default: every query answers "nothing happened".
|
|
12
11
|
* A witness consumer naturally converges to fail-closed — no evidence, no skip — and
|
|
13
12
|
* so does a precedent consumer (no evidence, gate stays shut).
|
|
14
13
|
*/
|
|
@@ -18,7 +17,7 @@ export const noopTranscript = {
|
|
|
18
17
|
findToolCalls: () => [],
|
|
19
18
|
};
|
|
20
19
|
/**
|
|
21
|
-
* Wrap a {@link CovenantInput} as a {@link CanonicalTranscript}
|
|
20
|
+
* Wrap a {@link CovenantInput} as a {@link CanonicalTranscript}.
|
|
22
21
|
*
|
|
23
22
|
* Exposes `subagentSpawns` as invocations (filtered when a kind is given) and
|
|
24
23
|
* `userMessages` with `timestampMs` omitted — the bare IR cannot prove freshness,
|
|
@@ -26,12 +25,10 @@ export const noopTranscript = {
|
|
|
26
25
|
* Order preserved; the input is never mutated, and every query returns fresh
|
|
27
26
|
* objects so consumers never hold live aliases into the shared IR.
|
|
28
27
|
*
|
|
29
|
-
* `findToolCalls` projects each call down to `{ name, args }` only:
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* being judged right now, so they have not run, and a call can never be its own
|
|
34
|
-
* precedent (COVENANT-13b §4.1).
|
|
28
|
+
* `findToolCalls` projects each call down to `{ name, args }` only: a call element also
|
|
29
|
+
* carries `fileChange` evidence, and evidence is judgment input, not session history — the
|
|
30
|
+
* two vocabularies stay separate. `succeeded` stays absent because these calls are the ones
|
|
31
|
+
* being judged right now: they have not run, and a call can never be its own precedent.
|
|
35
32
|
*/
|
|
36
33
|
export function transcriptFromInput(input) {
|
|
37
34
|
return {
|
package/package.json
CHANGED