@polydeukes/core 0.3.0 → 0.4.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 +3 -2
- package/dist/config.js +102 -62
- package/dist/exit-codes.d.ts +20 -0
- package/dist/exit-codes.js +20 -0
- package/dist/fail-policy.js +1 -1
- package/dist/index.d.ts +1 -14
- package/dist/index.js +2 -14
- package/dist/telemetry.d.ts +9 -4
- package/dist/telemetry.js +16 -7
- package/package.json +1 -1
- package/schema/polydeukes.schema.json +8 -5
package/dist/config.d.ts
CHANGED
|
@@ -156,7 +156,8 @@ export declare class ConfigValidationError extends Error {
|
|
|
156
156
|
* Throws {@link ConfigValidationError} (naming the offending field path) when the top level
|
|
157
157
|
* is not a plain object, any object level carries an unknown key, `languages` is
|
|
158
158
|
* missing/empty, any language's `productionGlob` is missing/empty, any `testCmd` is not a
|
|
159
|
-
* non-empty string template, `telemetry.logPath` is not a string
|
|
160
|
-
* non-string element, or `adapters` is not a map of
|
|
159
|
+
* non-empty string template, `telemetry.logPath` is not a non-empty string after trimming,
|
|
160
|
+
* `protectedPaths` carries a non-string or empty element, or `adapters` is not a map of
|
|
161
|
+
* plain-object namespaces.
|
|
161
162
|
*/
|
|
162
163
|
export declare function defineConfig(config: unknown): ResolvedConfig;
|
package/dist/config.js
CHANGED
|
@@ -170,6 +170,11 @@ function validateDisciplines(disciplines) {
|
|
|
170
170
|
if (predicate === 'forbid') {
|
|
171
171
|
const forbid = entry.forbid;
|
|
172
172
|
if (typeof forbid === 'string') {
|
|
173
|
+
// An empty pattern matches at every position, so the entry would break every
|
|
174
|
+
// in-scope change — rejected like every sibling pattern field.
|
|
175
|
+
if (forbid.length === 0) {
|
|
176
|
+
throw new ConfigValidationError(`${location} forbid must be a non-empty string pattern`);
|
|
177
|
+
}
|
|
173
178
|
rejectUncompilableRegex(forbid, `${location} forbid`);
|
|
174
179
|
}
|
|
175
180
|
else if (isPlainObject(forbid)) {
|
|
@@ -178,6 +183,9 @@ function validateDisciplines(disciplines) {
|
|
|
178
183
|
if (keys.length !== 1 || keys[0] !== 'added' || typeof forbid.added !== 'string') {
|
|
179
184
|
throw new ConfigValidationError(`${location} forbid object must have exactly one key 'added' with a string pattern`);
|
|
180
185
|
}
|
|
186
|
+
if (forbid.added.length === 0) {
|
|
187
|
+
throw new ConfigValidationError(`${location} forbid.added must be a non-empty string pattern`);
|
|
188
|
+
}
|
|
181
189
|
rejectUncompilableRegex(forbid.added, `${location} forbid.added`);
|
|
182
190
|
}
|
|
183
191
|
else {
|
|
@@ -193,6 +201,11 @@ function validateDisciplines(disciplines) {
|
|
|
193
201
|
if (typeof entry.forbidCommand !== 'string') {
|
|
194
202
|
throw new ConfigValidationError(`${location} forbidCommand must be a string pattern`);
|
|
195
203
|
}
|
|
204
|
+
if (entry.forbidCommand.length === 0) {
|
|
205
|
+
// An empty pattern matches every command line — one typo would block every
|
|
206
|
+
// shell call the entry sees.
|
|
207
|
+
throw new ConfigValidationError(`${location} forbidCommand must be a non-empty string pattern`);
|
|
208
|
+
}
|
|
196
209
|
rejectUncompilableRegex(entry.forbidCommand, `${location} forbidCommand`);
|
|
197
210
|
}
|
|
198
211
|
else {
|
|
@@ -213,27 +226,9 @@ function validateDisciplines(disciplines) {
|
|
|
213
226
|
return disciplines;
|
|
214
227
|
}
|
|
215
228
|
/**
|
|
216
|
-
* Validate
|
|
217
|
-
* {@link ResolvedConfig} with defaults filled and templates compiled (PRD §4.3).
|
|
218
|
-
* Pure — no file I/O.
|
|
219
|
-
*
|
|
220
|
-
* Throws {@link ConfigValidationError} (naming the offending field path) when the top level
|
|
221
|
-
* is not a plain object, any object level carries an unknown key, `languages` is
|
|
222
|
-
* missing/empty, any language's `productionGlob` is missing/empty, any `testCmd` is not a
|
|
223
|
-
* non-empty string template, `telemetry.logPath` is not a string, `protectedPaths` carries a
|
|
224
|
-
* non-string element, or `adapters` is not a map of plain-object namespaces.
|
|
229
|
+
* Validate the `languages` map and compile each profile's `{scope}` template (PRD §4.1).
|
|
225
230
|
*/
|
|
226
|
-
|
|
227
|
-
if (!isPlainObject(config)) {
|
|
228
|
-
throw new ConfigValidationError('config must be a plain object');
|
|
229
|
-
}
|
|
230
|
-
rejectUnknownKeys(config, TOP_LEVEL_KEYS, 'config');
|
|
231
|
-
// `$schema` is an IDE schema reference (CONFIG-03): accepted, type-checked, and
|
|
232
|
-
// ignored — it never appears in the resolution output.
|
|
233
|
-
if (config.$schema !== undefined && typeof config.$schema !== 'string') {
|
|
234
|
-
throw new ConfigValidationError('$schema must be a string');
|
|
235
|
-
}
|
|
236
|
-
const languages = config.languages;
|
|
231
|
+
function validateLanguages(languages) {
|
|
237
232
|
if (!isPlainObject(languages) || Object.keys(languages).length === 0) {
|
|
238
233
|
throw new ConfigValidationError('languages must be a non-empty object');
|
|
239
234
|
}
|
|
@@ -258,56 +253,101 @@ export function defineConfig(config) {
|
|
|
258
253
|
testCmd: compileTestCmd(profile.testCmd),
|
|
259
254
|
};
|
|
260
255
|
}
|
|
261
|
-
|
|
256
|
+
return resolvedLanguages;
|
|
257
|
+
}
|
|
258
|
+
/** Validate `protectedPaths` — an array of non-empty strings; the data passes through verbatim. */
|
|
259
|
+
function validateProtectedPaths(protectedPaths) {
|
|
260
|
+
if (!isStringArray(protectedPaths)) {
|
|
262
261
|
throw new ConfigValidationError('protectedPaths must be an array of strings');
|
|
263
262
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (Array.isArray(config.adapters) || !isPlainObject(config.adapters)) {
|
|
269
|
-
throw new ConfigValidationError('adapters must be an object map of adapter namespaces — the directory-list form ' +
|
|
270
|
-
'was removed; move directories to protectedPaths');
|
|
271
|
-
}
|
|
272
|
-
for (const [name, namespace] of Object.entries(config.adapters)) {
|
|
273
|
-
if (!isPlainObject(namespace)) {
|
|
274
|
-
throw new ConfigValidationError(`adapters.${name} must be an object`);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
adapters = config.adapters;
|
|
263
|
+
// An empty element carries no path meaning and would ride along unnoticed next to
|
|
264
|
+
// valid siblings.
|
|
265
|
+
if (protectedPaths.some((path) => path.length === 0)) {
|
|
266
|
+
throw new ConfigValidationError('protectedPaths must not contain an empty string');
|
|
278
267
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
268
|
+
return protectedPaths;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Validate the `adapters` map (CONFIG-07 layering) — each namespace is a plain object whose
|
|
272
|
+
* contents belong to that adapter's own validator, so they pass through verbatim.
|
|
273
|
+
*/
|
|
274
|
+
function validateAdapters(adapters) {
|
|
275
|
+
// Array first: the removed directory-list form deserves a migration hint, not a
|
|
276
|
+
// generic type error — and an EMPTY array must land here too, never pass as a map.
|
|
277
|
+
if (Array.isArray(adapters) || !isPlainObject(adapters)) {
|
|
278
|
+
throw new ConfigValidationError('adapters must be an object map of adapter namespaces — the directory-list form ' +
|
|
279
|
+
'was removed; move directories to protectedPaths');
|
|
292
280
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
throw new ConfigValidationError('witness must be an object');
|
|
297
|
-
}
|
|
298
|
-
rejectUnknownKeys(config.witness, WITNESS_KEYS, 'witness');
|
|
299
|
-
const { token, ttlMinutes } = config.witness;
|
|
300
|
-
if (typeof token !== 'string' || token.trim().length === 0) {
|
|
301
|
-
throw new ConfigValidationError('witness.token must be a non-empty string after trimming');
|
|
281
|
+
for (const [name, namespace] of Object.entries(adapters)) {
|
|
282
|
+
if (!isPlainObject(namespace)) {
|
|
283
|
+
throw new ConfigValidationError(`adapters.${name} must be an object`);
|
|
302
284
|
}
|
|
303
|
-
if (typeof ttlMinutes !== 'number' || !(Number.isFinite(ttlMinutes) && ttlMinutes > 0)) {
|
|
304
|
-
throw new ConfigValidationError('witness.ttlMinutes must be a finite number greater than 0');
|
|
305
|
-
}
|
|
306
|
-
witness = { token, ttlMinutes };
|
|
307
285
|
}
|
|
286
|
+
return adapters;
|
|
287
|
+
}
|
|
288
|
+
/** Validate the `telemetry` section and return its `logPath` (absent stays undefined). */
|
|
289
|
+
function validateTelemetry(telemetry) {
|
|
290
|
+
if (!isPlainObject(telemetry)) {
|
|
291
|
+
throw new ConfigValidationError('telemetry must be an object');
|
|
292
|
+
}
|
|
293
|
+
rejectUnknownKeys(telemetry, TELEMETRY_KEYS, 'telemetry');
|
|
294
|
+
if (telemetry.logPath === undefined) {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
if (typeof telemetry.logPath !== 'string') {
|
|
298
|
+
throw new ConfigValidationError('telemetry.logPath must be a string');
|
|
299
|
+
}
|
|
300
|
+
if (telemetry.logPath.trim().length === 0) {
|
|
301
|
+
throw new ConfigValidationError('telemetry.logPath must be a non-empty string after trimming');
|
|
302
|
+
}
|
|
303
|
+
return telemetry.logPath;
|
|
304
|
+
}
|
|
305
|
+
/** Validate the `witness` section (CONFIG-05) — both values are consumed at assembly time. */
|
|
306
|
+
function validateWitness(witness) {
|
|
307
|
+
if (!isPlainObject(witness)) {
|
|
308
|
+
throw new ConfigValidationError('witness must be an object');
|
|
309
|
+
}
|
|
310
|
+
rejectUnknownKeys(witness, WITNESS_KEYS, 'witness');
|
|
311
|
+
const { token, ttlMinutes } = witness;
|
|
312
|
+
if (typeof token !== 'string' || token.trim().length === 0) {
|
|
313
|
+
throw new ConfigValidationError('witness.token must be a non-empty string after trimming');
|
|
314
|
+
}
|
|
315
|
+
if (typeof ttlMinutes !== 'number' || !(Number.isFinite(ttlMinutes) && ttlMinutes > 0)) {
|
|
316
|
+
throw new ConfigValidationError('witness.ttlMinutes must be a finite number greater than 0');
|
|
317
|
+
}
|
|
318
|
+
return { token, ttlMinutes };
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Validate parsed unknown data as a {@link PolydeukesConfig} and return a
|
|
322
|
+
* {@link ResolvedConfig} with defaults filled and templates compiled (PRD §4.3).
|
|
323
|
+
* Pure — no file I/O.
|
|
324
|
+
*
|
|
325
|
+
* Throws {@link ConfigValidationError} (naming the offending field path) when the top level
|
|
326
|
+
* is not a plain object, any object level carries an unknown key, `languages` is
|
|
327
|
+
* missing/empty, any language's `productionGlob` is missing/empty, any `testCmd` is not a
|
|
328
|
+
* non-empty string template, `telemetry.logPath` is not a non-empty string after trimming,
|
|
329
|
+
* `protectedPaths` carries a non-string or empty element, or `adapters` is not a map of
|
|
330
|
+
* plain-object namespaces.
|
|
331
|
+
*/
|
|
332
|
+
export function defineConfig(config) {
|
|
333
|
+
if (!isPlainObject(config)) {
|
|
334
|
+
throw new ConfigValidationError('config must be a plain object');
|
|
335
|
+
}
|
|
336
|
+
rejectUnknownKeys(config, TOP_LEVEL_KEYS, 'config');
|
|
337
|
+
// `$schema` is an IDE schema reference (CONFIG-03): accepted, type-checked, and
|
|
338
|
+
// ignored — it never appears in the resolution output.
|
|
339
|
+
if (config.$schema !== undefined && typeof config.$schema !== 'string') {
|
|
340
|
+
throw new ConfigValidationError('$schema must be a string');
|
|
341
|
+
}
|
|
342
|
+
const resolvedLanguages = validateLanguages(config.languages);
|
|
343
|
+
const protectedPaths = config.protectedPaths !== undefined ? validateProtectedPaths(config.protectedPaths) : undefined;
|
|
344
|
+
const adapters = config.adapters !== undefined ? validateAdapters(config.adapters) : undefined;
|
|
345
|
+
const disciplines = config.disciplines !== undefined ? validateDisciplines(config.disciplines) : undefined;
|
|
346
|
+
const logPath = config.telemetry !== undefined ? validateTelemetry(config.telemetry) : undefined;
|
|
347
|
+
const witness = config.witness !== undefined ? validateWitness(config.witness) : undefined;
|
|
308
348
|
return {
|
|
309
349
|
languages: resolvedLanguages,
|
|
310
|
-
...(
|
|
350
|
+
...(protectedPaths !== undefined && { protectedPaths }),
|
|
311
351
|
...(adapters !== undefined && { adapters }),
|
|
312
352
|
telemetry: {
|
|
313
353
|
logPath: logPath ?? DEFAULT_TELEMETRY_LOG_PATH,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* exit-codes — the covenant protocol's exit-code vocabulary (CORE-01 PRD §4.1).
|
|
3
|
+
*
|
|
4
|
+
* The three codes are distinct and ordered by severity. The covenant *body* only
|
|
5
|
+
* ever emits `0` (uphold) or `1` (break, non-blocking); translating a break into the
|
|
6
|
+
* blocking `2` is the wrapper's job (COVENANT-01), never the core's. The sole place
|
|
7
|
+
* the core itself reaches for `2` is the fail-closed parse path in the barrel.
|
|
8
|
+
*
|
|
9
|
+
* These live in their own module rather than the barrel because `fail-policy.ts` needs
|
|
10
|
+
* them: importing them from the barrel, which re-exports fail-policy, is an
|
|
11
|
+
* initialization cycle — the constants read as `undefined` depending on which module
|
|
12
|
+
* the runtime evaluates first (CLEANUP-01 F1). The barrel re-exports them, so every
|
|
13
|
+
* consumer outside core still reaches them at the same path.
|
|
14
|
+
*/
|
|
15
|
+
/** Promise upheld — no violation, the edit/push passes. */
|
|
16
|
+
export declare const EXIT_UPHOLD = 0;
|
|
17
|
+
/** Violation reported as a non-blocking signal. The covenant body's break code. */
|
|
18
|
+
export declare const EXIT_BREAK_NON_BLOCKING = 1;
|
|
19
|
+
/** Violation blocked — the edit/push is refused. Reserved for the wrapper / fail-closed. */
|
|
20
|
+
export declare const EXIT_BREAK_BLOCKING = 2;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* exit-codes — the covenant protocol's exit-code vocabulary (CORE-01 PRD §4.1).
|
|
3
|
+
*
|
|
4
|
+
* The three codes are distinct and ordered by severity. The covenant *body* only
|
|
5
|
+
* ever emits `0` (uphold) or `1` (break, non-blocking); translating a break into the
|
|
6
|
+
* blocking `2` is the wrapper's job (COVENANT-01), never the core's. The sole place
|
|
7
|
+
* the core itself reaches for `2` is the fail-closed parse path in the barrel.
|
|
8
|
+
*
|
|
9
|
+
* These live in their own module rather than the barrel because `fail-policy.ts` needs
|
|
10
|
+
* them: importing them from the barrel, which re-exports fail-policy, is an
|
|
11
|
+
* initialization cycle — the constants read as `undefined` depending on which module
|
|
12
|
+
* the runtime evaluates first (CLEANUP-01 F1). The barrel re-exports them, so every
|
|
13
|
+
* consumer outside core still reaches them at the same path.
|
|
14
|
+
*/
|
|
15
|
+
/** Promise upheld — no violation, the edit/push passes. */
|
|
16
|
+
export const EXIT_UPHOLD = 0;
|
|
17
|
+
/** Violation reported as a non-blocking signal. The covenant body's break code. */
|
|
18
|
+
export const EXIT_BREAK_NON_BLOCKING = 1;
|
|
19
|
+
/** Violation blocked — the edit/push is refused. Reserved for the wrapper / fail-closed. */
|
|
20
|
+
export const EXIT_BREAK_BLOCKING = 2;
|
package/dist/fail-policy.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* performs I/O and never throws. The single source of truth for "which failures
|
|
6
6
|
* block and which pass through" lives here, not scattered across call sites.
|
|
7
7
|
*/
|
|
8
|
-
import { EXIT_BREAK_BLOCKING, EXIT_UPHOLD } from './
|
|
8
|
+
import { EXIT_BREAK_BLOCKING, EXIT_UPHOLD } from './exit-codes.js';
|
|
9
9
|
/**
|
|
10
10
|
* Policy table (PRD §4.1). Null-prototype so lookups can never reach
|
|
11
11
|
* Object.prototype members ('__proto__', 'toString', …) — those must resolve
|
package/dist/index.d.ts
CHANGED
|
@@ -8,25 +8,12 @@
|
|
|
8
8
|
* See https://github.com/huskyhoochu/polydeukes
|
|
9
9
|
*/
|
|
10
10
|
export { ConfigValidationError, DEFAULT_TELEMETRY_LOG_PATH, type DisciplineEntry, type DisciplineForbid, defineConfig, type LanguageProfile, type PolydeukesConfig, type ResolvedConfig, type ResolvedLanguageProfile, } from './config.js';
|
|
11
|
+
export { EXIT_BREAK_BLOCKING, EXIT_BREAK_NON_BLOCKING, EXIT_UPHOLD, } from './exit-codes.js';
|
|
11
12
|
export { type FailMode, type FailureKind, failModeToExitCode, resolveFailMode, } from './fail-policy.js';
|
|
12
13
|
export { isPlainObject } from './is-plain-object.js';
|
|
13
14
|
export { normalizeProtectedPaths } from './protected-paths.js';
|
|
14
15
|
export { aggregateGain, appendRecord, appendRecordFailOpen, formatRecordLine, type GainSummary, parseRecordLine, readRecords, runGain, type TelemetryEvent, type TelemetryRecord, } from './telemetry.js';
|
|
15
16
|
export { type CanonicalTranscript, noopTranscript, type SubagentInvocation, type TranscriptToolCall, type TranscriptUserMessage, transcriptFromInput, } from './transcript.js';
|
|
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
17
|
/**
|
|
31
18
|
* `FileChange` — one file's mutation evidence around the judged call (CORE-06 §4.1).
|
|
32
19
|
*
|
package/dist/index.js
CHANGED
|
@@ -7,27 +7,15 @@
|
|
|
7
7
|
* appendRecordFailOpen — the fail-open wrapper promoted by CORE-05).
|
|
8
8
|
* See https://github.com/huskyhoochu/polydeukes
|
|
9
9
|
*/
|
|
10
|
+
import { EXIT_BREAK_BLOCKING, EXIT_BREAK_NON_BLOCKING, EXIT_UPHOLD } from './exit-codes.js';
|
|
10
11
|
import { isPlainObject } from './is-plain-object.js';
|
|
11
12
|
export { ConfigValidationError, DEFAULT_TELEMETRY_LOG_PATH, defineConfig, } from './config.js';
|
|
13
|
+
export { EXIT_BREAK_BLOCKING, EXIT_BREAK_NON_BLOCKING, EXIT_UPHOLD, } from './exit-codes.js';
|
|
12
14
|
export { failModeToExitCode, resolveFailMode, } from './fail-policy.js';
|
|
13
15
|
export { isPlainObject } from './is-plain-object.js';
|
|
14
16
|
export { normalizeProtectedPaths } from './protected-paths.js';
|
|
15
17
|
export { aggregateGain, appendRecord, appendRecordFailOpen, formatRecordLine, parseRecordLine, readRecords, runGain, } from './telemetry.js';
|
|
16
18
|
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
19
|
/**
|
|
32
20
|
* Deserialize stdin-JSON into a {@link CovenantInput} (the protocol's reverse direction).
|
|
33
21
|
*
|
package/dist/telemetry.d.ts
CHANGED
|
@@ -8,13 +8,18 @@
|
|
|
8
8
|
* than building its own logger.
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
|
-
* The
|
|
11
|
+
* The six telemetry events. `witnessed` is a first-class event, not a flag on `passed`:
|
|
12
12
|
* a break a human stood behind by supplying the pass condition themselves. `advised` is a
|
|
13
13
|
* violation verdict an advise-level observer recorded but let through; `skipped` is a
|
|
14
14
|
* discipline a surface could not judge at all (no evidence channel) — a no-op that shows
|
|
15
15
|
* up in the data instead of vanishing.
|
|
16
|
+
*
|
|
17
|
+
* `unattributed` is the one observation event among them: a protected entry whose state
|
|
18
|
+
* changed with no judgment row explaining it. It sits on a different axis from the five
|
|
19
|
+
* verdicts — `skipped` is an inability known up front, `unattributed` an attribution
|
|
20
|
+
* failure found after the fact — and it never blocks or passes a call.
|
|
16
21
|
*/
|
|
17
|
-
export type TelemetryEvent = 'passed' | 'blocked' | 'witnessed' | 'advised' | 'skipped';
|
|
22
|
+
export type TelemetryEvent = 'passed' | 'blocked' | 'witnessed' | 'advised' | 'skipped' | 'unattributed';
|
|
18
23
|
/**
|
|
19
24
|
* `TelemetryRecord` — one measured covenant outcome (PRD §4.1).
|
|
20
25
|
*
|
|
@@ -43,7 +48,7 @@ export declare function formatRecordLine(record: TelemetryRecord): string;
|
|
|
43
48
|
* Parse one TSV line back into a {@link TelemetryRecord}, or `null` if malformed (pure).
|
|
44
49
|
*
|
|
45
50
|
* Tolerates a trailing newline (so it round-trips {@link formatRecordLine}). Returns
|
|
46
|
-
* `null` for the wrong field count, an event outside the
|
|
51
|
+
* `null` for the wrong field count, an event outside the six valid events, or an
|
|
47
52
|
* empty line — a malformed line is rejected, never coerced into a bogus record. The one
|
|
48
53
|
* exception is {@link LEGACY_WITNESSED_EVENT}, which reads back as `witnessed`.
|
|
49
54
|
*/
|
|
@@ -87,7 +92,7 @@ export declare function readRecords(path: string): {
|
|
|
87
92
|
/**
|
|
88
93
|
* Aggregate records into per-label event counts (PRD §4.4, pure).
|
|
89
94
|
*
|
|
90
|
-
* Each label gets its own counter across all
|
|
95
|
+
* Each label gets its own counter across all six events, so a corrupt or missing
|
|
91
96
|
* event never bleeds counts between labels.
|
|
92
97
|
*/
|
|
93
98
|
export declare function aggregateGain(records: TelemetryRecord[]): GainSummary;
|
package/dist/telemetry.js
CHANGED
|
@@ -16,6 +16,7 @@ const VALID_EVENTS = [
|
|
|
16
16
|
'witnessed',
|
|
17
17
|
'advised',
|
|
18
18
|
'skipped',
|
|
19
|
+
'unattributed',
|
|
19
20
|
];
|
|
20
21
|
/**
|
|
21
22
|
* The event name `witnessed` was written under before the rename — a read-only migration
|
|
@@ -51,7 +52,7 @@ export function formatRecordLine(record) {
|
|
|
51
52
|
* Parse one TSV line back into a {@link TelemetryRecord}, or `null` if malformed (pure).
|
|
52
53
|
*
|
|
53
54
|
* Tolerates a trailing newline (so it round-trips {@link formatRecordLine}). Returns
|
|
54
|
-
* `null` for the wrong field count, an event outside the
|
|
55
|
+
* `null` for the wrong field count, an event outside the six valid events, or an
|
|
55
56
|
* empty line — a malformed line is rejected, never coerced into a bogus record. The one
|
|
56
57
|
* exception is {@link LEGACY_WITNESSED_EVENT}, which reads back as `witnessed`.
|
|
57
58
|
*/
|
|
@@ -144,14 +145,21 @@ export function readRecords(path) {
|
|
|
144
145
|
/**
|
|
145
146
|
* Aggregate records into per-label event counts (PRD §4.4, pure).
|
|
146
147
|
*
|
|
147
|
-
* Each label gets its own counter across all
|
|
148
|
+
* Each label gets its own counter across all six events, so a corrupt or missing
|
|
148
149
|
* event never bleeds counts between labels.
|
|
149
150
|
*/
|
|
150
151
|
export function aggregateGain(records) {
|
|
151
152
|
const counts = {};
|
|
152
153
|
for (const record of records) {
|
|
153
154
|
if (!(record.label in counts)) {
|
|
154
|
-
counts[record.label] = {
|
|
155
|
+
counts[record.label] = {
|
|
156
|
+
passed: 0,
|
|
157
|
+
blocked: 0,
|
|
158
|
+
witnessed: 0,
|
|
159
|
+
advised: 0,
|
|
160
|
+
skipped: 0,
|
|
161
|
+
unattributed: 0,
|
|
162
|
+
};
|
|
155
163
|
}
|
|
156
164
|
counts[record.label][record.event] += 1;
|
|
157
165
|
}
|
|
@@ -160,9 +168,10 @@ export function aggregateGain(records) {
|
|
|
160
168
|
/**
|
|
161
169
|
* Render a {@link GainSummary} into human-readable lines (pure).
|
|
162
170
|
*
|
|
163
|
-
* Each label is mentioned with its passed/blocked/witnessed/advised/skipped
|
|
164
|
-
* is a distinct column, never folded into another (PRD §4.4). A non-zero
|
|
165
|
-
* count is reported rather than hidden — silent skipping would mask log
|
|
171
|
+
* Each label is mentioned with its passed/blocked/witnessed/advised/skipped/unattributed
|
|
172
|
+
* counts; each is a distinct column, never folded into another (PRD §4.4). A non-zero
|
|
173
|
+
* corrupt-line count is reported rather than hidden — silent skipping would mask log
|
|
174
|
+
* corruption.
|
|
166
175
|
*
|
|
167
176
|
* Two different meanings share the word `skipped`: the per-label EVENT column above,
|
|
168
177
|
* and the unparseable-line count below. They are rendered on separate lines and never
|
|
@@ -174,7 +183,7 @@ function renderGain(summary, skipped) {
|
|
|
174
183
|
}
|
|
175
184
|
const lines = [`total ${summary.total}`];
|
|
176
185
|
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}`);
|
|
186
|
+
lines.push(`${label}: passed=${counts.passed} blocked=${counts.blocked} witnessed=${counts.witnessed} advised=${counts.advised} skipped=${counts.skipped} unattributed=${counts.unattributed}`);
|
|
178
187
|
}
|
|
179
188
|
if (skipped > 0) {
|
|
180
189
|
lines.push(`corrupt lines skipped=${skipped}`);
|
package/package.json
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"protectedPaths": {
|
|
20
20
|
"description": "Raw protected path patterns — normalization happens at resolve time.",
|
|
21
21
|
"type": "array",
|
|
22
|
-
"items": { "type": "string" }
|
|
22
|
+
"items": { "type": "string", "minLength": 1 }
|
|
23
23
|
},
|
|
24
24
|
"adapters": {
|
|
25
25
|
"description": "Adapter namespaces — keys are adapter names, values are each adapter's own settings object (contents not validated by the core schema).",
|
|
@@ -64,8 +64,10 @@
|
|
|
64
64
|
"additionalProperties": false,
|
|
65
65
|
"properties": {
|
|
66
66
|
"logPath": {
|
|
67
|
-
"description": "Telemetry log path. Defaults to .polydeukes/roi.log when omitted.",
|
|
68
|
-
"type": "string"
|
|
67
|
+
"description": "Telemetry log path. Defaults to .polydeukes/roi.log when omitted. Must be non-empty after trimming.",
|
|
68
|
+
"type": "string",
|
|
69
|
+
"minLength": 1,
|
|
70
|
+
"pattern": "\\S"
|
|
69
71
|
}
|
|
70
72
|
}
|
|
71
73
|
},
|
|
@@ -136,13 +138,13 @@
|
|
|
136
138
|
"forbid": {
|
|
137
139
|
"description": "Delta family — string shorthand is equivalent to { added }.",
|
|
138
140
|
"anyOf": [
|
|
139
|
-
{ "type": "string", "format": "regex" },
|
|
141
|
+
{ "type": "string", "minLength": 1, "format": "regex" },
|
|
140
142
|
{
|
|
141
143
|
"type": "object",
|
|
142
144
|
"additionalProperties": false,
|
|
143
145
|
"required": ["added"],
|
|
144
146
|
"properties": {
|
|
145
|
-
"added": { "type": "string", "format": "regex" }
|
|
147
|
+
"added": { "type": "string", "minLength": 1, "format": "regex" }
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
150
|
]
|
|
@@ -172,6 +174,7 @@
|
|
|
172
174
|
"forbidCommand": {
|
|
173
175
|
"description": "Command family — regex over shell command strings.",
|
|
174
176
|
"type": "string",
|
|
177
|
+
"minLength": 1,
|
|
175
178
|
"format": "regex"
|
|
176
179
|
}
|
|
177
180
|
}
|