@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/index.d.ts CHANGED
@@ -1,34 +1,20 @@
1
1
  /**
2
2
  * @polydeukes/core — the thin, domain- and agent-agnostic core.
3
3
  *
4
- * Pre-alpha. The covenant protocol (CORE-01) landed first, then the ROI telemetry
5
- * collector (CORE-02) and the config loader (CONFIG-01). Pure types and functions,
6
- * except telemetry's confined I/O functions (appendRecord / readRecords /
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
- * exit-code semantics of the covenant protocol (PRD §4.1).
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 (PRD §4.2).
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 the call element it belongs to (CORE-06 §4.1):
61
- * `fileChange` absent means "this call is unproven", and no sibling call's evidence
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 (PRD §4.3).
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 (PRD §5.2): this never throws. Any failure — unparseable JSON, an empty
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 (CORE-06 §4.1).
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 (PRD §4.1): the body emits `0` when upheld and `1` when
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
- * Pre-alpha. The covenant protocol (CORE-01) landed first, then the ROI telemetry
5
- * collector (CORE-02) and the config loader (CONFIG-01). Pure types and functions,
6
- * except telemetry's confined I/O functions (appendRecord / readRecords /
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 (PRD §5.2): this never throws. Any failure — unparseable JSON, an empty
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 (CORE-06 §4.1).
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 (PRD §4.1): the body emits `0` when upheld and `1` when
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
- * Promoted by CORE-05 from per-package copies: typeof `object`, non-null, not an array.
4
+ * typeof `object`, non-null, not an array.
5
5
  */
6
6
  export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `isPlainObject` — the workspace's single canonical plain-object predicate.
3
3
  *
