@polydeukes/core 0.4.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 +61 -34
- package/dist/config.js +158 -109
- package/dist/exit-codes.d.ts +9 -9
- package/dist/exit-codes.js +9 -9
- package/dist/fail-policy.d.ts +7 -7
- package/dist/fail-policy.js +7 -7
- package/dist/index.d.ts +12 -14
- package/dist/index.js +6 -7
- 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 +15 -17
- package/dist/telemetry.js +21 -23
- package/dist/transcript.d.ts +16 -19
- package/dist/transcript.js +11 -14
- package/package.json +1 -1
- package/schema/polydeukes.schema.json +30 -4
package/dist/config.d.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config schema
|
|
2
|
+
* Config schema and the `defineConfig()` validator — config as data.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* it values the compiler never saw). It stays a pure function — zero file I/O, zero runtime
|
|
9
|
-
* dependencies (hand-rolled validation; the published JSON Schema is a sibling artifact the
|
|
10
|
-
* source never reads).
|
|
4
|
+
* The single settings surface the areas share. The input is pure JSON-representable data,
|
|
5
|
+
* and `defineConfig` is the runtime validator for parsed unknown values the compiler never
|
|
6
|
+
* saw. It stays a pure function — no file I/O and no runtime dependencies: validation is
|
|
7
|
+
* hand-rolled, and the published JSON Schema is a sibling artifact this source never reads.
|
|
11
8
|
*/
|
|
12
|
-
/** Conventional default telemetry log path
|
|
9
|
+
/** Conventional default telemetry log path — local-only observation data. */
|
|
13
10
|
export declare const DEFAULT_TELEMETRY_LOG_PATH = ".polydeukes/roi.log";
|
|
14
11
|
/**
|
|
15
|
-
* `LanguageProfile` — the unit of the language axis
|
|
12
|
+
* `LanguageProfile` — the unit of the language axis.
|
|
16
13
|
*
|
|
17
14
|
* `testCmd` is a shell command template: every literal `{scope}` token is substituted at
|
|
18
15
|
* resolve time, and the core only carries the resulting string — it never interprets it.
|
|
@@ -27,16 +24,22 @@ export type LanguageProfile = {
|
|
|
27
24
|
testCmd: string;
|
|
28
25
|
};
|
|
29
26
|
/**
|
|
30
|
-
* `DisciplineForbid` — the delta-family predicate value
|
|
27
|
+
* `DisciplineForbid` — the delta-family predicate value.
|
|
31
28
|
*
|
|
32
|
-
* The string shorthand is equivalent to `{ added }
|
|
33
|
-
*
|
|
29
|
+
* The string shorthand is equivalent to `{ added }`. It is the only direction that exists;
|
|
30
|
+
* anything else is rejected by validation.
|
|
34
31
|
*/
|
|
35
32
|
export type DisciplineForbid = string | {
|
|
36
33
|
added: string;
|
|
37
34
|
};
|
|
38
35
|
/**
|
|
39
|
-
* `
|
|
36
|
+
* `EnforceLevel` — an entry's own rung on the promotion ladder. `advise` records a break
|
|
37
|
+
* without stopping it; `block` pins the entry at block whatever default the ladder later
|
|
38
|
+
* adopts. Absence means advise; `block` is the promotion rung.
|
|
39
|
+
*/
|
|
40
|
+
export type EnforceLevel = 'block' | 'advise';
|
|
41
|
+
/**
|
|
42
|
+
* `DisciplineEntry` — one user-declared discipline. Pure JSON data.
|
|
40
43
|
*
|
|
41
44
|
* Exactly one predicate key (`forbid` | `immutable` | `forbidCommand` |
|
|
42
45
|
* `requirePrecedent`) per entry; `in`/`except` scope the delta and context families.
|
|
@@ -46,8 +49,10 @@ export type DisciplineForbid = string | {
|
|
|
46
49
|
export type DisciplineEntry = {
|
|
47
50
|
/** unique handle — telemetry label and verdict reason prefix */
|
|
48
51
|
id: string;
|
|
49
|
-
/** prose rationale
|
|
52
|
+
/** prose rationale — never judged, and carried into the break message */
|
|
50
53
|
why?: string;
|
|
54
|
+
/** the author's level; composes with the observer's surface level, lenient side winning */
|
|
55
|
+
enforce?: EnforceLevel;
|
|
51
56
|
/** delta/context-family scope: glob(s) the file path must match (absent = every file change) */
|
|
52
57
|
in?: string | string[];
|
|
53
58
|
/** delta/context-family scope: glob(s) excluded after `in` */
|
|
@@ -63,44 +68,61 @@ export type DisciplineEntry = {
|
|
|
63
68
|
/**
|
|
64
69
|
* context family — the session evidence one edit requires beforehand. Exactly one
|
|
65
70
|
* evidence key. The core owns and fully validates `command`; every other key is
|
|
66
|
-
* adapter vocabulary whose value passes through verbatim
|
|
71
|
+
* adapter vocabulary whose value passes through verbatim.
|
|
67
72
|
*/
|
|
68
73
|
requirePrecedent?: Record<string, unknown>;
|
|
69
74
|
};
|
|
70
75
|
/**
|
|
71
|
-
* `
|
|
76
|
+
* `DisciplineDraft` — an unpromoted discipline: the promotion ladder's first rung,
|
|
77
|
+
* registered as prose ahead of any predicate.
|
|
78
|
+
*
|
|
79
|
+
* A draft is declared, never inferred — only the literal `draft: true` makes one, and an
|
|
80
|
+
* entry with neither a predicate nor the marker stays a validation error. It carries no
|
|
81
|
+
* predicate, scope, or trigger key, produces no registration, no judgment, and no
|
|
82
|
+
* telemetry row; `pdks explain` renders it as unpromoted.
|
|
83
|
+
*/
|
|
84
|
+
export type DisciplineDraft = {
|
|
85
|
+
/** unique handle in the same label space as judged entries and meta-covenant labels */
|
|
86
|
+
id: string;
|
|
87
|
+
/** the draft's whole body — required prose, unlike the judged families' optional why */
|
|
88
|
+
why: string;
|
|
89
|
+
/** the explicit marker; only the literal true exists (false is rejected as dead data) */
|
|
90
|
+
draft: true;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* `PolydeukesConfig` — the input shape a user writes. JSON-serializable data.
|
|
72
94
|
*
|
|
73
95
|
* Language keys (`typescript`, `python`, …) are user *values*, not the core's vocabulary —
|
|
74
96
|
* no language or tool literal appears in the core source.
|
|
75
97
|
*/
|
|
76
98
|
export type PolydeukesConfig = {
|
|
77
|
-
/** IDE schema reference
|
|
99
|
+
/** IDE schema reference — accepted and ignored, never part of the resolution */
|
|
78
100
|
$schema?: string;
|
|
79
101
|
/** language axis, first-class. keys are user values ('typescript', 'python', …) */
|
|
80
102
|
languages: Record<string, LanguageProfile>;
|
|
81
|
-
/** raw protected path patterns —
|
|
103
|
+
/** raw protected path patterns — normalized downstream, never here */
|
|
82
104
|
protectedPaths?: string[];
|
|
83
105
|
/**
|
|
84
|
-
* adapter namespaces
|
|
85
|
-
*
|
|
86
|
-
*
|
|
106
|
+
* adapter namespaces — keys are ecosystem values (never validated), each value is that
|
|
107
|
+
* adapter's own settings object, passed through verbatim (the vocabulary belongs to the
|
|
108
|
+
* adapter, whose own validator judges the contents)
|
|
87
109
|
*/
|
|
88
110
|
adapters?: Record<string, Record<string, unknown>>;
|
|
89
111
|
telemetry?: {
|
|
90
|
-
/** conventional default applies when omitted
|
|
112
|
+
/** conventional default applies when omitted */
|
|
91
113
|
logPath?: string;
|
|
92
114
|
};
|
|
93
115
|
/** user-declared disciplines — validated here, compiled by the covenant package */
|
|
94
|
-
disciplines?: DisciplineEntry[];
|
|
116
|
+
disciplines?: (DisciplineEntry | DisciplineDraft)[];
|
|
95
117
|
/**
|
|
96
|
-
* TTL witness values for the covenant valve seam
|
|
97
|
-
*
|
|
118
|
+
* TTL witness values for the covenant valve seam — consumed at assembly time,
|
|
119
|
+
* validated here
|
|
98
120
|
*/
|
|
99
121
|
witness?: {
|
|
100
122
|
/**
|
|
101
123
|
* the agreed phrase a human types alone on a message's first line — quoting it
|
|
102
|
-
* mid-sentence is a mention, not an invocation
|
|
103
|
-
*
|
|
124
|
+
* mid-sentence is a mention, not an invocation. Non-empty after trimming; the value
|
|
125
|
+
* itself is free, since provenance rather than secrecy is the defence
|
|
104
126
|
*/
|
|
105
127
|
token: string;
|
|
106
128
|
/** validity window in minutes from the user message's timestamp — finite and > 0 */
|
|
@@ -110,11 +132,11 @@ export type PolydeukesConfig = {
|
|
|
110
132
|
/**
|
|
111
133
|
* `ResolvedLanguageProfile` — a {@link LanguageProfile} with its template compiled.
|
|
112
134
|
*
|
|
113
|
-
* Consumers keep the callable shape (`testCmd(scope)`)
|
|
135
|
+
* Consumers keep the callable shape (`testCmd(scope)`).
|
|
114
136
|
*/
|
|
115
137
|
export type ResolvedLanguageProfile = {
|
|
116
138
|
productionGlob: string | string[];
|
|
117
|
-
/** compiled from the template — consumers keep the callable shape
|
|
139
|
+
/** compiled from the template — consumers keep the callable shape */
|
|
118
140
|
testCmd: (scope: string) => string;
|
|
119
141
|
};
|
|
120
142
|
/**
|
|
@@ -130,8 +152,14 @@ export type ResolvedConfig = {
|
|
|
130
152
|
telemetry: {
|
|
131
153
|
logPath: string;
|
|
132
154
|
};
|
|
133
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* validated judged entries only — drafts are split out at resolution time so the
|
|
157
|
+
* covenant compiler has no path that receives one. Present whenever the input
|
|
158
|
+
* declared a `disciplines` array, holding exactly its judged entries in order.
|
|
159
|
+
*/
|
|
134
160
|
disciplines?: DisciplineEntry[];
|
|
161
|
+
/** validated drafts in declaration order (absent when the input carries none) */
|
|
162
|
+
drafts?: DisciplineDraft[];
|
|
135
163
|
/** validated witness data, passed through verbatim (absent stays absent) */
|
|
136
164
|
witness?: {
|
|
137
165
|
token: string;
|
|
@@ -139,7 +167,7 @@ export type ResolvedConfig = {
|
|
|
139
167
|
};
|
|
140
168
|
};
|
|
141
169
|
/**
|
|
142
|
-
* `ConfigValidationError` — raised when a config fails structural validation
|
|
170
|
+
* `ConfigValidationError` — raised when a config fails structural validation.
|
|
143
171
|
*
|
|
144
172
|
* The message names the offending field path so the developer sees exactly what is wrong.
|
|
145
173
|
* This throw is a developer-time error (config authoring), a different axis from the
|
|
@@ -150,8 +178,7 @@ export declare class ConfigValidationError extends Error {
|
|
|
150
178
|
}
|
|
151
179
|
/**
|
|
152
180
|
* Validate parsed unknown data as a {@link PolydeukesConfig} and return a
|
|
153
|
-
* {@link ResolvedConfig} with defaults filled and templates compiled
|
|
154
|
-
* Pure — no file I/O.
|
|
181
|
+
* {@link ResolvedConfig} with defaults filled and templates compiled. Pure — no file I/O.
|
|
155
182
|
*
|
|
156
183
|
* Throws {@link ConfigValidationError} (naming the offending field path) when the top level
|
|
157
184
|
* is not a plain object, any object level carries an unknown key, `languages` is
|
package/dist/config.js
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config schema
|
|
2
|
+
* Config schema and the `defineConfig()` validator — config as data.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* it values the compiler never saw). It stays a pure function — zero file I/O, zero runtime
|
|
9
|
-
* dependencies (hand-rolled validation; the published JSON Schema is a sibling artifact the
|
|
10
|
-
* source never reads).
|
|
4
|
+
* The single settings surface the areas share. The input is pure JSON-representable data,
|
|
5
|
+
* and `defineConfig` is the runtime validator for parsed unknown values the compiler never
|
|
6
|
+
* saw. It stays a pure function — no file I/O and no runtime dependencies: validation is
|
|
7
|
+
* hand-rolled, and the published JSON Schema is a sibling artifact this source never reads.
|
|
11
8
|
*/
|
|
12
9
|
import { isPlainObject } from './is-plain-object.js';
|
|
13
|
-
/** Conventional default telemetry log path
|
|
10
|
+
/** Conventional default telemetry log path — local-only observation data. */
|
|
14
11
|
export const DEFAULT_TELEMETRY_LOG_PATH = '.polydeukes/roi.log';
|
|
15
12
|
/**
|
|
16
|
-
* `ConfigValidationError` — raised when a config fails structural validation
|
|
13
|
+
* `ConfigValidationError` — raised when a config fails structural validation.
|
|
17
14
|
*
|
|
18
15
|
* The message names the offending field path so the developer sees exactly what is wrong.
|
|
19
16
|
* This throw is a developer-time error (config authoring), a different axis from the
|
|
@@ -25,7 +22,8 @@ export class ConfigValidationError extends Error {
|
|
|
25
22
|
this.name = 'ConfigValidationError';
|
|
26
23
|
}
|
|
27
24
|
}
|
|
28
|
-
/**
|
|
25
|
+
/** Labels the assembly reserves for the judging chain's own registrations. */
|
|
26
|
+
const META_COVENANT_LABELS = ['self-mod', 'shell-mod', 'transcript-mod'];
|
|
29
27
|
const TOP_LEVEL_KEYS = new Set([
|
|
30
28
|
'$schema',
|
|
31
29
|
'languages',
|
|
@@ -41,6 +39,7 @@ const WITNESS_KEYS = new Set(['token', 'ttlMinutes']);
|
|
|
41
39
|
const DISCIPLINE_KEYS = new Set([
|
|
42
40
|
'id',
|
|
43
41
|
'why',
|
|
42
|
+
'enforce',
|
|
44
43
|
'in',
|
|
45
44
|
'except',
|
|
46
45
|
'forbid',
|
|
@@ -49,8 +48,10 @@ const DISCIPLINE_KEYS = new Set([
|
|
|
49
48
|
'when',
|
|
50
49
|
'requirePrecedent',
|
|
51
50
|
]);
|
|
51
|
+
const DRAFT_KEYS = new Set(['id', 'why', 'draft']);
|
|
52
|
+
const ENFORCE_LEVELS = new Set(['block', 'advise']);
|
|
52
53
|
const PREDICATE_KEYS = ['forbid', 'immutable', 'forbidCommand', 'requirePrecedent'];
|
|
53
|
-
/** Predicate families that `in`/`except` may scope — delta and context
|
|
54
|
+
/** Predicate families that `in`/`except` may scope — delta and context. */
|
|
54
55
|
const SCOPED_PREDICATE_KEYS = new Set(['forbid', 'requirePrecedent']);
|
|
55
56
|
/** Throw on the first key outside the allowed vocabulary, naming the key and its location. */
|
|
56
57
|
function rejectUnknownKeys(record, allowed, location) {
|
|
@@ -75,7 +76,7 @@ function isValidGlob(glob) {
|
|
|
75
76
|
return false;
|
|
76
77
|
}
|
|
77
78
|
/**
|
|
78
|
-
* Compile a `{scope}` template into the callable consumers use
|
|
79
|
+
* Compile a `{scope}` template into the callable consumers use.
|
|
79
80
|
*
|
|
80
81
|
* Exactly the literal token `{scope}` is substituted, at every occurrence (`replaceAll`
|
|
81
82
|
* semantics). Other braces (`${VAR}`, `{a,b}`, `awk '{print}'`) are the shell's own
|
|
@@ -96,14 +97,13 @@ function rejectUncompilableRegex(pattern, location) {
|
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
/**
|
|
99
|
-
* Validate a context-family `requirePrecedent` value
|
|
100
|
+
* Validate a context-family `requirePrecedent` value.
|
|
100
101
|
*
|
|
101
102
|
* Evidence vocabulary is layered: the container (a flat object holding exactly one
|
|
102
103
|
* evidence key) is the core's, and so is the `command` key — a shell command is the
|
|
103
104
|
* agent-crossing surface, fully validated here. Every other key belongs to an adapter,
|
|
104
105
|
* whose own validator judges the value; the core passes it through verbatim and never
|
|
105
|
-
* inspects it
|
|
106
|
-
* assembly time, not here.
|
|
106
|
+
* inspects it. An unrecognized evidence key fails closed at assembly time, not here.
|
|
107
107
|
*/
|
|
108
108
|
function validateRequirePrecedent(evidence, location) {
|
|
109
109
|
if (!isPlainObject(evidence)) {
|
|
@@ -121,14 +121,128 @@ function validateRequirePrecedent(evidence, location) {
|
|
|
121
121
|
rejectUncompilableRegex(command, `${location} requirePrecedent.command`);
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
+
/** Validate a draft entry and return it as data. */
|
|
125
|
+
function validateDraft(entry, id, location) {
|
|
126
|
+
for (const key of Object.keys(entry)) {
|
|
127
|
+
if (!DRAFT_KEYS.has(key)) {
|
|
128
|
+
// Named as the draft rule, not as an unknown key: `forbid` et al. are legal
|
|
129
|
+
// discipline keys, just not on a draft.
|
|
130
|
+
throw new ConfigValidationError(`${location} allows only id, why, draft on a draft entry (found '${key}')`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (entry.draft !== true) {
|
|
134
|
+
throw new ConfigValidationError(`${location} draft must be the literal true`);
|
|
135
|
+
}
|
|
136
|
+
if (typeof entry.why !== 'string' || entry.why.length === 0) {
|
|
137
|
+
throw new ConfigValidationError(`${location} why must be a non-empty string on a draft entry — the prose is its whole body`);
|
|
138
|
+
}
|
|
139
|
+
return { id, why: entry.why, draft: true };
|
|
140
|
+
}
|
|
141
|
+
/** Validate the family-independent rules of a judged entry and return its predicate key. */
|
|
142
|
+
function validateJudgedHead(entry, location) {
|
|
143
|
+
rejectUnknownKeys(entry, DISCIPLINE_KEYS, location);
|
|
144
|
+
if (entry.why !== undefined && typeof entry.why !== 'string') {
|
|
145
|
+
throw new ConfigValidationError(`${location} why must be a string`);
|
|
146
|
+
}
|
|
147
|
+
if (entry.enforce !== undefined &&
|
|
148
|
+
(typeof entry.enforce !== 'string' || !ENFORCE_LEVELS.has(entry.enforce))) {
|
|
149
|
+
throw new ConfigValidationError(`${location} enforce must be 'block' or 'advise'`);
|
|
150
|
+
}
|
|
151
|
+
const predicates = PREDICATE_KEYS.filter((key) => entry[key] !== undefined);
|
|
152
|
+
if (predicates.length !== 1) {
|
|
153
|
+
throw new ConfigValidationError(`${location} must have exactly one predicate key ` +
|
|
154
|
+
`(forbid | immutable | forbidCommand | requirePrecedent)`);
|
|
155
|
+
}
|
|
156
|
+
const predicate = predicates[0];
|
|
157
|
+
if (!SCOPED_PREDICATE_KEYS.has(predicate) &&
|
|
158
|
+
(entry.in !== undefined || entry.except !== undefined)) {
|
|
159
|
+
throw new ConfigValidationError(`${location} allows in/except only on a forbid or requirePrecedent entry`);
|
|
160
|
+
}
|
|
161
|
+
// `when` is the context family's trigger; on any other family it would be dead data
|
|
162
|
+
// implying a trigger that is never applied.
|
|
163
|
+
if (entry.when !== undefined && predicate !== 'requirePrecedent') {
|
|
164
|
+
throw new ConfigValidationError(`${location} allows when only on a requirePrecedent entry`);
|
|
165
|
+
}
|
|
166
|
+
if (entry.in !== undefined && !isValidGlob(entry.in)) {
|
|
167
|
+
throw new ConfigValidationError(`${location} in must be a non-empty glob or glob array`);
|
|
168
|
+
}
|
|
169
|
+
if (entry.except !== undefined && !isValidGlob(entry.except)) {
|
|
170
|
+
throw new ConfigValidationError(`${location} except must be a non-empty glob or glob array`);
|
|
171
|
+
}
|
|
172
|
+
return predicate;
|
|
173
|
+
}
|
|
174
|
+
function validateForbid(entry, location) {
|
|
175
|
+
const forbid = entry.forbid;
|
|
176
|
+
if (typeof forbid === 'string') {
|
|
177
|
+
// An empty pattern matches at every position, so the entry would break every
|
|
178
|
+
// in-scope change — rejected like every sibling pattern field.
|
|
179
|
+
if (forbid.length === 0) {
|
|
180
|
+
throw new ConfigValidationError(`${location} forbid must be a non-empty string pattern`);
|
|
181
|
+
}
|
|
182
|
+
rejectUncompilableRegex(forbid, `${location} forbid`);
|
|
183
|
+
}
|
|
184
|
+
else if (isPlainObject(forbid)) {
|
|
185
|
+
const keys = Object.keys(forbid);
|
|
186
|
+
if (keys.length !== 1 || keys[0] !== 'added' || typeof forbid.added !== 'string') {
|
|
187
|
+
throw new ConfigValidationError(`${location} forbid object must have exactly one key 'added' with a string pattern`);
|
|
188
|
+
}
|
|
189
|
+
if (forbid.added.length === 0) {
|
|
190
|
+
throw new ConfigValidationError(`${location} forbid.added must be a non-empty string pattern`);
|
|
191
|
+
}
|
|
192
|
+
rejectUncompilableRegex(forbid.added, `${location} forbid.added`);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
throw new ConfigValidationError(`${location} forbid must be a string pattern or an { added } object`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function validateImmutable(entry, location) {
|
|
199
|
+
if (!isValidGlob(entry.immutable)) {
|
|
200
|
+
throw new ConfigValidationError(`${location} immutable must be a non-empty glob or glob array`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function validateForbidCommand(entry, location) {
|
|
204
|
+
if (typeof entry.forbidCommand !== 'string') {
|
|
205
|
+
throw new ConfigValidationError(`${location} forbidCommand must be a string pattern`);
|
|
206
|
+
}
|
|
207
|
+
if (entry.forbidCommand.length === 0) {
|
|
208
|
+
// An empty pattern matches every command line — one typo would block every
|
|
209
|
+
// shell call the entry sees.
|
|
210
|
+
throw new ConfigValidationError(`${location} forbidCommand must be a non-empty string pattern`);
|
|
211
|
+
}
|
|
212
|
+
rejectUncompilableRegex(entry.forbidCommand, `${location} forbidCommand`);
|
|
213
|
+
}
|
|
214
|
+
function validateContextEntry(entry, location) {
|
|
215
|
+
if (entry.when !== undefined) {
|
|
216
|
+
if (typeof entry.when !== 'string') {
|
|
217
|
+
throw new ConfigValidationError(`${location} when must be a string pattern`);
|
|
218
|
+
}
|
|
219
|
+
if (entry.when.length === 0) {
|
|
220
|
+
// An empty pattern matches at every position, so the trigger would fire on any
|
|
221
|
+
// file that merely grows — reject it like every sibling pattern field.
|
|
222
|
+
throw new ConfigValidationError(`${location} when must be a non-empty string pattern`);
|
|
223
|
+
}
|
|
224
|
+
rejectUncompilableRegex(entry.when, `${location} when`);
|
|
225
|
+
}
|
|
226
|
+
validateRequirePrecedent(entry.requirePrecedent, location);
|
|
227
|
+
}
|
|
228
|
+
/** One validator per family, keyed by the predicate that selects the family. */
|
|
229
|
+
const PREDICATE_VALIDATORS = {
|
|
230
|
+
forbid: validateForbid,
|
|
231
|
+
immutable: validateImmutable,
|
|
232
|
+
forbidCommand: validateForbidCommand,
|
|
233
|
+
requirePrecedent: validateContextEntry,
|
|
234
|
+
};
|
|
124
235
|
/**
|
|
125
|
-
* Validate the `disciplines` array
|
|
126
|
-
* naming the offending entry/key; the validated data passes
|
|
236
|
+
* Validate the `disciplines` array and split judged entries from drafts. Throws
|
|
237
|
+
* {@link ConfigValidationError} naming the offending entry/key; the validated data passes
|
|
238
|
+
* through verbatim, in declaration order.
|
|
127
239
|
*/
|
|
128
240
|
function validateDisciplines(disciplines) {
|
|
129
241
|
if (!Array.isArray(disciplines)) {
|
|
130
242
|
throw new ConfigValidationError('disciplines must be an array');
|
|
131
243
|
}
|
|
244
|
+
const judged = [];
|
|
245
|
+
const drafts = [];
|
|
132
246
|
const seenIds = new Set();
|
|
133
247
|
disciplines.forEach((entry, index) => {
|
|
134
248
|
if (!isPlainObject(entry)) {
|
|
@@ -141,93 +255,26 @@ function validateDisciplines(disciplines) {
|
|
|
141
255
|
if (seenIds.has(entry.id)) {
|
|
142
256
|
throw new ConfigValidationError(`${location} duplicates the id of an earlier entry`);
|
|
143
257
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const predicates = PREDICATE_KEYS.filter((key) => entry[key] !== undefined);
|
|
150
|
-
if (predicates.length !== 1) {
|
|
151
|
-
throw new ConfigValidationError(`${location} must have exactly one predicate key ` +
|
|
152
|
-
`(forbid | immutable | forbidCommand | requirePrecedent)`);
|
|
258
|
+
// The three meta-covenant registrations share the telemetry label space with
|
|
259
|
+
// discipline ids; a colliding id would make gain aggregation and any label-keyed
|
|
260
|
+
// reader (pdks explain) ambiguous.
|
|
261
|
+
if (META_COVENANT_LABELS.includes(entry.id)) {
|
|
262
|
+
throw new ConfigValidationError(`${location} id collides with a meta-covenant label`);
|
|
153
263
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// implying a trigger that is never applied.
|
|
161
|
-
if (entry.when !== undefined && predicate !== 'requirePrecedent') {
|
|
162
|
-
throw new ConfigValidationError(`${location} allows when only on a requirePrecedent entry`);
|
|
163
|
-
}
|
|
164
|
-
if (entry.in !== undefined && !isValidGlob(entry.in)) {
|
|
165
|
-
throw new ConfigValidationError(`${location} in must be a non-empty glob or glob array`);
|
|
166
|
-
}
|
|
167
|
-
if (entry.except !== undefined && !isValidGlob(entry.except)) {
|
|
168
|
-
throw new ConfigValidationError(`${location} except must be a non-empty glob or glob array`);
|
|
169
|
-
}
|
|
170
|
-
if (predicate === 'forbid') {
|
|
171
|
-
const forbid = entry.forbid;
|
|
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
|
-
}
|
|
178
|
-
rejectUncompilableRegex(forbid, `${location} forbid`);
|
|
179
|
-
}
|
|
180
|
-
else if (isPlainObject(forbid)) {
|
|
181
|
-
// Only the { added } direction exists before COVENANT-12.
|
|
182
|
-
const keys = Object.keys(forbid);
|
|
183
|
-
if (keys.length !== 1 || keys[0] !== 'added' || typeof forbid.added !== 'string') {
|
|
184
|
-
throw new ConfigValidationError(`${location} forbid object must have exactly one key 'added' with a string pattern`);
|
|
185
|
-
}
|
|
186
|
-
if (forbid.added.length === 0) {
|
|
187
|
-
throw new ConfigValidationError(`${location} forbid.added must be a non-empty string pattern`);
|
|
188
|
-
}
|
|
189
|
-
rejectUncompilableRegex(forbid.added, `${location} forbid.added`);
|
|
190
|
-
}
|
|
191
|
-
else {
|
|
192
|
-
throw new ConfigValidationError(`${location} forbid must be a string pattern or an { added } object`);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
else if (predicate === 'immutable') {
|
|
196
|
-
if (!isValidGlob(entry.immutable)) {
|
|
197
|
-
throw new ConfigValidationError(`${location} immutable must be a non-empty glob or glob array`);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
else if (predicate === 'forbidCommand') {
|
|
201
|
-
if (typeof entry.forbidCommand !== 'string') {
|
|
202
|
-
throw new ConfigValidationError(`${location} forbidCommand must be a string pattern`);
|
|
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
|
-
}
|
|
209
|
-
rejectUncompilableRegex(entry.forbidCommand, `${location} forbidCommand`);
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
if (entry.when !== undefined) {
|
|
213
|
-
if (typeof entry.when !== 'string') {
|
|
214
|
-
throw new ConfigValidationError(`${location} when must be a string pattern`);
|
|
215
|
-
}
|
|
216
|
-
if (entry.when.length === 0) {
|
|
217
|
-
// An empty pattern matches at every position, so the trigger would fire on any
|
|
218
|
-
// file that merely grows — reject it like every sibling pattern field.
|
|
219
|
-
throw new ConfigValidationError(`${location} when must be a non-empty string pattern`);
|
|
220
|
-
}
|
|
221
|
-
rejectUncompilableRegex(entry.when, `${location} when`);
|
|
222
|
-
}
|
|
223
|
-
validateRequirePrecedent(entry.requirePrecedent, location);
|
|
264
|
+
seenIds.add(entry.id);
|
|
265
|
+
// Selected by the marker's value, so an explicit `draft: undefined` is absence,
|
|
266
|
+
// like every other optional key in this validator.
|
|
267
|
+
if (entry.draft !== undefined) {
|
|
268
|
+
drafts.push(validateDraft(entry, entry.id, location));
|
|
269
|
+
return;
|
|
224
270
|
}
|
|
271
|
+
const predicate = validateJudgedHead(entry, location);
|
|
272
|
+
PREDICATE_VALIDATORS[predicate](entry, location);
|
|
273
|
+
judged.push(entry);
|
|
225
274
|
});
|
|
226
|
-
return
|
|
275
|
+
return { judged, drafts };
|
|
227
276
|
}
|
|
228
|
-
/**
|
|
229
|
-
* Validate the `languages` map and compile each profile's `{scope}` template (PRD §4.1).
|
|
230
|
-
*/
|
|
277
|
+
/** Validate the `languages` map and compile each profile's `{scope}` template. */
|
|
231
278
|
function validateLanguages(languages) {
|
|
232
279
|
if (!isPlainObject(languages) || Object.keys(languages).length === 0) {
|
|
233
280
|
throw new ConfigValidationError('languages must be a non-empty object');
|
|
@@ -268,8 +315,8 @@ function validateProtectedPaths(protectedPaths) {
|
|
|
268
315
|
return protectedPaths;
|
|
269
316
|
}
|
|
270
317
|
/**
|
|
271
|
-
* Validate the `adapters` map
|
|
272
|
-
*
|
|
318
|
+
* Validate the `adapters` map — each namespace is a plain object whose contents belong to
|
|
319
|
+
* that adapter's own validator, so they pass through verbatim.
|
|
273
320
|
*/
|
|
274
321
|
function validateAdapters(adapters) {
|
|
275
322
|
// Array first: the removed directory-list form deserves a migration hint, not a
|
|
@@ -302,7 +349,7 @@ function validateTelemetry(telemetry) {
|
|
|
302
349
|
}
|
|
303
350
|
return telemetry.logPath;
|
|
304
351
|
}
|
|
305
|
-
/** Validate the `witness` section
|
|
352
|
+
/** Validate the `witness` section — both values are consumed at assembly time. */
|
|
306
353
|
function validateWitness(witness) {
|
|
307
354
|
if (!isPlainObject(witness)) {
|
|
308
355
|
throw new ConfigValidationError('witness must be an object');
|
|
@@ -319,8 +366,7 @@ function validateWitness(witness) {
|
|
|
319
366
|
}
|
|
320
367
|
/**
|
|
321
368
|
* Validate parsed unknown data as a {@link PolydeukesConfig} and return a
|
|
322
|
-
* {@link ResolvedConfig} with defaults filled and templates compiled
|
|
323
|
-
* Pure — no file I/O.
|
|
369
|
+
* {@link ResolvedConfig} with defaults filled and templates compiled. Pure — no file I/O.
|
|
324
370
|
*
|
|
325
371
|
* Throws {@link ConfigValidationError} (naming the offending field path) when the top level
|
|
326
372
|
* is not a plain object, any object level carries an unknown key, `languages` is
|
|
@@ -334,15 +380,17 @@ export function defineConfig(config) {
|
|
|
334
380
|
throw new ConfigValidationError('config must be a plain object');
|
|
335
381
|
}
|
|
336
382
|
rejectUnknownKeys(config, TOP_LEVEL_KEYS, 'config');
|
|
337
|
-
// `$schema` is an IDE schema reference
|
|
338
|
-
//
|
|
383
|
+
// `$schema` is an IDE schema reference: accepted, type-checked, and ignored — it never
|
|
384
|
+
// appears in the resolution output.
|
|
339
385
|
if (config.$schema !== undefined && typeof config.$schema !== 'string') {
|
|
340
386
|
throw new ConfigValidationError('$schema must be a string');
|
|
341
387
|
}
|
|
342
388
|
const resolvedLanguages = validateLanguages(config.languages);
|
|
343
389
|
const protectedPaths = config.protectedPaths !== undefined ? validateProtectedPaths(config.protectedPaths) : undefined;
|
|
344
390
|
const adapters = config.adapters !== undefined ? validateAdapters(config.adapters) : undefined;
|
|
345
|
-
const
|
|
391
|
+
const split = config.disciplines !== undefined ? validateDisciplines(config.disciplines) : undefined;
|
|
392
|
+
const disciplines = split?.judged;
|
|
393
|
+
const drafts = split !== undefined && split.drafts.length > 0 ? split.drafts : undefined;
|
|
346
394
|
const logPath = config.telemetry !== undefined ? validateTelemetry(config.telemetry) : undefined;
|
|
347
395
|
const witness = config.witness !== undefined ? validateWitness(config.witness) : undefined;
|
|
348
396
|
return {
|
|
@@ -353,6 +401,7 @@ export function defineConfig(config) {
|
|
|
353
401
|
logPath: logPath ?? DEFAULT_TELEMETRY_LOG_PATH,
|
|
354
402
|
},
|
|
355
403
|
...(disciplines !== undefined && { disciplines }),
|
|
404
|
+
...(drafts !== undefined && { drafts }),
|
|
356
405
|
...(witness !== undefined && { witness }),
|
|
357
406
|
};
|
|
358
407
|
}
|
package/dist/exit-codes.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* exit-codes — the covenant protocol's exit-code vocabulary
|
|
2
|
+
* exit-codes — the covenant protocol's exit-code vocabulary.
|
|
3
3
|
*
|
|
4
|
-
* The three codes are distinct and ordered by severity. The covenant *body* only
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The three codes are distinct and ordered by severity. The covenant *body* only ever
|
|
5
|
+
* emits `0` (uphold) or `1` (break, non-blocking); translating a break into the blocking
|
|
6
|
+
* `2` is the wrapper's job, never the core's. The sole place the core itself reaches for
|
|
7
|
+
* `2` is the fail-closed parse path in the barrel.
|
|
8
8
|
*
|
|
9
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* them: importing them from the barrel, which re-exports fail-policy, is an initialization
|
|
11
|
+
* cycle — the constants read as `undefined` depending on which module the runtime evaluates
|
|
12
|
+
* first. The barrel re-exports them, so every consumer outside core still reaches them at
|
|
13
|
+
* the same path.
|
|
14
14
|
*/
|
|
15
15
|
/** Promise upheld — no violation, the edit/push passes. */
|
|
16
16
|
export declare const EXIT_UPHOLD = 0;
|
package/dist/exit-codes.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* exit-codes — the covenant protocol's exit-code vocabulary
|
|
2
|
+
* exit-codes — the covenant protocol's exit-code vocabulary.
|
|
3
3
|
*
|
|
4
|
-
* The three codes are distinct and ordered by severity. The covenant *body* only
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The three codes are distinct and ordered by severity. The covenant *body* only ever
|
|
5
|
+
* emits `0` (uphold) or `1` (break, non-blocking); translating a break into the blocking
|
|
6
|
+
* `2` is the wrapper's job, never the core's. The sole place the core itself reaches for
|
|
7
|
+
* `2` is the fail-closed parse path in the barrel.
|
|
8
8
|
*
|
|
9
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* them: importing them from the barrel, which re-exports fail-policy, is an initialization
|
|
11
|
+
* cycle — the constants read as `undefined` depending on which module the runtime evaluates
|
|
12
|
+
* first. The barrel re-exports them, so every consumer outside core still reaches them at
|
|
13
|
+
* the same path.
|
|
14
14
|
*/
|
|
15
15
|
/** Promise upheld — no violation, the edit/push passes. */
|
|
16
16
|
export const EXIT_UPHOLD = 0;
|