@polydeukes/core 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +2 -1
- package/README.md +2 -1
- package/dist/algebra.d.ts +174 -0
- package/dist/algebra.js +438 -0
- package/dist/catalogue.d.ts +69 -0
- package/dist/catalogue.js +191 -0
- package/dist/config.d.ts +67 -67
- package/dist/config.js +85 -174
- package/dist/exit-codes.d.ts +10 -10
- package/dist/exit-codes.js +10 -10
- package/dist/fail-policy.d.ts +7 -7
- package/dist/fail-policy.js +7 -7
- package/dist/index.d.ts +13 -99
- package/dist/index.js +7 -57
- package/dist/is-plain-object.d.ts +4 -1
- package/dist/is-plain-object.js +4 -1
- package/dist/protected-paths.d.ts +3 -3
- package/dist/protected-paths.js +3 -3
- package/dist/protocol.d.ts +152 -0
- package/dist/protocol.js +121 -0
- package/dist/source-names.d.ts +10 -0
- package/dist/source-names.js +18 -0
- package/dist/telemetry.d.ts +46 -21
- package/dist/telemetry.js +79 -30
- package/dist/transcript.d.ts +18 -28
- package/dist/transcript.js +12 -20
- package/dist/validation.d.ts +24 -0
- package/dist/validation.js +44 -0
- package/package.json +3 -2
- package/schema/algebra-declaration.schema.json +402 -0
- package/schema/polydeukes.schema.json +54 -81
package/dist/telemetry.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ROI telemetry — the single shared collector and its `gain` aggregation
|
|
2
|
+
* ROI telemetry — the single shared collector and its `gain` aggregation.
|
|
3
3
|
*
|
|
4
|
-
* One record is one line of
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* than building its own logger.
|
|
4
|
+
* One record is one line of TSV — four fields, or five when a judgment names witnesses;
|
|
5
|
+
* one append is one write call. I/O is confined to
|
|
6
|
+
* exactly two functions — {@link appendRecord} (the only write) and {@link readRecords}
|
|
7
|
+
* (the only read). Formatting, parsing, and aggregation are pure.
|
|
9
8
|
*/
|
|
10
9
|
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
11
10
|
import { dirname } from 'node:path';
|
|
11
|
+
/**
|
|
12
|
+
* Why a `skipped` row records no judgment, closed: the surface has no observation channel
|
|
13
|
+
* for what the entry reads, assembly could not compile the entry, or the declaration's own
|
|
14
|
+
* `supply: pass` let an absent source through.
|
|
15
|
+
*/
|
|
16
|
+
export const SKIP_REASONS = ['no-observation', 'config-fault', 'supply-pass'];
|
|
12
17
|
const TAB = '\t';
|
|
13
18
|
const VALID_EVENTS = [
|
|
14
19
|
'passed',
|
|
@@ -22,15 +27,15 @@ const VALID_EVENTS = [
|
|
|
22
27
|
* The event name `witnessed` was written under before the rename — a read-only migration
|
|
23
28
|
* seam, never a value this module emits.
|
|
24
29
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
+
* A log written before the rename still carries the old name, and rejecting those rows as
|
|
31
|
+
* corrupt would discard the measurement rather than migrate it. Compatibility runs one way —
|
|
32
|
+
* {@link formatRecordLine} has no path back to this name — and the match is the exact
|
|
33
|
+
* literal, so a genuinely corrupt field is still rejected rather than coerced into a
|
|
34
|
+
* fabricated record.
|
|
30
35
|
*/
|
|
31
36
|
const LEGACY_WITNESSED_EVENT = 'bypassed';
|
|
32
37
|
/**
|
|
33
|
-
* Replace tab/newline/carriage-return with single spaces
|
|
38
|
+
* Replace tab/newline/carriage-return with single spaces.
|
|
34
39
|
*
|
|
35
40
|
* Without this, a tab or newline inside a field would fabricate extra TSV fields or
|
|
36
41
|
* extra lines — a record is always exactly one line.
|
|
@@ -38,23 +43,55 @@ const LEGACY_WITNESSED_EVENT = 'bypassed';
|
|
|
38
43
|
function sanitize(value) {
|
|
39
44
|
return value.replace(/[\t\n\r]/g, ' ');
|
|
40
45
|
}
|
|
46
|
+
/** True when `text` parses as a JSON array — the only shape the fifth field takes. */
|
|
47
|
+
function isJsonArray(text) {
|
|
48
|
+
try {
|
|
49
|
+
return Array.isArray(JSON.parse(text));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
41
55
|
/**
|
|
42
56
|
* Serialize a {@link TelemetryRecord} into one newline-terminated TSV line (pure).
|
|
43
57
|
*
|
|
44
58
|
* The returned string already includes the trailing `\n`, so {@link appendRecord}
|
|
45
|
-
* writes it verbatim in a single call.
|
|
59
|
+
* writes it verbatim in a single call. The fifth field appears only when the record
|
|
60
|
+
* carries `witnesses` or a skip `reason`; a record without either writes the four-field
|
|
61
|
+
* line unchanged.
|
|
62
|
+
*
|
|
63
|
+
* Throws when a record claims the fifth field twice, or claims it as a reason on an event
|
|
64
|
+
* that is not `skipped`: both are assembly errors, and writing either one would produce a
|
|
65
|
+
* row no reader can take apart.
|
|
46
66
|
*/
|
|
47
67
|
export function formatRecordLine(record) {
|
|
68
|
+
if (record.reason !== undefined) {
|
|
69
|
+
if (record.event !== 'skipped') {
|
|
70
|
+
throw new Error(`a skip reason belongs to a skipped row, not to '${record.event}'`);
|
|
71
|
+
}
|
|
72
|
+
if (record.witnesses !== undefined) {
|
|
73
|
+
throw new Error('a record carries either witnesses or a skip reason, never both');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
48
76
|
const fields = [record.timestamp, record.event, sanitize(record.label), sanitize(record.subject)];
|
|
77
|
+
if (record.witnesses !== undefined) {
|
|
78
|
+
fields.push(sanitize(record.witnesses));
|
|
79
|
+
}
|
|
80
|
+
else if (record.reason !== undefined) {
|
|
81
|
+
fields.push(record.reason);
|
|
82
|
+
}
|
|
49
83
|
return `${fields.join(TAB)}\n`;
|
|
50
84
|
}
|
|
51
85
|
/**
|
|
52
86
|
* Parse one TSV line back into a {@link TelemetryRecord}, or `null` if malformed (pure).
|
|
53
87
|
*
|
|
54
88
|
* Tolerates a trailing newline (so it round-trips {@link formatRecordLine}). Returns
|
|
55
|
-
* `null` for
|
|
56
|
-
* empty line — a malformed line is rejected, never
|
|
57
|
-
*
|
|
89
|
+
* `null` for a field count outside four (no fifth field) and five (with one), an event
|
|
90
|
+
* outside the six valid events, or an empty line — a malformed line is rejected, never
|
|
91
|
+
* coerced into a bogus record. The fifth field is read per event: on `skipped` it is a
|
|
92
|
+
* token of {@link SKIP_REASONS} and nothing else, on every other event a JSON array of
|
|
93
|
+
* witnesses. The one exception is {@link LEGACY_WITNESSED_EVENT}, which reads back as
|
|
94
|
+
* `witnessed`.
|
|
58
95
|
*/
|
|
59
96
|
export function parseRecordLine(line) {
|
|
60
97
|
const trimmed = line.replace(/\n$/, '');
|
|
@@ -62,24 +99,36 @@ export function parseRecordLine(line) {
|
|
|
62
99
|
return null;
|
|
63
100
|
}
|
|
64
101
|
const fields = trimmed.split(TAB);
|
|
65
|
-
if (fields.length !== 4) {
|
|
102
|
+
if (fields.length !== 4 && fields.length !== 5) {
|
|
66
103
|
return null;
|
|
67
104
|
}
|
|
68
|
-
const [timestamp, event, label, subject] = fields;
|
|
105
|
+
const [timestamp, event, label, subject, fifth] = fields;
|
|
69
106
|
const resolved = event === LEGACY_WITNESSED_EVENT ? 'witnessed' : event;
|
|
70
107
|
if (!VALID_EVENTS.includes(resolved)) {
|
|
71
108
|
return null;
|
|
72
109
|
}
|
|
73
|
-
|
|
110
|
+
const record = { timestamp, event: resolved, label, subject };
|
|
111
|
+
if (fifth === undefined) {
|
|
112
|
+
return record;
|
|
113
|
+
}
|
|
114
|
+
if (resolved === 'skipped') {
|
|
115
|
+
// A skip carries no witness, so a JSON array here is as corrupt as a near-miss token.
|
|
116
|
+
return SKIP_REASONS.includes(fifth)
|
|
117
|
+
? { ...record, reason: fifth }
|
|
118
|
+
: null;
|
|
119
|
+
}
|
|
120
|
+
// The fifth field is a JSON array or the line is corrupt: a stray tab inside a subject
|
|
121
|
+
// would otherwise read as a record with the wrong subject and no witnesses.
|
|
122
|
+
return isJsonArray(fifth) ? { ...record, witnesses: fifth } : null;
|
|
74
123
|
}
|
|
75
124
|
/**
|
|
76
|
-
* Append one record to the log at `path` — the only write I/O
|
|
125
|
+
* Append one record to the log at `path` — the only write I/O.
|
|
77
126
|
*
|
|
78
127
|
* Exactly one {@link appendFileSync} call per record, writing {@link formatRecordLine}
|
|
79
128
|
* verbatim. Relying on POSIX `O_APPEND` single-write semantics, concurrent appends do
|
|
80
129
|
* not interleave lines.
|
|
81
130
|
*
|
|
82
|
-
* fail-open
|
|
131
|
+
* fail-open: any fs failure — bad path, permissions, disk — returns
|
|
83
132
|
* `{ ok: false }` and never throws. This is deliberately the opposite direction of the
|
|
84
133
|
* covenant path's fail-closed: the worst outcome of telemetry is a missing datum, never
|
|
85
134
|
* a blocked workflow.
|
|
@@ -94,12 +143,12 @@ export function appendRecord(path, record) {
|
|
|
94
143
|
}
|
|
95
144
|
}
|
|
96
145
|
/**
|
|
97
|
-
* Append one telemetry record fail-open, timestamping it here
|
|
146
|
+
* Append one telemetry record fail-open, timestamping it here.
|
|
98
147
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
148
|
+
* {@link appendRecord} is deliberately mkdir-free — for it an absent directory is just a
|
|
149
|
+
* fail-open `{ ok: false }` — so this wrapper carries the parent-directory guarantee. The
|
|
150
|
+
* mkdir and the append share one try block, and a failure of either never alters the
|
|
151
|
+
* caller's verdict and never propagates.
|
|
103
152
|
*/
|
|
104
153
|
export function appendRecordFailOpen(telemetryPath, record) {
|
|
105
154
|
try {
|
|
@@ -111,7 +160,7 @@ export function appendRecordFailOpen(telemetryPath, record) {
|
|
|
111
160
|
}
|
|
112
161
|
}
|
|
113
162
|
/**
|
|
114
|
-
* Read every record from the log at `path` — the only read I/O
|
|
163
|
+
* Read every record from the log at `path` — the only read I/O.
|
|
115
164
|
*
|
|
116
165
|
* fail-open: an absent file or any read error returns `{ records: [], skipped: 0 }`
|
|
117
166
|
* (an absent log means "nothing collected yet"), never throwing. Corrupt lines
|
|
@@ -143,7 +192,7 @@ export function readRecords(path) {
|
|
|
143
192
|
return { records, skipped };
|
|
144
193
|
}
|
|
145
194
|
/**
|
|
146
|
-
* Aggregate records into per-label event counts (
|
|
195
|
+
* Aggregate records into per-label event counts (pure).
|
|
147
196
|
*
|
|
148
197
|
* Each label gets its own counter across all six events, so a corrupt or missing
|
|
149
198
|
* event never bleeds counts between labels.
|
|
@@ -169,7 +218,7 @@ export function aggregateGain(records) {
|
|
|
169
218
|
* Render a {@link GainSummary} into human-readable lines (pure).
|
|
170
219
|
*
|
|
171
220
|
* Each label is mentioned with its passed/blocked/witnessed/advised/skipped/unattributed
|
|
172
|
-
* counts; each is a distinct column, never folded into another
|
|
221
|
+
* counts; each is a distinct column, never folded into another. A non-zero
|
|
173
222
|
* corrupt-line count is reported rather than hidden — silent skipping would mask log
|
|
174
223
|
* corruption.
|
|
175
224
|
*
|
|
@@ -191,7 +240,7 @@ function renderGain(summary, skipped) {
|
|
|
191
240
|
return lines.join('\n');
|
|
192
241
|
}
|
|
193
242
|
/**
|
|
194
|
-
* `gain` entry point — read the log at `path`, aggregate, and render
|
|
243
|
+
* `gain` entry point — read the log at `path`, aggregate, and render.
|
|
195
244
|
*
|
|
196
245
|
* Composes {@link readRecords} + {@link aggregateGain} + a pure renderer. An absent or
|
|
197
246
|
* empty log yields `no telemetry collected`; a corrupt line is skipped upstream, reported
|
package/dist/transcript.d.ts
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
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
|
-
import type { CovenantInput } from './
|
|
11
|
-
/** One subagent invocation observed in the session. `kind` is an adapter-supplied value. */
|
|
12
|
-
export type SubagentInvocation = {
|
|
13
|
-
kind: string;
|
|
14
|
-
};
|
|
9
|
+
import type { CovenantInput } from './protocol.ts';
|
|
15
10
|
/**
|
|
16
|
-
* One user message observed in the session
|
|
11
|
+
* One user message observed in the session.
|
|
17
12
|
*
|
|
18
13
|
* `timestampMs` is epoch milliseconds. Its absence means the source cannot prove
|
|
19
14
|
* freshness — the fail-closed signal a witness consumer must treat as "not fresh".
|
|
@@ -23,10 +18,10 @@ export type TranscriptUserMessage = {
|
|
|
23
18
|
timestampMs?: number;
|
|
24
19
|
};
|
|
25
20
|
/**
|
|
26
|
-
* One tool call observed in the session
|
|
27
|
-
*
|
|
21
|
+
* One tool call observed in the session. `name` and `args` are adapter-supplied values —
|
|
22
|
+
* the core knows the query vocabulary, never a tool's name.
|
|
28
23
|
*
|
|
29
|
-
* `succeeded` is three-valued
|
|
24
|
+
* `succeeded` is three-valued: `true` = it ran and reported success,
|
|
30
25
|
* `false` = it ran and reported an error, was blocked, or was refused, and absent = the
|
|
31
26
|
* provider cannot observe results at all. A consumer that treats the call as evidence
|
|
32
27
|
* accepts only `true`, so the latter two share a disposition while staying diagnosable.
|
|
@@ -37,40 +32,35 @@ export type TranscriptToolCall = {
|
|
|
37
32
|
succeeded?: boolean;
|
|
38
33
|
};
|
|
39
34
|
/**
|
|
40
|
-
* `CanonicalTranscript` — what a covenant may ask about the session
|
|
35
|
+
* `CanonicalTranscript` — what a covenant may ask about the session.
|
|
41
36
|
*
|
|
42
37
|
* Synchronous by design (covenant bodies are short-lived CLI processes) and
|
|
43
38
|
* verdict-free: the seam carries facts only; TTL filtering and token matching belong
|
|
44
39
|
* to the consumer.
|
|
45
40
|
*/
|
|
46
41
|
export type CanonicalTranscript = {
|
|
47
|
-
/** Invocations of the given kind, or all of them when omitted. Observation order preserved. */
|
|
48
|
-
findSubagentInvocations(kind?: string): SubagentInvocation[];
|
|
49
42
|
/** Every user message, observation order preserved. Missing timestampMs = freshness unprovable. */
|
|
50
43
|
findUserMessages(): TranscriptUserMessage[];
|
|
51
44
|
/** Tool calls with the given name, or all when omitted. Observation order preserved. */
|
|
52
45
|
findToolCalls(name?: string): TranscriptToolCall[];
|
|
53
46
|
};
|
|
54
47
|
/**
|
|
55
|
-
* The injection-absent default
|
|
48
|
+
* The injection-absent default: every query answers "nothing happened".
|
|
56
49
|
* A witness consumer naturally converges to fail-closed — no evidence, no skip — and
|
|
57
50
|
* so does a precedent consumer (no evidence, gate stays shut).
|
|
58
51
|
*/
|
|
59
52
|
export declare const noopTranscript: CanonicalTranscript;
|
|
60
53
|
/**
|
|
61
|
-
* Wrap a {@link CovenantInput} as a {@link CanonicalTranscript}
|
|
54
|
+
* Wrap a {@link CovenantInput} as a {@link CanonicalTranscript}.
|
|
62
55
|
*
|
|
63
|
-
* Exposes `
|
|
64
|
-
* `userMessages` with `timestampMs` omitted — the bare IR cannot prove freshness,
|
|
56
|
+
* Exposes `userMessages` with `timestampMs` omitted — the bare IR cannot prove freshness,
|
|
65
57
|
* and that absence is the *correct* fail-closed signal for a witness consumer.
|
|
66
58
|
* Order preserved; the input is never mutated, and every query returns fresh
|
|
67
59
|
* objects so consumers never hold live aliases into the shared IR.
|
|
68
60
|
*
|
|
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).
|
|
61
|
+
* `findToolCalls` projects each call down to `{ name, args }` only: a call element also
|
|
62
|
+
* carries `fileChange` evidence, and evidence is judgment input, not session history — the
|
|
63
|
+
* two vocabularies stay separate. `succeeded` stays absent because these calls are the ones
|
|
64
|
+
* being judged right now: they have not run, and a call can never be its own precedent.
|
|
75
65
|
*/
|
|
76
66
|
export declare function transcriptFromInput(input: CovenantInput): CanonicalTranscript;
|
package/dist/transcript.js
CHANGED
|
@@ -1,43 +1,35 @@
|
|
|
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
|
*/
|
|
15
14
|
export const noopTranscript = {
|
|
16
|
-
findSubagentInvocations: () => [],
|
|
17
15
|
findUserMessages: () => [],
|
|
18
16
|
findToolCalls: () => [],
|
|
19
17
|
};
|
|
20
18
|
/**
|
|
21
|
-
* Wrap a {@link CovenantInput} as a {@link CanonicalTranscript}
|
|
19
|
+
* Wrap a {@link CovenantInput} as a {@link CanonicalTranscript}.
|
|
22
20
|
*
|
|
23
|
-
* Exposes `
|
|
24
|
-
* `userMessages` with `timestampMs` omitted — the bare IR cannot prove freshness,
|
|
21
|
+
* Exposes `userMessages` with `timestampMs` omitted — the bare IR cannot prove freshness,
|
|
25
22
|
* and that absence is the *correct* fail-closed signal for a witness consumer.
|
|
26
23
|
* Order preserved; the input is never mutated, and every query returns fresh
|
|
27
24
|
* objects so consumers never hold live aliases into the shared IR.
|
|
28
25
|
*
|
|
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).
|
|
26
|
+
* `findToolCalls` projects each call down to `{ name, args }` only: a call element also
|
|
27
|
+
* carries `fileChange` evidence, and evidence is judgment input, not session history — the
|
|
28
|
+
* two vocabularies stay separate. `succeeded` stays absent because these calls are the ones
|
|
29
|
+
* being judged right now: they have not run, and a call can never be its own precedent.
|
|
35
30
|
*/
|
|
36
31
|
export function transcriptFromInput(input) {
|
|
37
32
|
return {
|
|
38
|
-
findSubagentInvocations: (kind) => input.subagentSpawns
|
|
39
|
-
.filter((spawn) => kind === undefined || spawn.kind === kind)
|
|
40
|
-
.map((spawn) => ({ kind: spawn.kind })),
|
|
41
33
|
findUserMessages: () => input.userMessages.map((message) => ({ text: message.text })),
|
|
42
34
|
findToolCalls: (name) => input.toolCalls
|
|
43
35
|
.filter((call) => name === undefined || call.name === name)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `validation.ts` — the shape-checking primitives the core's validators share.
|
|
3
|
+
*
|
|
4
|
+
* Internal to the core: `config.ts` and `algebra.ts` both build on these, and neither
|
|
5
|
+
* owns them. Not a public export — `isPlainObject` has its own file because it *is* one.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* `ConfigValidationError` — raised when a config fails structural validation.
|
|
9
|
+
*
|
|
10
|
+
* The message names the offending field path so the developer sees exactly what is wrong.
|
|
11
|
+
* This throw is a developer-time error (config authoring), a different axis from the
|
|
12
|
+
* covenant runtime's fail-closed exit code — a bad config should fail loud and early.
|
|
13
|
+
*/
|
|
14
|
+
export declare class ConfigValidationError extends Error {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
17
|
+
/** Throw on the first key outside the allowed vocabulary, naming the key and its location. */
|
|
18
|
+
export declare function rejectUnknownKeys(record: Record<string, unknown>, allowed: ReadonlySet<string>, location: string): void;
|
|
19
|
+
/** True when the value is a string with at least one character. */
|
|
20
|
+
export declare function isNonEmptyString(value: unknown): value is string;
|
|
21
|
+
/** True when the value is an array whose every element is a string. */
|
|
22
|
+
export declare function isStringArray(value: unknown): value is string[];
|
|
23
|
+
/** Throw unless the pattern string compiles with `new RegExp` — compilability only, never run. */
|
|
24
|
+
export declare function rejectUncompilableRegex(pattern: string, location: string): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `validation.ts` — the shape-checking primitives the core's validators share.
|
|
3
|
+
*
|
|
4
|
+
* Internal to the core: `config.ts` and `algebra.ts` both build on these, and neither
|
|
5
|
+
* owns them. Not a public export — `isPlainObject` has its own file because it *is* one.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* `ConfigValidationError` — raised when a config fails structural validation.
|
|
9
|
+
*
|
|
10
|
+
* The message names the offending field path so the developer sees exactly what is wrong.
|
|
11
|
+
* This throw is a developer-time error (config authoring), a different axis from the
|
|
12
|
+
* covenant runtime's fail-closed exit code — a bad config should fail loud and early.
|
|
13
|
+
*/
|
|
14
|
+
export class ConfigValidationError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'ConfigValidationError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Throw on the first key outside the allowed vocabulary, naming the key and its location. */
|
|
21
|
+
export function rejectUnknownKeys(record, allowed, location) {
|
|
22
|
+
for (const key of Object.keys(record)) {
|
|
23
|
+
if (!allowed.has(key)) {
|
|
24
|
+
throw new ConfigValidationError(`unknown key '${key}' in ${location}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** True when the value is a string with at least one character. */
|
|
29
|
+
export function isNonEmptyString(value) {
|
|
30
|
+
return typeof value === 'string' && value.length > 0;
|
|
31
|
+
}
|
|
32
|
+
/** True when the value is an array whose every element is a string. */
|
|
33
|
+
export function isStringArray(value) {
|
|
34
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
|
|
35
|
+
}
|
|
36
|
+
/** Throw unless the pattern string compiles with `new RegExp` — compilability only, never run. */
|
|
37
|
+
export function rejectUncompilableRegex(pattern, location) {
|
|
38
|
+
try {
|
|
39
|
+
new RegExp(pattern);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new ConfigValidationError(`${location} must be a compilable regular expression`);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polydeukes/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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": {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"types": "./dist/index.d.ts",
|
|
18
18
|
"import": "./dist/index.js"
|
|
19
19
|
},
|
|
20
|
-
"./schema.json": "./schema/polydeukes.schema.json"
|
|
20
|
+
"./schema.json": "./schema/polydeukes.schema.json",
|
|
21
|
+
"./algebra-declaration.schema.json": "./schema/algebra-declaration.schema.json"
|
|
21
22
|
},
|
|
22
23
|
"files": [
|
|
23
24
|
"dist",
|