4
- * Promoted by CORE-05 from per-package copies: typeof `object`, non-null, not an array.
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 (CONFIG-02). Pure string transformation —
4
- * zero file I/O, no glob expansion, no path resolution (PRD §4.2).
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 (PRD §4.2).
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.
@@ -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 (CONFIG-02). Pure string transformation —
4
- * zero file I/O, no glob expansion, no path resolution (PRD §4.2).
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 (PRD §4.2).
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.
@@ -1,22 +1,25 @@
1
1
  /**
2
- * ROI telemetry — the single shared collector and its `gain` aggregation (CORE-02).
2
+ * ROI telemetry — the single shared collector and its `gain` aggregation.
3
3
  *
4
- * One record is one line of 4-field TSV (PRD §4.1); one append is one write call
5
- * (PRD §4.2). I/O is confined to exactly two functions — {@link appendRecord} (the
6
- * only write) and {@link readRecords} (the only read). Formatting, parsing, and
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 five telemetry events. `witnessed` is a first-class event, not a flag on `passed`:
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 (PRD §4.1).
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 five valid events, or an
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 (PRD §4.2).
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 (PRD §4.3): any fs failure — bad path, permissions, disk — returns
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 (CORE-05).
70
+ * Append one telemetry record fail-open, timestamping it here.
68
71
  *
69
- * This layer lives above the deliberately mkdir-free {@link appendRecord} (COVENANT-01b:
70
- * an absent directory is a fail-open `{ ok: false }` for `appendRecord` itself), so this
71
- * wrapper carries the parent-directory guarantee. The mkdir and the append share one try
72
- * block, and a failure of either never alters the caller's verdict and never propagates.
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 (PRD §4.4).
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 (PRD §4.4, pure).
91
+ * Aggregate records into per-label event counts (pure).
89
92
  *
90
- * Each label gets its own counter across all five events, so a corrupt or missing
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 (PRD §4.4).
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 (CORE-02).
2
+ * ROI telemetry — the single shared collector and its `gain` aggregation.
3
3
  *
4
- * One record is one line of 4-field TSV (PRD §4.1); one append is one write call
5
- * (PRD §4.2). I/O is confined to exactly two functions — {@link appendRecord} (the
6
- * only write) and {@link readRecords} (the only read). Formatting, parsing, and
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
- * It exists because the collected log predates the rename: those rows are the sample the
25
- * milestone journal round argues from, and dropping them as corrupt would delete the
26
- * measurement instead of migrating it. Compatibility runs one way only {@link
27
- * formatRecordLine} has no path back to this name and the match is the exact literal, so
28
- * a genuinely corrupt field is still rejected rather than coerced into a fabricated record.
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 nameand 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 (PRD §4.1 line integrity).
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 five valid events, or an
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 (PRD §4.2).
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 (PRD §4.3): any fs failure — bad path, permissions, disk — returns
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 (CORE-05).
95
+ * Append one telemetry record fail-open, timestamping it here.
97
96
  *
98
- * This layer lives above the deliberately mkdir-free {@link appendRecord} (COVENANT-01b:
99
- * an absent directory is a fail-open `{ ok: false }` for `appendRecord` itself), so this
100
- * wrapper carries the parent-directory guarantee. The mkdir and the append share one try
101
- * block, and a failure of either never alters the caller's verdict and never propagates.
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 (PRD §4.4).
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 (PRD §4.4, pure).
144
+ * Aggregate records into per-label event counts (pure).
146
145
  *
147
- * Each label gets its own counter across all five events, so a corrupt or missing
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] = { passed: 0, blocked: 0, witnessed: 0, advised: 0, skipped: 0 };
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 counts; each
164
- * is a distinct column, never folded into another (PRD §4.4). A non-zero corrupt-line
165
- * count is reported rather than hidden — silent skipping would mask log corruption.
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 (PRD §4.4).
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
@@ -1,11 +1,10 @@
1
1
  /**
2
- * `CanonicalTranscript` — the agent-neutral session-query seam (CORE-04).
2
+ * `CanonicalTranscript` — the agent-neutral session-query seam.
3
3
  *
4
- * Layering (PRD §1): this seam does not replace `CovenantInput`. The IR (CORE-01) is
5
- * the *data* a covenant judges; `CanonicalTranscript` is the *behavioral seam* that
6
- * queries session data CORE-04 sits on top of CORE-01, and the IR is one source it
7
- * can wrap. Concrete transcript formats stay in adapters; the core knows only the
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 (PRD §4.1).
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 (COVENANT-13 §4.2). `name` and `args` are
27
- * adapter-supplied values — the core knows the query vocabulary, never a tool's name.
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 (COVENANT-13b §4.1): `true` = it ran and reported success,
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 (PRD §4.1).
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 (PRD §4.2): every query answers "nothing happened".
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} (PRD §4.2).
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: since CORE-06 a
70
- * call element also carries `fileChange` evidence, and evidence is judgment input, not
71
- * session history — the two vocabularies stay separate (COVENANT-13 §4.2). `succeeded`
72
- * stays absent for the same reason it is left three-valued: these calls are the ones
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;
@@ -1,14 +1,13 @@
1
1
  /**
2
- * `CanonicalTranscript` — the agent-neutral session-query seam (CORE-04).
2
+ * `CanonicalTranscript` — the agent-neutral session-query seam.
3
3
  *
4
- * Layering (PRD §1): this seam does not replace `CovenantInput`. The IR (CORE-01) is
5
- * the *data* a covenant judges; `CanonicalTranscript` is the *behavioral seam* that
6
- * queries session data CORE-04 sits on top of CORE-01, and the IR is one source it
7
- * can wrap. Concrete transcript formats stay in adapters; the core knows only the
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 (PRD §4.2): every query answers "nothing happened".
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} (PRD §4.2).
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: since CORE-06 a
30
- * call element also carries `fileChange` evidence, and evidence is judgment input, not
31
- * session history — the two vocabularies stay separate (COVENANT-13 §4.2). `succeeded`
32
- * stays absent for the same reason it is left three-valued: these calls are the ones
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polydeukes/core",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Polydeukes core — covenant protocol, config loader, and transcript interface. Domain- and agent-agnostic. Alpha.",
5
5
  "license": "MIT",
6
6
  "repository": {